]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Conveyor Belt optimization and prediction (#12929)
authorkeronshb <54602815+keronshb@users.noreply.github.com>
Mon, 13 Feb 2023 12:20:39 +0000 (07:20 -0500)
committerGitHub <noreply@github.com>
Mon, 13 Feb 2023 12:20:39 +0000 (12:20 +0000)
* belt multithreading

* moves away from multithreading and changes setting awake directly to physics system method

* prediction for conveyors

* Fixes missing reference in FaxSystem

* Fixes oddities

* Adds networked to conveyor components

* Some more cleanup.

* reverts power change event

* Removes the event, fixes a file

* Should fix the rest of the weird additions

* More cleanup to fix extra files

* Fixes again

* fix

* fixes fax system

* Adds component state, cleans up the dependencies

* Checks for prediction

* Merge conflicts

* powa

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Content.Client/Physics/Controllers/ConveyorController.cs [new file with mode: 0644]
Content.Server/Conveyor/ConveyorComponent.cs [deleted file]
Content.Server/Physics/Controllers/ConveyorController.cs
Content.Server/Power/Components/ApcPowerReceiverComponent.cs
Content.Shared/Conveyor/ActiveConveyorComponent.cs [moved from Content.Server/Conveyor/ActiveConveyorComponent.cs with 73% similarity]
Content.Shared/Conveyor/ConveyorComponent.cs [new file with mode: 0644]
Content.Shared/Conveyor/SharedConveyorComponent.cs [deleted file]
Content.Shared/Physics/Controllers/SharedConveyorController.cs [new file with mode: 0644]

diff --git a/Content.Client/Physics/Controllers/ConveyorController.cs b/Content.Client/Physics/Controllers/ConveyorController.cs
new file mode 100644 (file)
index 0000000..2030492
--- /dev/null
@@ -0,0 +1,8 @@
+using Content.Shared.Physics.Controllers;
+
+namespace Content.Client.Physics.Controllers;
+
+public sealed class ConveyorController : SharedConveyorController
+{
+    //Class is empty, needed for prediction and networking
+}
diff --git a/Content.Server/Conveyor/ConveyorComponent.cs b/Content.Server/Conveyor/ConveyorComponent.cs
deleted file mode 100644 (file)
index 12cca3e..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-using Content.Server.Physics.Controllers;
-using Content.Shared.Conveyor;
-using Content.Shared.MachineLinking;
-using Robust.Shared.Physics.Collision.Shapes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Conveyor
-{
-    [RegisterComponent]
-    [Access(typeof(ConveyorController))]
-    public sealed class ConveyorComponent : Component
-    {
-        /// <summary>
-        ///     The angle to move entities by in relation to the owner's rotation.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("angle")]
-        public Angle Angle = Angle.Zero;
-
-        /// <summary>
-        ///     The amount of units to move the entity by per second.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("speed")]
-        public float Speed = 2f;
-
-        /// <summary>
-        ///     The current state of this conveyor
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        public ConveyorState State;
-
-        [DataField("forwardPort", customTypeSerializer: typeof(PrototypeIdSerializer<ReceiverPortPrototype>))]
-        public string ForwardPort = "Forward";
-
-        [DataField("reversePort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>))]
-        public string ReversePort = "Reverse";
-
-        [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>))]
-        public string OffPort = "Off";
-
-        [ViewVariables]
-        public readonly HashSet<EntityUid> Intersecting = new();
-    }
-}
index 7b480b2db113c1c1ff5878db5f2af6a65d560399..1d18eb70f4ecee69c33a35f1079ad9f18c0a8dad 100644 (file)
-using Content.Server.Conveyor;
-using Content.Server.Gravity;
 using Content.Server.MachineLinking.Events;
 using Content.Server.MachineLinking.System;
 using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
 using Content.Server.Recycling;
 using Content.Server.Recycling.Components;
 using Content.Shared.Conveyor;
 using Content.Shared.Maps;
 using Content.Shared.Physics;
-using Robust.Shared.Map;
+using Content.Shared.Physics.Controllers;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Collision.Shapes;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Controllers;
 using Robust.Shared.Physics.Dynamics;
-using Robust.Shared.Physics.Events;
 using Robust.Shared.Physics.Systems;
 
-namespace Content.Server.Physics.Controllers
-{
-    public sealed class ConveyorController : VirtualController
-    {
-        [Dependency] private readonly IMapManager _mapManager = default!;
-        [Dependency] private readonly EntityLookupSystem _lookup = default!;
-        [Dependency] private readonly FixtureSystem _fixtures = default!;
-        [Dependency] private readonly GravitySystem _gravity = default!;
-        [Dependency] private readonly RecyclerSystem _recycler = default!;
-        [Dependency] private readonly SignalLinkerSystem _signalSystem = default!;
-        [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
-        [Dependency] private readonly SharedPhysicsSystem _physics = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-
-        public const string ConveyorFixture = "conveyor";
+namespace Content.Server.Physics.Controllers;
 
-        public override void Initialize()
-        {
-            UpdatesAfter.Add(typeof(MoverController));
-            SubscribeLocalEvent<ConveyorComponent, ComponentInit>(OnInit);
-            SubscribeLocalEvent<ConveyorComponent, ComponentShutdown>(OnConveyorShutdown);
-            SubscribeLocalEvent<ConveyorComponent, SignalReceivedEvent>(OnSignalReceived);
-            SubscribeLocalEvent<ConveyorComponent, PowerChangedEvent>(OnPowerChanged);
-            SubscribeLocalEvent<ConveyorComponent, StartCollideEvent>(OnConveyorStartCollide);
-            SubscribeLocalEvent<ConveyorComponent, EndCollideEvent>(OnConveyorEndCollide);
-
-            base.Initialize();
-        }
+public sealed class ConveyorController : SharedConveyorController
+{
+    [Dependency] private readonly FixtureSystem _fixtures = default!;
+    [Dependency] private readonly RecyclerSystem _recycler = default!;
+    [Dependency] private readonly SignalLinkerSystem _signalSystem = default!;
+    [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
 
-        private void OnConveyorEndCollide(EntityUid uid, ConveyorComponent component, ref EndCollideEvent args)
-        {
-            component.Intersecting.Remove(args.OtherFixture.Body.Owner);
+    public override void Initialize()
+    {
+        UpdatesAfter.Add(typeof(MoverController));
+        SubscribeLocalEvent<ConveyorComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<ConveyorComponent, ComponentShutdown>(OnConveyorShutdown);
 
-            if (component.Intersecting.Count == 0)
-            {
-                RemComp<ActiveConveyorComponent>(uid);
-            }
-        }
 
-        private void OnConveyorStartCollide(EntityUid uid, ConveyorComponent component, ref StartCollideEvent args)
-        {
-            var otherUid = args.OtherFixture.Body.Owner;
+        SubscribeLocalEvent<ConveyorComponent, SignalReceivedEvent>(OnSignalReceived);
+        SubscribeLocalEvent<ConveyorComponent, PowerChangedEvent>(OnPowerChanged);
 
-            if (args.OtherFixture.Body.BodyType == BodyType.Static || component.State == ConveyorState.Off)
-                return;
+        base.Initialize();
+    }
 
-            component.Intersecting.Add(otherUid);
-            EnsureComp<ActiveConveyorComponent>(uid);
-        }
+    private void OnInit(EntityUid uid, ConveyorComponent component, ComponentInit args)
+    {
+        _signalSystem.EnsureReceiverPorts(uid, component.ReversePort, component.ForwardPort, component.OffPort);
 
-        private void OnInit(EntityUid uid, ConveyorComponent component, ComponentInit args)
+        if (TryComp<PhysicsComponent>(uid, out var physics))
         {
-            _signalSystem.EnsureReceiverPorts(uid, component.ReversePort, component.ForwardPort, component.OffPort);
-
-            if (TryComp<PhysicsComponent>(uid, out var body))
-            {
-                var shape = new PolygonShape();
-                shape.SetAsBox(0.55f, 0.55f);
+            var shape = new PolygonShape();
+            shape.SetAsBox(0.55f, 0.55f);
 
-                _fixtures.TryCreateFixture(uid, shape, ConveyorFixture, hard: false,
-                    collisionLayer: (int) (CollisionGroup.LowImpassable | CollisionGroup.MidImpassable |
-                                           CollisionGroup.Impassable), body: body);
-            }
-        }
-
-        private void OnPowerChanged(EntityUid uid, ConveyorComponent component, ref PowerChangedEvent args)
-        {
-            UpdateAppearance(component);
-        }
+            _fixtures.TryCreateFixture(uid, shape, ConveyorFixture,
+                collisionLayer: (int) (CollisionGroup.LowImpassable | CollisionGroup.MidImpassable |
+                                       CollisionGroup.Impassable), hard: false, body: physics);
 
-        private void UpdateAppearance(ConveyorComponent component)
-        {
-            var isPowered = this.IsPowered(component.Owner, EntityManager);
-            _appearance.SetData(component.Owner, ConveyorVisuals.State, isPowered ? component.State : ConveyorState.Off);
         }
+    }
 
-        private void OnSignalReceived(EntityUid uid, ConveyorComponent component, SignalReceivedEvent args)
-        {
-            if (args.Port == component.OffPort)
-                SetState(uid, ConveyorState.Off, component);
-            else if (args.Port == component.ForwardPort)
-            {
-                AwakenEntities(component);
-                SetState(uid, ConveyorState.Forward, component);
-            }
-            else if (args.Port == component.ReversePort)
-            {
-                AwakenEntities(component);
-                SetState(uid, ConveyorState.Reverse, component);
-            }
-        }
-
-        private void SetState(EntityUid uid, ConveyorState state, ConveyorComponent? component = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
-
-            component.State = state;
-
-            if (TryComp<PhysicsComponent>(uid, out var physics))
-            {
-                _broadphase.RegenerateContacts(physics);
-            }
-
-            if (TryComp<RecyclerComponent>(component.Owner, out var recycler))
-            {
-                if (component.State != ConveyorState.Off)
-                    _recycler.EnableRecycler(recycler);
-                else
-                    _recycler.DisableRecycler(recycler);
-            }
-
-            UpdateAppearance(component);
-        }
+    private void OnConveyorShutdown(EntityUid uid, ConveyorComponent component, ComponentShutdown args)
+    {
+        if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
+            return;
 
-        /// <summary>
-        /// Awakens sleeping entities on the conveyor belt's tile when it's turned on.
-        /// Fixes an issue where non-hard/sleeping entities refuse to wake up + collide if a belt is turned off and on again.
-        /// </summary>
-        private void AwakenEntities(ConveyorComponent component)
-        {
-            var xformQuery = GetEntityQuery<TransformComponent>();
-            var bodyQuery = GetEntityQuery<PhysicsComponent>();
+        RemComp<ActiveConveyorComponent>(uid);
 
-            if (!xformQuery.TryGetComponent(component.Owner, out var xform))
-                return;
+        if (!TryComp<PhysicsComponent>(uid, out var physics))
+            return;
 
-            var beltTileRef = xform.Coordinates.GetTileRef(EntityManager, _mapManager);
+        _fixtures.DestroyFixture(uid, ConveyorFixture, body: physics);
+    }
 
-            if (beltTileRef != null)
-            {
-                var intersecting = _lookup.GetEntitiesIntersecting(beltTileRef.Value);
+    private void OnPowerChanged(EntityUid uid, ConveyorComponent component, ref PowerChangedEvent args)
+    {
+        component.Powered = args.Powered;
+        UpdateAppearance(uid, component);
+        Dirty(component);
+    }
 
-                foreach (var entity in intersecting)
-                {
-                    if (!bodyQuery.TryGetComponent(entity, out var physics))
-                        continue;
+    private void UpdateAppearance(EntityUid uid, ConveyorComponent component)
+    {
+        _appearance.SetData(uid, ConveyorVisuals.State, component.Powered ? component.State : ConveyorState.Off);
+    }
 
-                    if (physics.BodyType != BodyType.Static)
-                        _physics.WakeBody(entity, body: physics);
-                }
-            }
-        }
+    private void OnSignalReceived(EntityUid uid, ConveyorComponent component, SignalReceivedEvent args)
+    {
+        if (args.Port == component.OffPort)
+            SetState(uid, ConveyorState.Off, component);
 
-        public bool CanRun(ConveyorComponent component)
+        else if (args.Port == component.ForwardPort)
         {
-            return component.State != ConveyorState.Off && this.IsPowered(component.Owner, EntityManager);
+            AwakenEntities(uid, component);
+            SetState(uid, ConveyorState.Forward, component);
         }
 
-        private void OnConveyorShutdown(EntityUid uid, ConveyorComponent component, ComponentShutdown args)
+        else if (args.Port == component.ReversePort)
         {
-            if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
-                return;
-
-            RemComp<ActiveConveyorComponent>(uid);
-
-            if (!TryComp<PhysicsComponent>(uid, out var body))
-                return;
-
-            _fixtures.DestroyFixture(uid, ConveyorFixture, body: body);
+            AwakenEntities(uid, component);
+            SetState(uid, ConveyorState.Reverse, component);
         }
+    }
 
-        public override void UpdateBeforeSolve(bool prediction, float frameTime)
-        {
-            base.UpdateBeforeSolve(prediction, frameTime);
+    private void SetState(EntityUid uid, ConveyorState state, ConveyorComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return;
 
-            var conveyed = new HashSet<EntityUid>();
-            // Don't use it directly in EntityQuery because we may be able to save getcomponents.
-            var xformQuery = GetEntityQuery<TransformComponent>();
-            var bodyQuery = GetEntityQuery<PhysicsComponent>();
+        component.State = state;
 
-            foreach (var (_, comp) in EntityQuery<ActiveConveyorComponent, ConveyorComponent>())
-            {
-                Convey(comp, xformQuery, bodyQuery, conveyed, frameTime);
-            }
-        }
+        if (TryComp<PhysicsComponent>(uid, out var physics))
+            _broadphase.RegenerateContacts(physics);
 
-        private void Convey(ConveyorComponent comp, EntityQuery<TransformComponent> xformQuery, EntityQuery<PhysicsComponent> bodyQuery, HashSet<EntityUid> conveyed, float frameTime)
+        if (TryComp<RecyclerComponent>(uid, out var recycler))
         {
-            // Use an event for conveyors to know what needs to run
-            if (!CanRun(comp))
-            {
-                return;
-            }
-
-            var speed = comp.Speed;
-
-            if (speed <= 0f ||
-                !xformQuery.TryGetComponent(comp.Owner, out var xform) ||
-                xform.GridUid == null)
-                return;
-
-            var conveyorPos = xform.LocalPosition;
-            var conveyorRot = xform.LocalRotation;
-
-            conveyorRot += comp.Angle;
-
-            if (comp.State == ConveyorState.Reverse)
-            {
-                conveyorRot += MathF.PI;
-            }
-
-            var direction = conveyorRot.ToWorldVec();
-
-            foreach (var (entity, transform, body) in GetEntitiesToMove(comp, xform, xformQuery, bodyQuery))
-            {
-                if (!conveyed.Add(entity))
-                    continue;
-
-                var localPos = transform.LocalPosition;
-                var itemRelative = conveyorPos - localPos;
-
-                localPos += Convey(direction, speed, frameTime, itemRelative);
-                transform.LocalPosition = localPos;
-
-                // Force it awake for collisionwake reasons.
-                // TODO: Just use sleepallowed
-                _physics.SetAwake(entity, body, true);
-                _physics.SetSleepTime(body, 0f);
-            }
+            if (component.State != ConveyorState.Off)
+                _recycler.EnableRecycler(recycler);
+            else
+                _recycler.DisableRecycler(recycler);
         }
 
-        private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
-        {
-            if (speed == 0 || direction.Length == 0)
-                return Vector2.Zero;
+        UpdateAppearance(uid, component);
+        Dirty(component);
+    }
 
-            /*
-             * Basic idea: if the item is not in the middle of the conveyor in the direction that the conveyor is running,
-             * move the item towards the middle. Otherwise, move the item along the direction. This lets conveyors pick up
-             * items that are not perfectly aligned in the middle, and also makes corner cuts work.
-             *
-             * We do this by computing the projection of 'itemRelative' on 'direction', yielding a vector 'p' in the direction
-             * of 'direction'. We also compute the rejection 'r'. If the magnitude of 'r' is not (near) zero, then the item
-             * is not on the centerline.
-             */
+    /// <summary>
+    /// Awakens sleeping entities on the conveyor belt's tile when it's turned on.
+    /// Fixes an issue where non-hard/sleeping entities refuse to wake up + collide if a belt is turned off and on again.
+    /// </summary>
+    private void AwakenEntities(EntityUid uid, ConveyorComponent component)
+    {
+        var xformQuery = GetEntityQuery<TransformComponent>();
+        var bodyQuery = GetEntityQuery<PhysicsComponent>();
 
-            var p = direction * (Vector2.Dot(itemRelative, direction) / Vector2.Dot(direction, direction));
-            var r = itemRelative - p;
+        if (!xformQuery.TryGetComponent(uid, out var xform))
+            return;
 
-            if (r.Length < 0.1)
-            {
-                var velocity = direction * speed;
-                return velocity * frameTime;
-            }
-            else
-            {
-                // Give a slight nudge in the direction of the conveyor to prevent
-                // to collidable objects (e.g. crates) on the locker from getting stuck
-                // pushing each other when rounding a corner.
-                var velocity = (r + direction*0.2f).Normalized * speed;
-                return velocity * frameTime;
-            }
-        }
+        var beltTileRef = xform.Coordinates.GetTileRef(EntityManager, _mapManager);
 
-        private IEnumerable<(EntityUid, TransformComponent, PhysicsComponent)> GetEntitiesToMove(
-            ConveyorComponent comp,
-            TransformComponent xform,
-            EntityQuery<TransformComponent> xformQuery,
-            EntityQuery<PhysicsComponent> bodyQuery)
+        if (beltTileRef != null)
         {
-            // Check if the thing's centre overlaps the grid tile.
-            var grid = _mapManager.GetGrid(xform.GridUid!.Value);
-            var tile = grid.GetTileRef(xform.Coordinates);
-            var conveyorBounds = _lookup.GetLocalBounds(tile, grid.TileSize);
+            var intersecting = _lookup.GetEntitiesIntersecting(beltTileRef.Value);
 
-            foreach (var entity in comp.Intersecting)
+            foreach (var entity in intersecting)
             {
-                if (!xformQuery.TryGetComponent(entity, out var entityXform) ||
-                    entityXform.ParentUid != grid.Owner)
-                {
-                    continue;
-                }
-
-                if (!bodyQuery.TryGetComponent(entity, out var physics) ||
-                    physics.BodyType == BodyType.Static ||
-                    physics.BodyStatus == BodyStatus.InAir ||
-                    _gravity.IsWeightless(entity, physics, entityXform))
-                {
-                    continue;
-                }
-
-                // Yes there's still going to be the occasional rounding issue where it stops getting conveyed
-                // When you fix the corner issue that will fix this anyway.
-                var gridAABB = new Box2(entityXform.LocalPosition - 0.1f, entityXform.LocalPosition + 0.1f);
-
-                if (!conveyorBounds.Intersects(gridAABB))
+                if (!bodyQuery.TryGetComponent(entity, out var physics))
                     continue;
 
-                yield return (entity, entityXform, physics);
+                if (physics.BodyType != BodyType.Static)
+                    _physics.WakeBody(entity, body: physics);
             }
         }
     }
index ff919d4a01e9cff79d1386b7929b1a8f8ce56a25..bb8d5549da39006d69274c28313fc8824a4ce2f4 100644 (file)
@@ -70,6 +70,7 @@ namespace Content.Server.Power.Components
 
     /// <summary>
     /// Raised whenever an ApcPowerReceiver becomes powered / unpowered.
+    /// Does nothing on the client.
     /// </summary>
     [ByRefEvent]
     public readonly record struct PowerChangedEvent(bool Powered, float ReceivingPower)
@@ -77,4 +78,5 @@ namespace Content.Server.Power.Components
         public readonly bool Powered = Powered;
         public readonly float ReceivingPower = ReceivingPower;
     }
+
 }
similarity index 73%
rename from Content.Server/Conveyor/ActiveConveyorComponent.cs
rename to Content.Shared/Conveyor/ActiveConveyorComponent.cs
index da18c7ef878b18e0c8994f685ddd86c844278729..951bed4f1fb6db8811eb6886e53876d0ae1b9fcb 100644 (file)
@@ -1,4 +1,6 @@
-namespace Content.Server.Conveyor;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Conveyor;
 
 /// <summary>
 /// Used to track which conveyors are relevant in case there's a lot of them.
diff --git a/Content.Shared/Conveyor/ConveyorComponent.cs b/Content.Shared/Conveyor/ConveyorComponent.cs
new file mode 100644 (file)
index 0000000..efa0215
--- /dev/null
@@ -0,0 +1,77 @@
+using Content.Shared.MachineLinking;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Conveyor;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class ConveyorComponent : Component
+{
+    /// <summary>
+    ///     The angle to move entities by in relation to the owner's rotation.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("angle")]
+    public Angle Angle = Angle.Zero;
+
+    /// <summary>
+    ///     The amount of units to move the entity by per second.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("speed")]
+    public float Speed = 2f;
+
+    /// <summary>
+    ///     The current state of this conveyor
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public ConveyorState State;
+
+    [ViewVariables]
+    public bool Powered;
+
+    [DataField("forwardPort", customTypeSerializer: typeof(PrototypeIdSerializer<ReceiverPortPrototype>))]
+    public string ForwardPort = "Forward";
+
+    [DataField("reversePort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>))]
+    public string ReversePort = "Reverse";
+
+    [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>))]
+    public string OffPort = "Off";
+
+    [ViewVariables]
+    public readonly HashSet<EntityUid> Intersecting = new();
+}
+
+[Serializable, NetSerializable]
+public sealed class ConveyorComponentState : ComponentState
+{
+    public bool Powered;
+    public Angle Angle;
+    public float Speed;
+    public ConveyorState State;
+
+    public ConveyorComponentState(Angle angle, float speed, ConveyorState state, bool powered)
+    {
+        Angle = angle;
+        Speed = speed;
+        State = state;
+        Powered = powered;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum ConveyorVisuals : byte
+{
+    State
+}
+
+[Serializable, NetSerializable]
+public enum ConveyorState : byte
+{
+    Off,
+    Forward,
+    Reverse
+}
+
diff --git a/Content.Shared/Conveyor/SharedConveyorComponent.cs b/Content.Shared/Conveyor/SharedConveyorComponent.cs
deleted file mode 100644 (file)
index 3f9fb80..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Conveyor
-{
-    [Serializable, NetSerializable]
-    public enum ConveyorVisuals
-    {
-        State
-    }
-
-    [Serializable, NetSerializable]
-    public enum ConveyorState
-    {
-        Off,
-        Forward,
-        Reverse
-    }
-}
diff --git a/Content.Shared/Physics/Controllers/SharedConveyorController.cs b/Content.Shared/Physics/Controllers/SharedConveyorController.cs
new file mode 100644 (file)
index 0000000..9dce765
--- /dev/null
@@ -0,0 +1,191 @@
+using Content.Shared.Conveyor;
+using Content.Shared.Gravity;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Controllers;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+
+namespace Content.Shared.Physics.Controllers;
+
+public abstract class SharedConveyorController : VirtualController
+{
+    [Dependency] protected readonly IMapManager _mapManager = default!;
+    [Dependency] protected readonly EntityLookupSystem _lookup = default!;
+    [Dependency] protected readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedGravitySystem _gravity = default!;
+
+    protected const string ConveyorFixture = "conveyor";
+    public override void Initialize()
+    {
+        UpdatesAfter.Add(typeof(SharedMoverController));
+        SubscribeLocalEvent<ConveyorComponent, ComponentGetState>(OnConveyorGetState);
+        SubscribeLocalEvent<ConveyorComponent, ComponentHandleState>(OnConveyorHandleState);
+
+        SubscribeLocalEvent<ConveyorComponent, StartCollideEvent>(OnConveyorStartCollide);
+        SubscribeLocalEvent<ConveyorComponent, EndCollideEvent>(OnConveyorEndCollide);
+
+        base.Initialize();
+    }
+
+    private void OnConveyorGetState(EntityUid uid, ConveyorComponent component, ref ComponentGetState args)
+    {
+        args.State = new ConveyorComponentState(component.Angle, component.Speed, component.State, component.Powered);
+    }
+
+    private void OnConveyorHandleState(EntityUid uid, ConveyorComponent component, ref ComponentHandleState args)
+    {
+        if (args.Current is not ConveyorComponentState state)
+            return;
+
+        component.Powered = state.Powered;
+        component.Angle = state.Angle;
+        component.Speed = state.Speed;
+        component.State = state.State;
+    }
+
+    private void OnConveyorStartCollide(EntityUid uid, ConveyorComponent component, ref StartCollideEvent args)
+    {
+        var otherUid = args.OtherFixture.Body.Owner;
+
+        if (args.OtherFixture.Body.BodyType == BodyType.Static || component.State == ConveyorState.Off)
+            return;
+
+        component.Intersecting.Add(otherUid);
+        EnsureComp<ActiveConveyorComponent>(uid);
+    }
+
+    private void OnConveyorEndCollide(EntityUid uid, ConveyorComponent component, ref EndCollideEvent args)
+    {
+        component.Intersecting.Remove(args.OtherFixture.Body.Owner);
+
+        if (component.Intersecting.Count == 0)
+            RemComp<ActiveConveyorComponent>(uid);
+    }
+
+    public override void UpdateBeforeSolve(bool prediction, float frameTime)
+    {
+        base.UpdateBeforeSolve(prediction, frameTime);
+
+        var conveyed = new HashSet<EntityUid>();
+        // Don't use it directly in EntityQuery because we may be able to save getcomponents.
+        var xformQuery = GetEntityQuery<TransformComponent>();
+        var bodyQuery = GetEntityQuery<PhysicsComponent>();
+
+        foreach (var (_, comp) in EntityQuery<ActiveConveyorComponent, ConveyorComponent>())
+        {
+            var uid = comp.Owner;
+            Convey(uid, comp, xformQuery, bodyQuery, conveyed, frameTime, prediction);
+        }
+    }
+
+    private void Convey(EntityUid uid, ConveyorComponent comp, EntityQuery<TransformComponent> xformQuery, EntityQuery<PhysicsComponent> bodyQuery, HashSet<EntityUid> conveyed, float frameTime, bool prediction)
+    {
+        // Use an event for conveyors to know what needs to run
+        if (!CanRun(comp))
+            return;
+
+        var speed = comp.Speed;
+
+        if (speed <= 0f || !xformQuery.TryGetComponent(uid, out var xform) || xform.GridUid == null)
+            return;
+
+        var conveyorPos = xform.LocalPosition;
+        var conveyorRot = xform.LocalRotation;
+
+        conveyorRot += comp.Angle;
+
+        if (comp.State == ConveyorState.Reverse)
+            conveyorRot += MathF.PI;
+
+        var direction = conveyorRot.ToWorldVec();
+
+        foreach (var (entity, transform, body) in GetEntitiesToMove(comp, xform, xformQuery, bodyQuery))
+        {
+            if (!conveyed.Add(entity) || prediction && !body.Predict)
+                continue;
+
+            var localPos = transform.LocalPosition;
+            var itemRelative = conveyorPos - localPos;
+
+            localPos += Convey(direction, speed, frameTime, itemRelative);
+            transform.LocalPosition = localPos;
+
+            // Force it awake for collisionwake reasons.
+            _physics.SetAwake(entity, body, true);
+            _physics.SetSleepTime(body, 0f);
+        }
+        Dirty(comp);
+    }
+
+    private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
+    {
+        if (speed == 0 || direction.Length == 0)
+            return Vector2.Zero;
+
+        /*
+         * Basic idea: if the item is not in the middle of the conveyor in the direction that the conveyor is running,
+         * move the item towards the middle. Otherwise, move the item along the direction. This lets conveyors pick up
+         * items that are not perfectly aligned in the middle, and also makes corner cuts work.
+         *
+         * We do this by computing the projection of 'itemRelative' on 'direction', yielding a vector 'p' in the direction
+         * of 'direction'. We also compute the rejection 'r'. If the magnitude of 'r' is not (near) zero, then the item
+         * is not on the centerline.
+         */
+
+        var p = direction * (Vector2.Dot(itemRelative, direction) / Vector2.Dot(direction, direction));
+        var r = itemRelative - p;
+
+        if (r.Length < 0.1)
+        {
+            var velocity = direction * speed;
+            return velocity * frameTime;
+        }
+        else
+        {
+            // Give a slight nudge in the direction of the conveyor to prevent
+            // to collidable objects (e.g. crates) on the locker from getting stuck
+            // pushing each other when rounding a corner.
+            var velocity = (r + direction*0.2f).Normalized * speed;
+            return velocity * frameTime;
+        }
+    }
+
+    private IEnumerable<(EntityUid, TransformComponent, PhysicsComponent)> GetEntitiesToMove(
+        ConveyorComponent comp,
+        TransformComponent xform,
+        EntityQuery<TransformComponent> xformQuery,
+        EntityQuery<PhysicsComponent> bodyQuery)
+    {
+        // Check if the thing's centre overlaps the grid tile.
+        var grid = _mapManager.GetGrid(xform.GridUid!.Value);
+        var tile = grid.GetTileRef(xform.Coordinates);
+        var conveyorBounds = _lookup.GetLocalBounds(tile, grid.TileSize);
+
+        foreach (var entity in comp.Intersecting)
+        {
+            if (!xformQuery.TryGetComponent(entity, out var entityXform) || entityXform.ParentUid != grid.Owner)
+                continue;
+
+            if (!bodyQuery.TryGetComponent(entity, out var physics) || physics.BodyType == BodyType.Static || physics.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(entity, physics, entityXform))
+                continue;
+
+            // Yes there's still going to be the occasional rounding issue where it stops getting conveyed
+            // When you fix the corner issue that will fix this anyway.
+            var gridAABB = new Box2(entityXform.LocalPosition - 0.1f, entityXform.LocalPosition + 0.1f);
+
+            if (!conveyorBounds.Intersects(gridAABB))
+                continue;
+
+            yield return (entity, entityXform, physics);
+        }
+    }
+
+    public bool CanRun(ConveyorComponent component)
+    {
+        return component.State != ConveyorState.Off && component.Powered;
+    }
+}