From 42745b1c6e143279c2fb439bdfbbf929f2568d7e Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Mon, 13 Feb 2023 07:20:39 -0500 Subject: [PATCH] Conveyor Belt optimization and prediction (#12929) * 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 --- .../Physics/Controllers/ConveyorController.cs | 8 + Content.Server/Conveyor/ConveyorComponent.cs | 45 --- .../Physics/Controllers/ConveyorController.cs | 346 +++++------------- .../Components/ApcPowerReceiverComponent.cs | 2 + .../Conveyor/ActiveConveyorComponent.cs | 4 +- Content.Shared/Conveyor/ConveyorComponent.cs | 77 ++++ .../Conveyor/SharedConveyorComponent.cs | 18 - .../Controllers/SharedConveyorController.cs | 191 ++++++++++ 8 files changed, 371 insertions(+), 320 deletions(-) create mode 100644 Content.Client/Physics/Controllers/ConveyorController.cs delete mode 100644 Content.Server/Conveyor/ConveyorComponent.cs rename {Content.Server => Content.Shared}/Conveyor/ActiveConveyorComponent.cs (73%) create mode 100644 Content.Shared/Conveyor/ConveyorComponent.cs delete mode 100644 Content.Shared/Conveyor/SharedConveyorComponent.cs create mode 100644 Content.Shared/Physics/Controllers/SharedConveyorController.cs diff --git a/Content.Client/Physics/Controllers/ConveyorController.cs b/Content.Client/Physics/Controllers/ConveyorController.cs new file mode 100644 index 0000000000..20304923a4 --- /dev/null +++ b/Content.Client/Physics/Controllers/ConveyorController.cs @@ -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 index 12cca3e261..0000000000 --- a/Content.Server/Conveyor/ConveyorComponent.cs +++ /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 - { - /// - /// The angle to move entities by in relation to the owner's rotation. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("angle")] - public Angle Angle = Angle.Zero; - - /// - /// The amount of units to move the entity by per second. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("speed")] - public float Speed = 2f; - - /// - /// The current state of this conveyor - /// - [ViewVariables(VVAccess.ReadWrite)] - public ConveyorState State; - - [DataField("forwardPort", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ForwardPort = "Forward"; - - [DataField("reversePort", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ReversePort = "Reverse"; - - [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string OffPort = "Off"; - - [ViewVariables] - public readonly HashSet Intersecting = new(); - } -} diff --git a/Content.Server/Physics/Controllers/ConveyorController.cs b/Content.Server/Physics/Controllers/ConveyorController.cs index 7b480b2db1..1d18eb70f4 100644 --- a/Content.Server/Physics/Controllers/ConveyorController.cs +++ b/Content.Server/Physics/Controllers/ConveyorController.cs @@ -1,313 +1,147 @@ -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(OnInit); - SubscribeLocalEvent(OnConveyorShutdown); - SubscribeLocalEvent(OnSignalReceived); - SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnConveyorStartCollide); - SubscribeLocalEvent(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(OnInit); + SubscribeLocalEvent(OnConveyorShutdown); - if (component.Intersecting.Count == 0) - { - RemComp(uid); - } - } - private void OnConveyorStartCollide(EntityUid uid, ConveyorComponent component, ref StartCollideEvent args) - { - var otherUid = args.OtherFixture.Body.Owner; + SubscribeLocalEvent(OnSignalReceived); + SubscribeLocalEvent(OnPowerChanged); - if (args.OtherFixture.Body.BodyType == BodyType.Static || component.State == ConveyorState.Off) - return; + base.Initialize(); + } - component.Intersecting.Add(otherUid); - EnsureComp(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(uid, out var physics)) { - _signalSystem.EnsureReceiverPorts(uid, component.ReversePort, component.ForwardPort, component.OffPort); - - if (TryComp(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(uid, out var physics)) - { - _broadphase.RegenerateContacts(physics); - } - - if (TryComp(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; - /// - /// 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. - /// - private void AwakenEntities(ConveyorComponent component) - { - var xformQuery = GetEntityQuery(); - var bodyQuery = GetEntityQuery(); + RemComp(uid); - if (!xformQuery.TryGetComponent(component.Owner, out var xform)) - return; + if (!TryComp(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(uid); - - if (!TryComp(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(); - // Don't use it directly in EntityQuery because we may be able to save getcomponents. - var xformQuery = GetEntityQuery(); - var bodyQuery = GetEntityQuery(); + component.State = state; - foreach (var (_, comp) in EntityQuery()) - { - Convey(comp, xformQuery, bodyQuery, conveyed, frameTime); - } - } + if (TryComp(uid, out var physics)) + _broadphase.RegenerateContacts(physics); - private void Convey(ConveyorComponent comp, EntityQuery xformQuery, EntityQuery bodyQuery, HashSet conveyed, float frameTime) + if (TryComp(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. - */ + /// + /// 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. + /// + private void AwakenEntities(EntityUid uid, ConveyorComponent component) + { + var xformQuery = GetEntityQuery(); + var bodyQuery = GetEntityQuery(); - 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 xformQuery, - EntityQuery 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); } } } diff --git a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs index ff919d4a01..bb8d5549da 100644 --- a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs +++ b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs @@ -70,6 +70,7 @@ namespace Content.Server.Power.Components /// /// Raised whenever an ApcPowerReceiver becomes powered / unpowered. + /// Does nothing on the client. /// [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; } + } diff --git a/Content.Server/Conveyor/ActiveConveyorComponent.cs b/Content.Shared/Conveyor/ActiveConveyorComponent.cs similarity index 73% rename from Content.Server/Conveyor/ActiveConveyorComponent.cs rename to Content.Shared/Conveyor/ActiveConveyorComponent.cs index da18c7ef87..951bed4f1f 100644 --- a/Content.Server/Conveyor/ActiveConveyorComponent.cs +++ b/Content.Shared/Conveyor/ActiveConveyorComponent.cs @@ -1,4 +1,6 @@ -namespace Content.Server.Conveyor; +using Robust.Shared.GameStates; + +namespace Content.Shared.Conveyor; /// /// 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 index 0000000000..efa0215a69 --- /dev/null +++ b/Content.Shared/Conveyor/ConveyorComponent.cs @@ -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 +{ + /// + /// The angle to move entities by in relation to the owner's rotation. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("angle")] + public Angle Angle = Angle.Zero; + + /// + /// The amount of units to move the entity by per second. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("speed")] + public float Speed = 2f; + + /// + /// The current state of this conveyor + /// + [ViewVariables(VVAccess.ReadWrite)] + public ConveyorState State; + + [ViewVariables] + public bool Powered; + + [DataField("forwardPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ForwardPort = "Forward"; + + [DataField("reversePort", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ReversePort = "Reverse"; + + [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string OffPort = "Off"; + + [ViewVariables] + public readonly HashSet 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 index 3f9fb80c95..0000000000 --- a/Content.Shared/Conveyor/SharedConveyorComponent.cs +++ /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 index 0000000000..9dce765692 --- /dev/null +++ b/Content.Shared/Physics/Controllers/SharedConveyorController.cs @@ -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(OnConveyorGetState); + SubscribeLocalEvent(OnConveyorHandleState); + + SubscribeLocalEvent(OnConveyorStartCollide); + SubscribeLocalEvent(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(uid); + } + + private void OnConveyorEndCollide(EntityUid uid, ConveyorComponent component, ref EndCollideEvent args) + { + component.Intersecting.Remove(args.OtherFixture.Body.Owner); + + if (component.Intersecting.Count == 0) + RemComp(uid); + } + + public override void UpdateBeforeSolve(bool prediction, float frameTime) + { + base.UpdateBeforeSolve(prediction, frameTime); + + var conveyed = new HashSet(); + // Don't use it directly in EntityQuery because we may be able to save getcomponents. + var xformQuery = GetEntityQuery(); + var bodyQuery = GetEntityQuery(); + + foreach (var (_, comp) in EntityQuery()) + { + var uid = comp.Owner; + Convey(uid, comp, xformQuery, bodyQuery, conveyed, frameTime, prediction); + } + } + + private void Convey(EntityUid uid, ConveyorComponent comp, EntityQuery xformQuery, EntityQuery bodyQuery, HashSet 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 xformQuery, + EntityQuery 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; + } +} -- 2.52.0