]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Mob collisions (#34580)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fri, 4 Apr 2025 13:33:52 +0000 (00:33 +1100)
committerGitHub <noreply@github.com>
Fri, 4 Apr 2025 13:33:52 +0000 (00:33 +1100)
* Conveyor optimisations

- Optimise movement for moving stuff. Better flags + less resolves + slapped parallelrobustjob on it.
- Sleeping for entities getting conveyed into walls.

* Blocker version

* Finish

* Final

* Mob collisions

* impulses

* Collision smoothing

* Locked in

* 30tps working

* r

* fixes

* Best

* Fixes + CVars

* CVars in place

* Pushies

* Opt attempt 1

* Revert "Opt attempt 1"

This reverts commit 5ccd72dcbea09261a992aa1f7f05df169a1ce676.

* Fix mispredicts

* Ready-ish

* better

* Cleanup

* Fix conveyor power mispredict

* Forgetting to actually do deltas

* Fix buckle pushes

* Bagel save

* Revert "Bagel save"

This reverts commit 1b93fda81fb852d89b89b0beae0b80f8a61165f2.

* Conveyor resave

* Fix prediction

* Mob movement rewrite

* Bandaid

* Working version

* Tentatively working

* Friction to fix cornering

* More fixes

* Revert bagel

* Revert this

* Bad parity

* Working

* Fixes

* Woops

* Doc comments

* Pen cap cvar

* StandingState cleanup and sub

* Fix downed mobs

* fish

* client

* Disable pushing on tests

* More variables

* Movement mods

* Mass diff

* 1 more tweak

* Cvar

18 files changed:
Content.Client/Buckle/BuckleSystem.cs
Content.Client/Movement/Systems/MobCollisionSystem.cs [new file with mode: 0644]
Content.Client/Physics/Controllers/MoverController.cs
Content.IntegrationTests/PoolManager.Cvars.cs
Content.Server/Movement/Commands/ToggleMobCollisionCommand.cs [new file with mode: 0644]
Content.Server/Movement/Systems/MobCollisionSystem.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.Movement.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.Physics.cs
Content.Shared/Movement/Components/MobCollisionComponent.cs [new file with mode: 0644]
Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs [new file with mode: 0644]
Content.Shared/Movement/Systems/SharedMoverController.Relay.cs
Content.Shared/Movement/Systems/SharedMoverController.cs
Content.Shared/Physics/Controllers/SharedConveyorController.cs
Content.Shared/Standing/StandingStateSystem.cs
Resources/ConfigPresets/Build/development.toml
Resources/ConfigPresets/WizardsDen/wizardsDen.toml
Resources/Prototypes/Entities/Mobs/base.yml
Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml

index 40b2092a26cfc14ef00ba188e2e558907ae31ec5..748f15922f3e91effded09e8f1dd605f3360fb82 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Client.Rotation;
 using Content.Shared.Buckle;
 using Content.Shared.Buckle.Components;
+using Content.Shared.Movement.Systems;
 using Content.Shared.Rotation;
 using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
@@ -21,6 +22,15 @@ internal sealed class BuckleSystem : SharedBuckleSystem
         SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
         SubscribeLocalEvent<BuckleComponent, BuckledEvent>(OnBuckledEvent);
         SubscribeLocalEvent<BuckleComponent, UnbuckledEvent>(OnUnbuckledEvent);
+        SubscribeLocalEvent<BuckleComponent, AttemptMobCollideEvent>(OnMobCollide);
+    }
+
+    private void OnMobCollide(Entity<BuckleComponent> ent, ref AttemptMobCollideEvent args)
+    {
+        if (ent.Comp.Buckled)
+        {
+            args.Cancelled = true;
+        }
     }
 
     private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
diff --git a/Content.Client/Movement/Systems/MobCollisionSystem.cs b/Content.Client/Movement/Systems/MobCollisionSystem.cs
new file mode 100644 (file)
index 0000000..b7d464a
--- /dev/null
@@ -0,0 +1,42 @@
+using System.Numerics;
+using Content.Shared.CCVar;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+using Robust.Client.Player;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Movement.Systems;
+
+public sealed class MobCollisionSystem : SharedMobCollisionSystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+
+    public override void Update(float frameTime)
+    {
+        if (!CfgManager.GetCVar(CCVars.MovementMobPushing))
+            return;
+
+        if (_timing.IsFirstTimePredicted)
+        {
+            var player = _player.LocalEntity;
+
+            if (MobQuery.TryComp(player, out var comp) && PhysicsQuery.TryComp(player, out var physics))
+            {
+                HandleCollisions((player.Value, comp, physics), frameTime);
+            }
+        }
+
+        base.Update(frameTime);
+    }
+
+    protected override void RaiseCollisionEvent(EntityUid uid, Vector2 direction, float speedMod)
+    {
+        RaisePredictiveEvent(new MobCollisionMessage()
+        {
+            Direction = direction,
+            SpeedModifier = speedMod,
+        });
+    }
+}
index d2ac0cdefdc1312f0a4a48bb05a50f6c97bcdd35..37e3d83ddbb6558dd870102db2c1bc7eea48013f 100644 (file)
@@ -62,16 +62,16 @@ public sealed class MoverController : SharedMoverController
 
     private void OnRelayPlayerAttached(Entity<RelayInputMoverComponent> entity, ref LocalPlayerAttachedEvent args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
-        Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity);
         if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
             SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
     }
 
     private void OnRelayPlayerDetached(Entity<RelayInputMoverComponent> entity, ref LocalPlayerDetachedEvent args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
-        Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity);
         if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
             SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
     }
index 23f0ded7df2afd506d994cef35b432b0db109789..2c51bdbc3a3c9b3c3f02f0ca7239d910b7e80223 100644 (file)
@@ -39,6 +39,7 @@ public static partial class PoolManager
         (CVars.NetBufferSize.Name, "0"),
         (CCVars.InteractionRateLimitCount.Name, "9999999"),
         (CCVars.InteractionRateLimitPeriod.Name, "0.1"),
+        (CCVars.MovementMobPushing.Name, "false"),
     };
 
     public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
diff --git a/Content.Server/Movement/Commands/ToggleMobCollisionCommand.cs b/Content.Server/Movement/Commands/ToggleMobCollisionCommand.cs
new file mode 100644 (file)
index 0000000..8276291
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Movement.Commands;
+
+/// <summary>
+/// Temporary command to enable admins to toggle the mob collision cvar.
+/// </summary>
+[AdminCommand(AdminFlags.VarEdit)]
+public sealed class ToggleMobCollisionCommand : IConsoleCommand
+{
+    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+
+    public string Command => "toggle_mob_collision";
+    public string Description => "Toggles mob collision";
+    public string Help => Description;
+    public void Execute(IConsoleShell shell, string argStr, string[] args)
+    {
+        _cfgManager.SetCVar(CCVars.MovementMobPushing, !_cfgManager.GetCVar(CCVars.MovementMobPushing));
+    }
+}
diff --git a/Content.Server/Movement/Systems/MobCollisionSystem.cs b/Content.Server/Movement/Systems/MobCollisionSystem.cs
new file mode 100644 (file)
index 0000000..2badac5
--- /dev/null
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Content.Shared.CCVar;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.Player;
+
+namespace Content.Server.Movement.Systems;
+
+public sealed class MobCollisionSystem : SharedMobCollisionSystem
+{
+    private EntityQuery<ActorComponent> _actorQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _actorQuery = GetEntityQuery<ActorComponent>();
+        SubscribeLocalEvent<MobCollisionComponent, MobCollisionMessage>(OnServerMobCollision);
+    }
+
+    private void OnServerMobCollision(Entity<MobCollisionComponent> ent, ref MobCollisionMessage args)
+    {
+        MoveMob((ent.Owner, ent.Comp, Transform(ent.Owner)), args.Direction, args.SpeedModifier);
+    }
+
+    public override void Update(float frameTime)
+    {
+        if (!CfgManager.GetCVar(CCVars.MovementMobPushing))
+            return;
+
+        var query = EntityQueryEnumerator<MobCollisionComponent>();
+
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (_actorQuery.HasComp(uid) || !PhysicsQuery.TryComp(uid, out var physics))
+                continue;
+
+            HandleCollisions((uid, comp, physics), frameTime);
+        }
+
+        base.Update(frameTime);
+    }
+
+    protected override void RaiseCollisionEvent(EntityUid uid, Vector2 direction, float speedMod)
+    {
+        RaiseLocalEvent(uid, new MobCollisionMessage()
+        {
+            Direction = direction,
+            SpeedModifier = speedMod,
+        });
+    }
+}
diff --git a/Content.Shared/CCVar/CCVars.Movement.cs b/Content.Shared/CCVar/CCVars.Movement.cs
new file mode 100644 (file)
index 0000000..539a27c
--- /dev/null
@@ -0,0 +1,50 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+    /// <summary>
+    /// Is mob pushing enabled.
+    /// </summary>
+    public static readonly CVarDef<bool> MovementMobPushing =
+        CVarDef.Create("movement.mob_pushing", false, CVar.SERVER | CVar.REPLICATED);
+
+    /// <summary>
+    /// Can we push mobs not moving.
+    /// </summary>
+    public static readonly CVarDef<bool> MovementPushingStatic =
+        CVarDef.Create("movement.pushing_static", true, CVar.SERVER | CVar.REPLICATED);
+
+    /// <summary>
+    /// Dot product for the pushed entity's velocity to a target entity's velocity before it gets moved.
+    /// </summary>
+    public static readonly CVarDef<float> MovementPushingVelocityProduct =
+        CVarDef.Create("movement.pushing_velocity_product", 0.0f, CVar.SERVER | CVar.REPLICATED);
+
+    /// <summary>
+    /// Cap for how much an entity can be pushed per second.
+    /// </summary>
+    public static readonly CVarDef<float> MovementPushingCap =
+        CVarDef.Create("movement.pushing_cap", 100f, CVar.SERVER | CVar.REPLICATED);
+
+    /// <summary>
+    /// Minimum pushing impulse per tick. If the value is below this it rounds to 0.
+    /// This is an optimisation to avoid pushing small values that won't actually move the mobs.
+    /// </summary>
+    public static readonly CVarDef<float> MovementMinimumPush =
+        CVarDef.Create("movement.minimum_push", 0.1f, CVar.SERVER | CVar.REPLICATED);
+
+    // Really this just exists because hot reloading is cooked on rider.
+    /// <summary>
+    /// Penetration depth cap for considering mob collisions.
+    /// </summary>
+    public static readonly CVarDef<float> MovementPenetrationCap =
+        CVarDef.Create("movement.penetration_cap", 0.3f, CVar.SERVER | CVar.REPLICATED);
+
+    /// <summary>
+    /// Based on the mass difference multiplies the push amount by this proportionally.
+    /// </summary>
+    public static readonly CVarDef<float> MovementPushMassCap =
+        CVarDef.Create("movement.push_mass_cap", 1.75f, CVar.SERVER | CVar.REPLICATED);
+}
index 379676b5df91c32bd15eb186cc78c21641c3e4d5..32f81f023d4adf1673cfd7dd093626cb4c30f885 100644 (file)
@@ -15,13 +15,4 @@ public sealed partial class CCVars
 
     public static readonly CVarDef<float> StopSpeed =
         CVarDef.Create("physics.stop_speed", 0.1f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
-
-    /// <summary>
-    ///     Whether mobs can push objects like lockers.
-    /// </summary>
-    /// <remarks>
-    ///     Technically client doesn't need to know about it but this may prevent a bug in the distant future so it stays.
-    /// </remarks>
-    public static readonly CVarDef<bool> MobPushing =
-        CVarDef.Create("physics.mob_pushing", false, CVar.REPLICATED | CVar.SERVER);
 }
diff --git a/Content.Shared/Movement/Components/MobCollisionComponent.cs b/Content.Shared/Movement/Components/MobCollisionComponent.cs
new file mode 100644 (file)
index 0000000..437cdfd
--- /dev/null
@@ -0,0 +1,60 @@
+using System.Numerics;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// Handles mobs pushing against each other.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)]
+public sealed partial class MobCollisionComponent : Component
+{
+    // If you want to tweak the feel of the pushing use SpeedModifier and Strength.
+    // Strength goes both ways and affects how much the other mob is pushed by so controls static pushing a lot.
+    // Speed mod affects your own mob primarily.
+
+    /// <summary>
+    /// Is this mob currently colliding? Used for SpeedModifier.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Colliding;
+
+    // TODO: I hate this but also I couldn't quite figure out a way to avoid having to dirty it every tick.
+    // The issue is it's a time target that changes constantly so we can't just use a timespan.
+    // However that doesn't mean it should be modified every tick if we're still colliding.
+
+    /// <summary>
+    /// Buffer time for <see cref="SpeedModifier"/> to keep applying after the entities are no longer colliding.
+    /// Without this you will get jittering unless you are very specific with your values.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float BufferAccumulator = SharedMobCollisionSystem.BufferTime;
+
+    /// <summary>
+    /// The speed modifier for mobs currently pushing.
+    /// By setting this low you can ensure you don't have to set the push-strength too high if you can push static entities.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float SpeedModifier = 1f;
+
+    [DataField, AutoNetworkedField]
+    public float MinimumSpeedModifier = 0.35f;
+
+    /// <summary>
+    /// Strength of the pushback for entities. This is combined between the 2 entities being pushed.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float Strength = 50f;
+
+    // Yes I know, I will deal with it if I ever refactor collision layers due to misuse.
+    // If anything it probably needs some assurance on mobcollisionsystem for it.
+    /// <summary>
+    /// Fixture to listen to for mob collisions.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public string FixtureId = "flammable";
+
+    [DataField, AutoNetworkedField]
+    public Vector2 Direction;
+}
diff --git a/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs b/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs
new file mode 100644 (file)
index 0000000..bcc1fd6
--- /dev/null
@@ -0,0 +1,338 @@
+using System.Numerics;
+using Content.Shared.CCVar;
+using Content.Shared.Movement.Components;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Movement.Systems;
+
+public abstract class SharedMobCollisionSystem : EntitySystem
+{
+    [Dependency] protected readonly IConfigurationManager CfgManager = default!;
+    [Dependency] private   readonly IRobustRandom _random = default!;
+    [Dependency] private   readonly MovementSpeedModifierSystem _moveMod = default!;
+    [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
+    [Dependency] private   readonly SharedTransformSystem _xformSystem = default!;
+
+    protected EntityQuery<MobCollisionComponent> MobQuery;
+    protected EntityQuery<PhysicsComponent> PhysicsQuery;
+
+    /// <summary>
+    /// <see cref="CCVars.MovementPushingCap"/>
+    /// </summary>
+    private float _pushingCap;
+
+    /// <summary>
+    /// <see cref="CCVars.MovementPushingVelocityProduct"/>
+    /// </summary>
+    private float _pushingDotProduct;
+
+    /// <summary>
+    /// <see cref="CCVars.MovementMinimumPush"/>
+    /// </summary>
+    private float _minimumPushSquared = 0.01f;
+
+    private float _penCap;
+
+    /// <summary>
+    /// Time after we stop colliding with another mob before adjusting the movespeedmodifier.
+    /// This is required so if we stop colliding for a frame we don't fully reset and get jerky movement.
+    /// </summary>
+    public const float BufferTime = 0.2f;
+
+    private float _massDiffCap;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        UpdatePushCap();
+        Subs.CVar(CfgManager, CVars.NetTickrate, _ => UpdatePushCap());
+        Subs.CVar(CfgManager, CCVars.MovementMinimumPush, val => _minimumPushSquared = val * val, true);
+        Subs.CVar(CfgManager, CCVars.MovementPenetrationCap, val => _penCap = val, true);
+        Subs.CVar(CfgManager, CCVars.MovementPushingCap, _ => UpdatePushCap());
+        Subs.CVar(CfgManager, CCVars.MovementPushingVelocityProduct,
+            value =>
+            {
+                _pushingDotProduct = value;
+            }, true);
+        Subs.CVar(CfgManager, CCVars.MovementPushMassCap, val => _massDiffCap = val, true);
+
+        MobQuery = GetEntityQuery<MobCollisionComponent>();
+        PhysicsQuery = GetEntityQuery<PhysicsComponent>();
+        SubscribeAllEvent<MobCollisionMessage>(OnCollision);
+        SubscribeLocalEvent<MobCollisionComponent, RefreshMovementSpeedModifiersEvent>(OnMoveModifier);
+
+        UpdatesBefore.Add(typeof(SharedPhysicsSystem));
+    }
+
+    private void UpdatePushCap()
+    {
+        _pushingCap = (1f / CfgManager.GetCVar(CVars.NetTickrate)) * CfgManager.GetCVar(CCVars.MovementPushingCap);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = AllEntityQuery<MobCollisionComponent>();
+
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (!comp.Colliding)
+                continue;
+
+            comp.BufferAccumulator -= frameTime;
+            DirtyField(uid, comp, nameof(MobCollisionComponent.BufferAccumulator));
+            var direction = comp.Direction;
+
+            if (comp.BufferAccumulator <= 0f)
+            {
+                SetColliding((uid, comp), false, 1f);
+            }
+            // Apply the mob collision; if it's too low ignore it (e.g. if mob friction would overcome it).
+            // This is so we don't spam velocity changes every tick. It's not that expensive for physics but
+            // avoids the networking side.
+            else if (direction != Vector2.Zero && PhysicsQuery.TryComp(uid, out var physics))
+            {
+                DebugTools.Assert(direction.LengthSquared() >= _minimumPushSquared);
+
+                if (direction.Length() > _pushingCap)
+                {
+                    direction = direction.Normalized() * _pushingCap;
+                }
+
+                Physics.ApplyLinearImpulse(uid, direction * physics.Mass, body: physics);
+                comp.Direction = Vector2.Zero;
+                DirtyField(uid, comp, nameof(MobCollisionComponent.Direction));
+            }
+        }
+    }
+
+    private void OnMoveModifier(Entity<MobCollisionComponent> ent, ref RefreshMovementSpeedModifiersEvent args)
+    {
+        if (!ent.Comp.Colliding)
+            return;
+
+        args.ModifySpeed(ent.Comp.SpeedModifier);
+    }
+
+    private void SetColliding(Entity<MobCollisionComponent> entity, bool value, float speedMod)
+    {
+        if (value)
+        {
+            entity.Comp.BufferAccumulator = BufferTime;
+            DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.BufferAccumulator));
+        }
+        else
+        {
+            DebugTools.Assert(speedMod.Equals(1f));
+        }
+
+        if (entity.Comp.Colliding != value)
+        {
+            entity.Comp.Colliding = value;
+            DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.Colliding));
+        }
+
+        if (!entity.Comp.SpeedModifier.Equals(speedMod))
+        {
+            entity.Comp.SpeedModifier = speedMod;
+            _moveMod.RefreshMovementSpeedModifiers(entity.Owner);
+            DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.SpeedModifier));
+        }
+    }
+
+    private void OnCollision(MobCollisionMessage msg, EntitySessionEventArgs args)
+    {
+        var player = args.SenderSession.AttachedEntity;
+
+        if (!MobQuery.TryComp(player, out var comp))
+            return;
+
+        var xform = Transform(player.Value);
+
+        // If not parented directly to a grid then fail it.
+        if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid)
+            return;
+
+        var direction = msg.Direction;
+
+        MoveMob((player.Value, comp, xform), direction, msg.SpeedModifier);
+    }
+
+    protected void MoveMob(Entity<MobCollisionComponent, TransformComponent> entity, Vector2 direction, float speedMod)
+    {
+        // Length too short to do anything.
+        var pushing = true;
+
+        if (direction.LengthSquared() < _minimumPushSquared)
+        {
+            pushing = false;
+            direction = Vector2.Zero;
+            speedMod = 1f;
+        }
+        else if (float.IsNaN(direction.X) || float.IsNaN(direction.Y))
+        {
+            direction = Vector2.Zero;
+        }
+
+        speedMod = Math.Clamp(speedMod, 0f, 1f);
+
+        SetColliding(entity, pushing, speedMod);
+
+        if (direction == entity.Comp1.Direction)
+            return;
+
+        entity.Comp1.Direction = direction;
+        DirtyField(entity.Owner, entity.Comp1, nameof(MobCollisionComponent.Direction));
+    }
+
+    protected bool HandleCollisions(Entity<MobCollisionComponent, PhysicsComponent> entity, float frameTime)
+    {
+        var physics = entity.Comp2;
+
+        if (physics.ContactCount == 0)
+            return false;
+
+        var ourVelocity = entity.Comp2.LinearVelocity;
+
+        if (ourVelocity == Vector2.Zero && !CfgManager.GetCVar(CCVars.MovementPushingStatic))
+            return false;
+
+        var xform = Transform(entity.Owner);
+
+        if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid)
+            return false;
+
+        var ev = new AttemptMobCollideEvent();
+
+        RaiseLocalEvent(entity.Owner, ref ev);
+
+        if (ev.Cancelled)
+            return false;
+
+        var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
+        var ourTransform = new Transform(worldPos, worldRot);
+        var contacts = Physics.GetContacts(entity.Owner);
+        var direction = Vector2.Zero;
+        var contactCount = 0;
+        var ourMass = physics.FixturesMass;
+        var speedMod = 1f;
+
+        while (contacts.MoveNext(out var contact))
+        {
+            if (!contact.IsTouching)
+                continue;
+
+            var ourFixture = contact.OurFixture(entity.Owner);
+
+            if (ourFixture.Id != entity.Comp1.FixtureId)
+                continue;
+
+            var other = contact.OtherEnt(entity.Owner);
+
+            if (!MobQuery.TryComp(other, out var otherComp) || !PhysicsQuery.TryComp(other, out var otherPhysics))
+                continue;
+
+            var velocityProduct = Vector2.Dot(ourVelocity, otherPhysics.LinearVelocity);
+
+            // If we're moving opposite directions for example then ignore (based on cvar).
+            if (velocityProduct < _pushingDotProduct)
+            {
+                continue;
+            }
+
+            var targetEv = new AttemptMobTargetCollideEvent();
+            RaiseLocalEvent(other, ref targetEv);
+
+            if (targetEv.Cancelled)
+                continue;
+
+            // TODO: More robust overlap detection.
+            var otherTransform = Physics.GetPhysicsTransform(other);
+            var diff = ourTransform.Position - otherTransform.Position;
+
+            if (diff == Vector2.Zero)
+            {
+                diff = _random.NextVector2(0.01f);
+            }
+
+            // 0.7 for 0.35 + 0.35 for mob bounds (see TODO above).
+            // Clamp so we don't get a heap of penetration depth and suddenly lurch other mobs.
+            // This is also so we don't have to trigger the speed-cap above.
+            // Maybe we just do speedcap and dump this? Though it's less configurable and the cap is just there for cheaters.
+            var penDepth = Math.Clamp(0.7f - diff.Length(), 0f, _penCap);
+
+            // Sum the strengths so we get pushes back the same amount (impulse-wise, ignoring prediction).
+            var mobMovement = penDepth * diff.Normalized() * (entity.Comp1.Strength + otherComp.Strength);
+
+            // Big mob push smaller mob, needs fine-tuning and potentially another co-efficient.
+            if (_massDiffCap > 0f)
+            {
+                var modifier = Math.Clamp(
+                    otherPhysics.FixturesMass / ourMass,
+                    1f / _massDiffCap,
+                    _massDiffCap);
+
+                mobMovement *= modifier;
+
+                var speedReduction = 1f - entity.Comp1.MinimumSpeedModifier;
+                var speedModifier = Math.Clamp(
+                    1f - speedReduction * modifier,
+                    entity.Comp1.MinimumSpeedModifier, 1f);
+
+                speedMod = MathF.Min(speedModifier, 1f);
+            }
+
+            // Need the push strength proportional to penetration depth.
+            direction += mobMovement;
+            contactCount++;
+        }
+
+        if (direction == Vector2.Zero)
+        {
+            return contactCount > 0;
+        }
+
+        direction *= frameTime;
+        RaiseCollisionEvent(entity.Owner, direction, speedMod);
+        return true;
+    }
+
+    protected abstract void RaiseCollisionEvent(EntityUid uid, Vector2 direction, float speedmodifier);
+
+    /// <summary>
+    /// Raised from client -> server indicating mob push direction OR server -> server for NPC mob pushes.
+    /// </summary>
+    [Serializable, NetSerializable]
+    protected sealed class MobCollisionMessage : EntityEventArgs
+    {
+        public Vector2 Direction;
+        public float SpeedModifier;
+    }
+}
+
+/// <summary>
+/// Raised on the entity itself when attempting to handle mob collisions.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptMobCollideEvent
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Raised on the other entity when attempting mob collisions.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptMobTargetCollideEvent
+{
+    public bool Cancelled;
+}
index 81569553772a73a97c776691317c8b10299b07df..f843b664359be4752b8abf773846593b558689a3 100644 (file)
@@ -14,12 +14,12 @@ public abstract partial class SharedMoverController
 
     private void OnAfterRelayTargetState(Entity<MovementRelayTargetComponent> entity, ref AfterAutoHandleStateEvent args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
     }
 
     private void OnAfterRelayState(Entity<RelayInputMoverComponent> entity, ref AfterAutoHandleStateEvent args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
     }
 
     /// <summary>
@@ -42,7 +42,7 @@ public abstract partial class SharedMoverController
         {
             oldTarget.Source = EntityUid.Invalid;
             RemComp(component.RelayEntity, oldTarget);
-            Physics.UpdateIsPredicted(component.RelayEntity);
+            PhysicsSystem.UpdateIsPredicted(component.RelayEntity);
         }
 
         var targetComp = EnsureComp<MovementRelayTargetComponent>(relayEntity);
@@ -50,11 +50,11 @@ public abstract partial class SharedMoverController
         {
             oldRelay.RelayEntity = EntityUid.Invalid;
             RemComp(targetComp.Source, oldRelay);
-            Physics.UpdateIsPredicted(targetComp.Source);
+            PhysicsSystem.UpdateIsPredicted(targetComp.Source);
         }
 
-        Physics.UpdateIsPredicted(uid);
-        Physics.UpdateIsPredicted(relayEntity);
+        PhysicsSystem.UpdateIsPredicted(uid);
+        PhysicsSystem.UpdateIsPredicted(relayEntity);
         component.RelayEntity = relayEntity;
         targetComp.Source = uid;
         Dirty(uid, component);
@@ -63,8 +63,8 @@ public abstract partial class SharedMoverController
 
     private void OnRelayShutdown(Entity<RelayInputMoverComponent> entity, ref ComponentShutdown args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
-        Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity);
 
         if (TryComp<InputMoverComponent>(entity.Comp.RelayEntity, out var inputMover))
             SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
@@ -78,8 +78,8 @@ public abstract partial class SharedMoverController
 
     private void OnTargetRelayShutdown(Entity<MovementRelayTargetComponent> entity, ref ComponentShutdown args)
     {
-        Physics.UpdateIsPredicted(entity.Owner);
-        Physics.UpdateIsPredicted(entity.Comp.Source);
+        PhysicsSystem.UpdateIsPredicted(entity.Owner);
+        PhysicsSystem.UpdateIsPredicted(entity.Comp.Source);
 
         if (Timing.ApplyingState)
             return;
index 6456444080380b6299d6e09e996d90c98216198e..76e2f3436888c50fc6cd3c2379c757eba9bd4a17 100644 (file)
@@ -43,7 +43,6 @@ public abstract partial class SharedMoverController : VirtualController
     [Dependency] private   readonly SharedContainerSystem _container = default!;
     [Dependency] private   readonly SharedMapSystem _mapSystem = default!;
     [Dependency] private   readonly SharedGravitySystem _gravity = default!;
-    [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
     [Dependency] private   readonly SharedTransformSystem _transform = default!;
     [Dependency] private   readonly TagSystem _tags = default!;
 
@@ -105,6 +104,14 @@ public abstract partial class SharedMoverController : VirtualController
     public override void UpdateAfterSolve(bool prediction, float frameTime)
     {
         base.UpdateAfterSolve(prediction, frameTime);
+
+        var query = AllEntityQuery<InputMoverComponent, PhysicsComponent>();
+
+        while (query.MoveNext(out var uid, out var _, out var physics))
+        {
+            //PhysicsSystem.SetLinearVelocity(uid, Vector2.Zero, body: physics);
+        }
+
         UsedMobMovement.Clear();
     }
 
index 07bf6c7332c7dbcdbe24ac397b3edfa098b8ea80..4b2523b1d7fc036f8b905ec1472eb3d6fa71b324 100644 (file)
@@ -1,7 +1,6 @@
 using System.Numerics;
 using Content.Shared.Conveyor;
 using Content.Shared.Gravity;
-using Content.Shared.Magic;
 using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Systems;
index c534f4795587ed6643d67c4bda560e473580076a..86d2b961eb5fa9200c211b569e40086c83110df0 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Hands.Components;
+using Content.Shared.Movement.Systems;
 using Content.Shared.Physics;
 using Content.Shared.Rotation;
 using Robust.Shared.Audio.Systems;
@@ -16,6 +17,29 @@ public sealed class StandingStateSystem : EntitySystem
     // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited.
     private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
 
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide);
+        SubscribeLocalEvent<StandingStateComponent, AttemptMobTargetCollideEvent>(OnMobTargetCollide);
+    }
+
+    private void OnMobTargetCollide(Entity<StandingStateComponent> ent, ref AttemptMobTargetCollideEvent args)
+    {
+        if (!ent.Comp.Standing)
+        {
+            args.Cancelled = true;
+        }
+    }
+
+    private void OnMobCollide(Entity<StandingStateComponent> ent, ref AttemptMobCollideEvent args)
+    {
+        if (!ent.Comp.Standing)
+        {
+            args.Cancelled = true;
+        }
+    }
+
     public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null)
     {
         if (!Resolve(uid, ref standingState, false))
index 7990b3545b48af7920ffbca2a70d2863613f13de..4465ea8ee039f22715decf5496dcd4db2c2dd44b 100644 (file)
@@ -15,6 +15,9 @@ quick_lottery = true
 [gateway]
 generator_enabled = false
 
+[movement]
+mob_pushing = true
+
 [physics]
 # Makes mapping annoying
 grid_splitting = false
index 558e60da6911961f5f07ab2e196bb3bc3fe17771..b29ee87d5c065384f8d15276e3e54a8155c7e3cd 100644 (file)
@@ -31,6 +31,9 @@ appeal     = "https://appeal.ss14.io"
 [server]
 rules_file = "StandardRuleset"
 
+[movement]
+mob_pushing = true
+
 [net]
 max_connections = 1024
 
index 970ec52cd799c44ebb2ff6f65a5111d00b3675d5..f7c2f74411e67784df36e9d7941118380ee774d7 100644 (file)
@@ -8,6 +8,7 @@
   - type: Sprite
     noRot: true
     drawdepth: Mobs
+  - type: MobCollision
   - type: Physics
     bodyType: KinematicController
   - type: Fixtures
index 33c4dfbe178ebe18ab1fa0ecf282290c526cd125..12b3261dfa06cd537a9a6d445d379a009e2d14bc 100644 (file)
@@ -65,6 +65,7 @@
     - type: Damageable
       damageContainer: Inorganic
       damageModifierSet: Metallic
+    - type: MobCollision
     - type: Physics
       bodyType: Dynamic
     - type: Fixtures
@@ -74,6 +75,7 @@
             !type:PhysShapeAabb
             bounds: "-0.25,-0.25,0.25,0.25"
           density: 190
+          hard: false
           mask:
           - SmallMobMask
           layer: