diff --git a/Content.Server/Alert/Click/StopBeingPulled.cs b/Content.Server/Alert/Click/StopBeingPulled.cs index 11ae93117d..2b70e7e028 100644 --- a/Content.Server/Alert/Click/StopBeingPulled.cs +++ b/Content.Server/Alert/Click/StopBeingPulled.cs @@ -1,6 +1,8 @@ using Content.Shared.Alert; using Content.Shared.Pulling.Components; +using Content.Shared.Pulling; using JetBrains.Annotations; +using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Alert.Click @@ -14,7 +16,11 @@ namespace Content.Server.Alert.Click { public void AlertClicked(ClickAlertEventArgs args) { - args.Player.GetComponentOrNull()?.TryStopPull(args.Player); + var ps = EntitySystem.Get(); + if (args.Player.TryGetComponent(out var playerPullable)) + { + ps.TryStopPull(playerPullable); + } } } } diff --git a/Content.Server/Alert/Click/StopPulling.cs b/Content.Server/Alert/Click/StopPulling.cs index dd880abeff..37c68a742a 100644 --- a/Content.Server/Alert/Click/StopPulling.cs +++ b/Content.Server/Alert/Click/StopPulling.cs @@ -16,11 +16,13 @@ namespace Content.Server.Alert.Click { public void AlertClicked(ClickAlertEventArgs args) { - EntitySystem - .Get() - .GetPulled(args.Player)? - .GetComponentOrNull()? - .TryStopPull(); + var ps = EntitySystem.Get(); + var playerTargetPullable = ps.GetPulled(args.Player)? + .GetComponentOrNull(); + if (playerTargetPullable != null) + { + ps.TryStopPull(playerTargetPullable); + } } } } diff --git a/Content.Server/Buckle/Components/BuckleComponent.cs b/Content.Server/Buckle/Components/BuckleComponent.cs index f1472abd36..fe41940f2d 100644 --- a/Content.Server/Buckle/Components/BuckleComponent.cs +++ b/Content.Server/Buckle/Components/BuckleComponent.cs @@ -268,7 +268,7 @@ namespace Content.Server.Buckle.Components { if (ownerPullable.Puller != null) { - ownerPullable.TryStopPull(); + EntitySystem.Get().TryStopPull(ownerPullable); } } @@ -277,7 +277,7 @@ namespace Content.Server.Buckle.Components if (toPullable.Puller == Owner) { // can't pull it and buckle to it at the same time - toPullable.TryStopPull(); + EntitySystem.Get().TryStopPull(toPullable); } } diff --git a/Content.Server/Construction/Components/AnchorableComponent.cs b/Content.Server/Construction/Components/AnchorableComponent.cs index e813f18abc..3862bb39c7 100644 --- a/Content.Server/Construction/Components/AnchorableComponent.cs +++ b/Content.Server/Construction/Components/AnchorableComponent.cs @@ -79,7 +79,7 @@ namespace Content.Server.Construction.Components { if (pullableComponent.Puller != null) { - pullableComponent.TryStopPull(); + EntitySystem.Get().TryStopPull(pullableComponent); } } diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index e25da84dd1..fb93535cde 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -166,7 +166,7 @@ namespace Content.Server.Hands.Components || puller.Pulling == null || !puller.Pulling.TryGetComponent(out PullableComponent? pullable)) return false; - return pullable.TryStopPull(); + return _entitySystemManager.GetEntitySystem().TryStopPull(pullable); } #endregion diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 928c785c46..32e55c5c35 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -47,6 +47,7 @@ namespace Content.Server.Interaction [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] private readonly PullingSystem _pullSystem = default!; public override void Initialize() { @@ -313,7 +314,7 @@ namespace Content.Server.Interaction if (!pulledObject.TryGetComponent(out PullableComponent? pull)) return false; - return pull.TogglePull(userEntity); + return _pullSystem.TogglePull(userEntity, pull); } /// diff --git a/Content.Server/Physics/Controllers/PullController.cs b/Content.Server/Physics/Controllers/PullController.cs index 5ce023f43d..c02669bb0a 100644 --- a/Content.Server/Physics/Controllers/PullController.cs +++ b/Content.Server/Physics/Controllers/PullController.cs @@ -51,6 +51,10 @@ namespace Content.Server.Physics.Controllers 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; @@ -61,12 +65,17 @@ namespace Content.Server.Physics.Controllers continue; } - DebugTools.AssertNotNull(pullable.Puller); + if (pullable.Puller == null) + { + continue; + } + + // Now that's over with... var pullerPosition = pullable.Puller!.Transform.MapPosition; if (pullable.MovingTo.Value.MapId != pullerPosition.MapId) { - pullable.MovingTo = null; + _pullableSystem.StopMoveTo(pullable); continue; } @@ -74,22 +83,23 @@ namespace Content.Server.Physics.Controllers physics.BodyType == BodyType.Static || pullable.MovingTo.Value.MapId != pullable.Owner.Transform.MapID) { - pullable.MovingTo = null; + _pullableSystem.StopMoveTo(pullable); continue; } var movingPosition = pullable.MovingTo.Value.Position; var ownerPosition = pullable.Owner.Transform.MapPosition.Position; - if (movingPosition.EqualsApprox(ownerPosition, MaximumSettleDistance) && (physics.LinearVelocity.Length < MaximumSettleVelocity)) + var diff = movingPosition - ownerPosition; + var diffLength = diff.Length; + + if ((diffLength < MaximumSettleDistance) && (physics.LinearVelocity.Length < MaximumSettleVelocity)) { physics.LinearVelocity = Vector2.Zero; - pullable.MovingTo = null; + _pullableSystem.StopMoveTo(pullable); continue; } - var diff = movingPosition - ownerPosition; - var diffLength = diff.Length; 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; @@ -102,6 +112,7 @@ namespace Content.Server.Physics.Controllers var scaling = (SettleShutdownDistance - diffLength) / SettleShutdownDistance; accel -= physics.LinearVelocity * SettleShutdownMultiplier * scaling; } + physics.WakeBody(); physics.ApplyLinearImpulse(accel * physics.Mass * frameTime); } } diff --git a/Content.Server/Pulling/PullableComponent.cs b/Content.Server/Pulling/PullableComponent.cs index d6bfcd7bb6..2c1d3fb767 100644 --- a/Content.Server/Pulling/PullableComponent.cs +++ b/Content.Server/Pulling/PullableComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Pulling.Components; +using Content.Shared.Pulling; using Content.Shared.Verbs; using Robust.Shared.GameObjects; using Robust.Shared.Localization; @@ -50,11 +51,11 @@ namespace Content.Server.Pulling // Why no reason? Because they're supposed to be performed in TryStartPull. if (component.Puller == user) { - component.TryStopPull(); + EntitySystem.Get().TryStopPull(component); } else { - component.TryStartPull(user); + EntitySystem.Get().TryStartPull(component.Owner, user); } } } diff --git a/Content.Server/Pulling/PullingSystem.cs b/Content.Server/Pulling/PullingSystem.cs index 8e26a50060..99e9e45aae 100644 --- a/Content.Server/Pulling/PullingSystem.cs +++ b/Content.Server/Pulling/PullingSystem.cs @@ -44,7 +44,7 @@ namespace Content.Server.Pulling return; } - pullable.TryStopPull(); + TryStopPull(pullable); } } } diff --git a/Content.Shared/Pulling/Components/SharedPullableComponent.cs b/Content.Shared/Pulling/Components/SharedPullableComponent.cs index 0030ba1638..0d967044e5 100644 --- a/Content.Shared/Pulling/Components/SharedPullableComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullableComponent.cs @@ -1,5 +1,6 @@ using System; using Content.Shared.Physics.Pull; +using Robust.Shared.Analyzers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; @@ -12,311 +13,32 @@ using Robust.Shared.Serialization; namespace Content.Shared.Pulling.Components { + // Before you try to add another type than SharedPullingStateManagementSystem, consider the can of worms you may be opening! [NetworkedComponent()] + [Friend(typeof(SharedPullingStateManagementSystem))] public abstract class SharedPullableComponent : Component { public override string Name => "Pullable"; + // At this point this field exists solely for the component dependency (which is mandatory). [ComponentDependency] private readonly PhysicsComponent? _physics = default!; - /// - /// Only set in Puller->set! Only set in unison with _pullerPhysics! - /// - private IEntity? _puller; - - public IPhysBody? PullerPhysics { get; private set; } - - private DistanceJoint? _pullJoint; - - public float? MaxDistance => _pullJoint?.MaxLength; - - private MapCoordinates? _movingTo; + public float? MaxDistance => PullJoint?.MaxLength; /// /// The current entity pulling this component. - /// Setting this performs the entire setup process for pulling. + /// Ideally, alter using TryStartPull and TryStopPull. /// - public virtual IEntity? Puller - { - get => _puller; - set - { - if (_puller == value) - { - return; - } - - var eventBus = Owner.EntityManager.EventBus; - // TODO: JESUS - - // New value. Abandon being pulled by any existing object. - if (_puller != null) - { - var oldPuller = _puller; - var oldPullerPhysics = PullerPhysics; - - if (_puller.TryGetComponent(out SharedPullerComponent? puller)) - { - puller.Pulling = null; - } - - _puller = null; - Dirty(); - PullerPhysics = null; - - if (_physics != null && oldPullerPhysics != null) - { - var message = new PullStoppedMessage(oldPullerPhysics, _physics); - - eventBus.RaiseLocalEvent(oldPuller.Uid, message, broadcast: false); - - if (Owner.LifeStage <= EntityLifeStage.MapInitialized) - eventBus.RaiseLocalEvent(Owner.Uid, message); - - _physics.WakeBody(); - } - - // else-branch warning is handled below - } - - // Now that is settled, prepare to be pulled by a new object. - if (_physics == null) - { - Logger.WarningS("c.go.c.pulling", "Well now you've done it, haven't you? SharedPullableComponent on {0} didn't have an IPhysBody.", Owner); - return; - } - - if (value == null) - { - MovingTo = null; - } - else - { - // Pulling a new object : Perform sanity checks. - - if (!_canStartPull(value)) - { - return; - } - - if (!value.TryGetComponent(out var pullerPhysics)) - { - return; - } - - if (!value.TryGetComponent(out var valuePuller)) - { - return; - } - - // 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 oldPulling = valuePuller.Pulling; - if (oldPulling != null) - { - if (oldPulling.TryGetComponent(out SharedPullableComponent? pullable)) - { - pullable.TryStopPull(); - } - else - { - Logger.WarningS("c.go.c.pulling", "Well now you've done it, haven't you? Someone transferred pulling to this component (on {0}) while presently pulling something that has no Pullable component (on {1})!", Owner, oldPulling); - return; - } - } - - // Continue with pulling process. - - var pullAttempt = new PullAttemptMessage(pullerPhysics, _physics); - - eventBus.RaiseLocalEvent(value.Uid, pullAttempt, broadcast: false); - - if (pullAttempt.Cancelled) - { - return; - } - - eventBus.RaiseLocalEvent(Owner.Uid, pullAttempt); - - if (pullAttempt.Cancelled) - { - return; - } - - // Pull start confirm - - valuePuller.Pulling = Owner; - _puller = value; - Dirty(); - PullerPhysics = pullerPhysics; - - var message = new PullStartedMessage(PullerPhysics, _physics); - - eventBus.RaiseLocalEvent(_puller.Uid, message, broadcast: false); - eventBus.RaiseLocalEvent(Owner.Uid, message); - - var union = PullerPhysics.GetWorldAABB().Union(_physics.GetWorldAABB()); - var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; - - _physics.WakeBody(); - _pullJoint = EntitySystem.Get().CreateDistanceJoint(Owner.Uid, _puller.Uid, id:$"pull-joint-{_physics.Owner.Uid}"); - // _physics.BodyType = BodyType.Kinematic; // TODO: Need to consider their original bodytype - _pullJoint.CollideConnected = false; - _pullJoint.Length = length * 0.75f; - _pullJoint.MinLength = 0f; - _pullJoint.MaxLength = length; - _pullJoint.Stiffness = 1f; - } - // Code here will not run if pulling a new object was attempted and failed because of the returns from the refactor. - } - } + public IEntity? Puller { get; set; } + /// + /// The pull joint. + /// SharedPullingStateManagementSystem should be writing this. This means probably not you. + /// + public DistanceJoint? PullJoint { get; set; } public bool BeingPulled => Puller != null; - public MapCoordinates? MovingTo - { - get => _movingTo; - set - { - if (_movingTo == value) - { - return; - } - - _movingTo = value; - - if (value == null) - { - Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PullableStopMovingMessage()); - } - else - { - Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PullableMoveMessage()); - } - } - } - - /// - /// Sanity-check pull. This is called from Puller setter, so it will never deny a pull that's valid by setting Puller. - /// It might allow an impossible pull (i.e: puller has no PhysicsComponent somehow). - /// Ultimately this is only used separately to stop TryStartPull from cancelling a pull for no reason. - /// - private bool _canStartPull(IEntity puller) - { - if (!puller.HasComponent()) - { - return false; - } - - if (!EntitySystem.Get().CanPull(puller, Owner)) - { - return false; - } - - if (_physics == null) - { - return false; - } - - if (_physics.BodyType == BodyType.Static) - { - return false; - } - - if (puller == Owner) - { - return false; - } - - if (!puller.IsInSameOrNoContainer(Owner)) - { - return false; - } - - return true; - } - - public bool TryStartPull(IEntity puller) - { - if (!_canStartPull(puller)) - { - return false; - } - - TryStopPull(); - - Puller = puller; - - if(Puller != puller) - { - return false; - } - - return true; - } - - public bool TryStopPull(IEntity? user = null) - { - if (!BeingPulled) - { - return false; - } - - var msg = new StopPullingEvent(user?.Uid); - Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, msg); - - if (msg.Cancelled) return false; - - if (_physics != null && _pullJoint != null) - { - EntitySystem.Get().RemoveJoint(_pullJoint); - } - - if (user != null && user.TryGetComponent(out var puller)) - { - puller.Pulling = null; - } - - _pullJoint = null; - Puller = null; - return true; - } - - public bool TogglePull(IEntity puller) - { - if (BeingPulled) - { - if (Puller == puller) - { - return TryStopPull(); - } - else - { - TryStopPull(); - return TryStartPull(puller); - } - } - - return TryStartPull(puller); - } - - public bool TryMoveTo(MapCoordinates to) - { - if (Puller == null) - { - return false; - } - - if (_physics == null) - { - return false; - } - - MovingTo = to; - return true; - } + public MapCoordinates? MovingTo { get; set; } public override ComponentState GetComponentState(ICommonSession player) { @@ -347,11 +69,19 @@ namespace Content.Shared.Pulling.Components Puller = entity; } + protected override void Shutdown() + { + EntitySystem.Get().ForceDisconnectPullable(this); + base.Shutdown(); + } + protected override void OnRemove() { - TryStopPull(); - MovingTo = null; - + if (Puller != null) + { + // This is absolute paranoia but it's also absolutely necessary. Too many puller state bugs. - 20kdc + Logger.ErrorS("c.go.c.pulling", "PULLING STATE CORRUPTION IMMINENT IN PULLABLE {0} - OnRemove called when Puller is set!", Owner); + } base.OnRemove(); } } diff --git a/Content.Shared/Pulling/Components/SharedPullerComponent.cs b/Content.Shared/Pulling/Components/SharedPullerComponent.cs index 15afacd88e..89398b0665 100644 --- a/Content.Shared/Pulling/Components/SharedPullerComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullerComponent.cs @@ -1,49 +1,40 @@ -using Content.Shared.Movement.Components; +using Content.Shared.Pulling; +using Content.Shared.Movement.Components; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.ViewVariables; +using Robust.Shared.Log; using Component = Robust.Shared.GameObjects.Component; namespace Content.Shared.Pulling.Components { [RegisterComponent] + [Friend(typeof(SharedPullingStateManagementSystem))] public class SharedPullerComponent : Component, IMoveSpeedModifier { public override string Name => "Puller"; - private IEntity? _pulling; + // Before changing how this is updated, please see SharedPullerSystem.RefreshMovementSpeed + public float WalkSpeedModifier => Pulling == null ? 1.0f : 0.75f; - public float WalkSpeedModifier => _pulling == null ? 1.0f : 0.75f; - - public float SprintSpeedModifier => _pulling == null ? 1.0f : 0.75f; + public float SprintSpeedModifier => Pulling == null ? 1.0f : 0.75f; [ViewVariables] - public IEntity? Pulling + public IEntity? Pulling { get; set; } + + protected override void Shutdown() { - get => _pulling; - set - { - if (_pulling == value) - { - return; - } - - _pulling = value; - - if (Owner.TryGetComponent(out MovementSpeedModifierComponent? speed)) - { - speed.RefreshMovementSpeedModifiers(); - } - } + EntitySystem.Get().ForceDisconnectPuller(this); + base.Shutdown(); } protected override void OnRemove() { - if (Pulling != null && - Pulling.TryGetComponent(out SharedPullableComponent? pullable)) + if (Pulling != null) { - pullable.TryStopPull(); + // This is absolute paranoia but it's also absolutely necessary. Too many puller state bugs. - 20kdc + 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/SharedPullableSystem.cs b/Content.Shared/Pulling/Systems/SharedPullableSystem.cs index 554138452c..6404b63f8d 100644 --- a/Content.Shared/Pulling/Systems/SharedPullableSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullableSystem.cs @@ -9,6 +9,7 @@ namespace Content.Shared.Pulling.Systems public class SharedPullableSystem : EntitySystem { [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly SharedPullingSystem _pullSystem = default!; public override void Initialize() { @@ -20,7 +21,7 @@ namespace Content.Shared.Pulling.Systems { var entity = args.Session.AttachedEntity; if (entity == null || !_blocker.CanMove(entity)) return; - component.TryStopPull(); + _pullSystem.TryStopPull(component); } } } diff --git a/Content.Shared/Pulling/Systems/SharedPullerSystem.cs b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs index d4cd71e31c..e7e3ff0ec5 100644 --- a/Content.Shared/Pulling/Systems/SharedPullerSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs @@ -1,15 +1,19 @@ using Content.Shared.Alert; using Content.Shared.Hands; +using Content.Shared.Movement.Components; using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; namespace Content.Shared.Pulling.Systems { [UsedImplicitly] public sealed class SharedPullerSystem : EntitySystem { + [Dependency] private readonly SharedPullingSystem _pullSystem = default!; + public override void Initialize() { base.Initialize(); @@ -28,7 +32,7 @@ namespace Content.Shared.Pulling.Systems { if (EntityManager.TryGetComponent(args.BlockingEntity, out var comp)) { - comp.TryStopPull(EntityManager.GetEntity(uid)); + _pullSystem.TryStopPull(comp, EntityManager.GetEntity(uid)); } } } @@ -43,6 +47,8 @@ namespace Content.Shared.Pulling.Systems if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) alerts.ShowAlert(AlertType.Pulling); + + RefreshMovementSpeed(component); } private static void PullerHandlePullStopped( @@ -55,6 +61,17 @@ namespace Content.Shared.Pulling.Systems if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) alerts.ClearAlert(AlertType.Pulling); + + RefreshMovementSpeed(component); + } + + private static void RefreshMovementSpeed(SharedPullerComponent component) + { + // Before changing how this is updated, please see SharedPullerComponent + if (component.Owner.TryGetComponent(out var speed)) + { + speed.RefreshMovementSpeedModifiers(); + } } } } diff --git a/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs new file mode 100644 index 0000000000..1b9ee0639e --- /dev/null +++ b/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs @@ -0,0 +1,168 @@ +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 +{ + /// + /// 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. + /// + [UsedImplicitly] + public class SharedPullingStateManagementSystem : EntitySystem + { + [Dependency] private readonly SharedJointSystem _jointSystem = default!; + + // 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 = puller.Owner.GetComponent(); + var pullablePhysics = pullable.Owner.GetComponent(); + + // MovingTo shutdown + ForceSetMovingTo(pullable, null); + + // Joint shutdown + if (puller.Owner.TryGetComponent(out var jointComp)) + { + if (jointComp.GetJoints.Contains(pullable.PullJoint!)) + { + _jointSystem.RemoveJoint(pullable.PullJoint!); + } + } + pullable.PullJoint = null; + + // State shutdown + puller.Pulling = null; + pullable.Puller = null; + + // Messaging + var message = new PullStoppedMessage(pullerPhysics, pullablePhysics); + + RaiseLocalEvent(puller.Owner.Uid, message, broadcast: false); + + if (pullable.Owner.LifeStage <= EntityLifeStage.MapInitialized) + RaiseLocalEvent(pullable.Owner.Uid, message); + + // Networking + puller.Dirty(); + pullable.Dirty(); + } + + public void ForceRelationship(SharedPullerComponent? puller, SharedPullableComponent? pullable) + { + if ((puller != null) && (puller.Pulling == pullable)) + { + // Already done + return; + } + + // Start by disconnecting the pullable from whatever it is currently connected to. + var pullableOldPullerE = pullable?.Puller; + if (pullableOldPullerE != null) + { + ForceDisconnect(pullableOldPullerE.GetComponent(), pullable!); + } + + // Continue with the puller. + var pullerOldPullableE = puller?.Pulling; + if (pullerOldPullableE != null) + { + ForceDisconnect(puller!, pullerOldPullableE.GetComponent()); + } + + // And now for the actual connection (if any). + + if ((puller != null) && (pullable != null)) + { + var pullerPhysics = puller.Owner.GetComponent(); + var pullablePhysics = pullable.Owner.GetComponent(); + + // State startup + puller.Pulling = pullable.Owner; + pullable.Puller = puller.Owner; + + // Joint startup + var union = pullerPhysics.GetWorldAABB().Union(pullablePhysics.GetWorldAABB()); + var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; + + pullable.PullJoint = _jointSystem.CreateDistanceJoint(pullablePhysics.Owner.Uid, pullerPhysics.Owner.Uid, id:$"pull-joint-{pullablePhysics.Owner.Uid}"); + pullable.PullJoint.CollideConnected = false; + pullable.PullJoint.Length = length * 0.75f; + pullable.PullJoint.MinLength = 0f; + pullable.PullJoint.MaxLength = length; + pullable.PullJoint.Stiffness = 1f; + + // Messaging + var message = new PullStartedMessage(pullerPhysics, pullablePhysics); + + RaiseLocalEvent(puller.Owner.Uid, message, broadcast: false); + RaiseLocalEvent(pullable.Owner.Uid, message); + + // Networking + puller.Dirty(); + pullable.Dirty(); + } + } + + // 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, MapCoordinates? 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.Uid, new PullableStopMovingMessage()); + } + else + { + RaiseLocalEvent(pullable.Owner.Uid, new PullableMoveMessage()); + } + } + } +} diff --git a/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs new file mode 100644 index 0000000000..18962ccdf8 --- /dev/null +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +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.Pulling.Events; +using Content.Shared.Rotatable; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Players; +using Robust.Shared.Log; + +namespace Content.Shared.Pulling +{ + public abstract partial class SharedPullingSystem : EntitySystem + { + public bool CanPull(IEntity puller, IEntity pulled) + { + if (!puller.HasComponent()) + { + return false; + } + + if (!pulled.TryGetComponent(out var _physics)) + { + return false; + } + + if (_physics.BodyType == BodyType.Static) + { + return false; + } + + if (puller == pulled) + { + return false; + } + + if (!puller.IsInSameOrNoContainer(pulled)) + { + return false; + } + + var startPull = new StartPullAttemptEvent(puller, pulled); + RaiseLocalEvent(puller.Uid, startPull); + return !startPull.Cancelled; + } + + public bool TogglePull(IEntity puller, SharedPullableComponent pullable) + { + if (pullable.Puller == puller) + { + return TryStopPull(pullable); + } + return TryStartPull(puller, pullable.Owner); + } + + // -- Core attempted actions -- + + public bool TryStopPull(SharedPullableComponent pullable, IEntity? user = null) + { + if (!pullable.BeingPulled) + { + return false; + } + + var msg = new StopPullingEvent(user?.Uid); + RaiseLocalEvent(pullable.Owner.Uid, msg); + + if (msg.Cancelled) return false; + + _pullSm.ForceRelationship(null, pullable); + return true; + } + + public bool TryStartPull(IEntity puller, IEntity pullable) + { + if (!puller.TryGetComponent(out var pullerComp)) + { + return false; + } + if (!pullable.TryGetComponent(out var pullableComp)) + { + return false; + } + return TryStartPull(pullerComp, pullableComp); + } + + // The main "start pulling" function. + public bool TryStartPull(SharedPullerComponent puller, SharedPullableComponent pullable) + { + if (puller.Pulling == pullable) + return true; + + // Pulling a new object : Perform sanity checks. + + if (!EntitySystem.Get().CanPull(puller.Owner, pullable.Owner)) + { + return false; + } + + if (!puller.Owner.TryGetComponent(out var pullerPhysics)) + { + return false; + } + + if (!pullable.Owner.TryGetComponent(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 (oldPullable.TryGetComponent(out var oldPullableComp)) + { + if (!TryStopPull(oldPullableComp)) + { + return false; + } + } + else + { + Logger.WarningS("c.go.c.pulling", "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 PullAttemptMessage(pullerPhysics, pullablePhysics); + + RaiseLocalEvent(puller.Owner.Uid, pullAttempt, broadcast: false); + + if (pullAttempt.Cancelled) + { + return false; + } + + RaiseLocalEvent(pullable.Owner.Uid, pullAttempt); + + if (pullAttempt.Cancelled) + { + return false; + } + + _pullSm.ForceRelationship(puller, pullable); + return true; + } + + public bool TryMoveTo(SharedPullableComponent pullable, MapCoordinates to) + { + if (pullable.Puller == null) + { + return false; + } + + if (!pullable.Owner.HasComponent()) + { + return false; + } + + _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 12d67b3130..0d09c5ac82 100644 --- a/Content.Shared/Pulling/Systems/SharedPullingSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs @@ -16,12 +16,15 @@ using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Players; +using Robust.Shared.IoC; namespace Content.Shared.Pulling { [UsedImplicitly] - public abstract class SharedPullingSystem : EntitySystem + public abstract partial class SharedPullingSystem : EntitySystem { + [Dependency] private readonly SharedPullingStateManagementSystem _pullSm = default!; + /// /// A mapping of pullers to the entity that they are pulling. /// @@ -101,12 +104,6 @@ namespace Content.Shared.Pulling private void OnPullStarted(PullStartedMessage message) { - if (_pullers.TryGetValue(message.Puller.Owner, out var pulled) && - pulled.TryGetComponent(out SharedPullableComponent? pulledComponent)) - { - pulledComponent.TryStopPull(); - } - SetPuller(message.Puller.Owner, message.Pulled.Owner); } @@ -128,11 +125,20 @@ namespace Content.Shared.Pulling private void PullerMoved(ref MoveEvent ev) { var puller = ev.Sender; + if (!TryGetPulled(ev.Sender, out var pulled)) { return; } + // The pulled object may have already been deleted. + // TODO: Work out why. Monkey + meat spike is a good test for this, + // assuming you're still pulling the monkey when it gets gibbed. + if (pulled.Deleted) + { + return; + } + if (!pulled.TryGetComponent(out IPhysBody? physics)) { return; @@ -144,7 +150,7 @@ namespace Content.Shared.Pulling if (pulled.TryGetComponent(out SharedPullableComponent? pullable)) { - pullable.MovingTo = null; + _pullSm.ForceSetMovingTo(pullable, null); } } @@ -153,7 +159,7 @@ namespace Content.Shared.Pulling { if (message.Entity.TryGetComponent(out SharedPullableComponent? pullable)) { - pullable.TryStopPull(); + TryStopPull(pullable); } if (message.Entity.TryGetComponent(out SharedPullerComponent? puller)) @@ -165,7 +171,7 @@ namespace Content.Shared.Pulling return; } - pulling.TryStopPull(); + TryStopPull(pulling); } } @@ -188,7 +194,7 @@ namespace Content.Shared.Pulling return false; } - pullable.TryMoveTo(coords.ToMap(EntityManager)); + TryMoveTo(pullable, coords.ToMap(EntityManager)); return false; } @@ -238,12 +244,5 @@ namespace Content.Shared.Pulling pulled.Transform.WorldRotation = newAngle; } } - - public bool CanPull(IEntity puller, IEntity pulled) - { - var startPull = new StartPullAttemptEvent(puller, pulled); - RaiseLocalEvent(puller.Uid, startPull); - return !startPull.Cancelled; - } } }