From 593804b51792b70c37fe8c58db6b74ba4978d437 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:33:52 +1100 Subject: [PATCH] Mob collisions (#34580) * 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 --- Content.Client/Buckle/BuckleSystem.cs | 10 + .../Movement/Systems/MobCollisionSystem.cs | 42 +++ .../Physics/Controllers/MoverController.cs | 8 +- Content.IntegrationTests/PoolManager.Cvars.cs | 1 + .../Commands/ToggleMobCollisionCommand.cs | 24 ++ .../Movement/Systems/MobCollisionSystem.cs | 51 +++ Content.Shared/CCVar/CCVars.Movement.cs | 50 +++ Content.Shared/CCVar/CCVars.Physics.cs | 9 - .../Components/MobCollisionComponent.cs | 60 ++++ .../Systems/SharedMobCollisionSystem.cs | 338 ++++++++++++++++++ .../Systems/SharedMoverController.Relay.cs | 20 +- .../Movement/Systems/SharedMoverController.cs | 9 +- .../Controllers/SharedConveyorController.cs | 1 - .../Standing/StandingStateSystem.cs | 24 ++ .../ConfigPresets/Build/development.toml | 3 + .../ConfigPresets/WizardsDen/wizardsDen.toml | 3 + Resources/Prototypes/Entities/Mobs/base.yml | 1 + .../Storage/Canisters/gas_canisters.yml | 2 + 18 files changed, 631 insertions(+), 25 deletions(-) create mode 100644 Content.Client/Movement/Systems/MobCollisionSystem.cs create mode 100644 Content.Server/Movement/Commands/ToggleMobCollisionCommand.cs create mode 100644 Content.Server/Movement/Systems/MobCollisionSystem.cs create mode 100644 Content.Shared/CCVar/CCVars.Movement.cs create mode 100644 Content.Shared/Movement/Components/MobCollisionComponent.cs create mode 100644 Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index 40b2092a26..748f15922f 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -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(OnStrapMoveEvent); SubscribeLocalEvent(OnBuckledEvent); SubscribeLocalEvent(OnUnbuckledEvent); + SubscribeLocalEvent(OnMobCollide); + } + + private void OnMobCollide(Entity 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 index 0000000000..b7d464afab --- /dev/null +++ b/Content.Client/Movement/Systems/MobCollisionSystem.cs @@ -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, + }); + } +} diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index d2ac0cdefd..37e3d83ddb 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -62,16 +62,16 @@ public sealed class MoverController : SharedMoverController private void OnRelayPlayerAttached(Entity 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 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); } diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs index 23f0ded7df..2c51bdbc3a 100644 --- a/Content.IntegrationTests/PoolManager.Cvars.cs +++ b/Content.IntegrationTests/PoolManager.Cvars.cs @@ -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 index 0000000000..8276291cb8 --- /dev/null +++ b/Content.Server/Movement/Commands/ToggleMobCollisionCommand.cs @@ -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; + +/// +/// Temporary command to enable admins to toggle the mob collision cvar. +/// +[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 index 0000000000..2badac5676 --- /dev/null +++ b/Content.Server/Movement/Systems/MobCollisionSystem.cs @@ -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 _actorQuery; + + public override void Initialize() + { + base.Initialize(); + _actorQuery = GetEntityQuery(); + SubscribeLocalEvent(OnServerMobCollision); + } + + private void OnServerMobCollision(Entity 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(); + + 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 index 0000000000..539a27c100 --- /dev/null +++ b/Content.Shared/CCVar/CCVars.Movement.cs @@ -0,0 +1,50 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// Is mob pushing enabled. + /// + public static readonly CVarDef MovementMobPushing = + CVarDef.Create("movement.mob_pushing", false, CVar.SERVER | CVar.REPLICATED); + + /// + /// Can we push mobs not moving. + /// + public static readonly CVarDef MovementPushingStatic = + CVarDef.Create("movement.pushing_static", true, CVar.SERVER | CVar.REPLICATED); + + /// + /// Dot product for the pushed entity's velocity to a target entity's velocity before it gets moved. + /// + public static readonly CVarDef MovementPushingVelocityProduct = + CVarDef.Create("movement.pushing_velocity_product", 0.0f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Cap for how much an entity can be pushed per second. + /// + public static readonly CVarDef MovementPushingCap = + CVarDef.Create("movement.pushing_cap", 100f, CVar.SERVER | CVar.REPLICATED); + + /// + /// 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. + /// + public static readonly CVarDef MovementMinimumPush = + CVarDef.Create("movement.minimum_push", 0.1f, CVar.SERVER | CVar.REPLICATED); + + // Really this just exists because hot reloading is cooked on rider. + /// + /// Penetration depth cap for considering mob collisions. + /// + public static readonly CVarDef MovementPenetrationCap = + CVarDef.Create("movement.penetration_cap", 0.3f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Based on the mass difference multiplies the push amount by this proportionally. + /// + public static readonly CVarDef MovementPushMassCap = + CVarDef.Create("movement.push_mass_cap", 1.75f, CVar.SERVER | CVar.REPLICATED); +} diff --git a/Content.Shared/CCVar/CCVars.Physics.cs b/Content.Shared/CCVar/CCVars.Physics.cs index 379676b5df..32f81f023d 100644 --- a/Content.Shared/CCVar/CCVars.Physics.cs +++ b/Content.Shared/CCVar/CCVars.Physics.cs @@ -15,13 +15,4 @@ public sealed partial class CCVars public static readonly CVarDef StopSpeed = CVarDef.Create("physics.stop_speed", 0.1f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); - - /// - /// Whether mobs can push objects like lockers. - /// - /// - /// Technically client doesn't need to know about it but this may prevent a bug in the distant future so it stays. - /// - public static readonly CVarDef 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 index 0000000000..437cdfd409 --- /dev/null +++ b/Content.Shared/Movement/Components/MobCollisionComponent.cs @@ -0,0 +1,60 @@ +using System.Numerics; +using Content.Shared.Movement.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Movement.Components; + +/// +/// Handles mobs pushing against each other. +/// +[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. + + /// + /// Is this mob currently colliding? Used for SpeedModifier. + /// + [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. + + /// + /// Buffer time for to keep applying after the entities are no longer colliding. + /// Without this you will get jittering unless you are very specific with your values. + /// + [DataField, AutoNetworkedField] + public float BufferAccumulator = SharedMobCollisionSystem.BufferTime; + + /// + /// 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. + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 1f; + + [DataField, AutoNetworkedField] + public float MinimumSpeedModifier = 0.35f; + + /// + /// Strength of the pushback for entities. This is combined between the 2 entities being pushed. + /// + [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. + /// + /// Fixture to listen to for mob collisions. + /// + [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 index 0000000000..bcc1fd6d04 --- /dev/null +++ b/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs @@ -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 MobQuery; + protected EntityQuery PhysicsQuery; + + /// + /// + /// + private float _pushingCap; + + /// + /// + /// + private float _pushingDotProduct; + + /// + /// + /// + private float _minimumPushSquared = 0.01f; + + private float _penCap; + + /// + /// 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. + /// + 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(); + PhysicsQuery = GetEntityQuery(); + SubscribeAllEvent(OnCollision); + SubscribeLocalEvent(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(); + + 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 ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.Colliding) + return; + + args.ModifySpeed(ent.Comp.SpeedModifier); + } + + private void SetColliding(Entity 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 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 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); + + /// + /// Raised from client -> server indicating mob push direction OR server -> server for NPC mob pushes. + /// + [Serializable, NetSerializable] + protected sealed class MobCollisionMessage : EntityEventArgs + { + public Vector2 Direction; + public float SpeedModifier; + } +} + +/// +/// Raised on the entity itself when attempting to handle mob collisions. +/// +[ByRefEvent] +public record struct AttemptMobCollideEvent +{ + public bool Cancelled; +} + +/// +/// Raised on the other entity when attempting mob collisions. +/// +[ByRefEvent] +public record struct AttemptMobTargetCollideEvent +{ + public bool Cancelled; +} diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs index 8156955377..f843b66435 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs @@ -14,12 +14,12 @@ public abstract partial class SharedMoverController private void OnAfterRelayTargetState(Entity entity, ref AfterAutoHandleStateEvent args) { - Physics.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Owner); } private void OnAfterRelayState(Entity entity, ref AfterAutoHandleStateEvent args) { - Physics.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Owner); } /// @@ -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(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 entity, ref ComponentShutdown args) { - Physics.UpdateIsPredicted(entity.Owner); - Physics.UpdateIsPredicted(entity.Comp.RelayEntity); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity); if (TryComp(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 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; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 6456444080..76e2f34368 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -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(); + + while (query.MoveNext(out var uid, out var _, out var physics)) + { + //PhysicsSystem.SetLinearVelocity(uid, Vector2.Zero, body: physics); + } + UsedMobMovement.Clear(); } diff --git a/Content.Shared/Physics/Controllers/SharedConveyorController.cs b/Content.Shared/Physics/Controllers/SharedConveyorController.cs index 07bf6c7332..4b2523b1d7 100644 --- a/Content.Shared/Physics/Controllers/SharedConveyorController.cs +++ b/Content.Shared/Physics/Controllers/SharedConveyorController.cs @@ -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; diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index c534f47955..86d2b961eb 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -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(OnMobCollide); + SubscribeLocalEvent(OnMobTargetCollide); + } + + private void OnMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) + { + if (!ent.Comp.Standing) + { + args.Cancelled = true; + } + } + + private void OnMobCollide(Entity 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)) diff --git a/Resources/ConfigPresets/Build/development.toml b/Resources/ConfigPresets/Build/development.toml index 7990b3545b..4465ea8ee0 100644 --- a/Resources/ConfigPresets/Build/development.toml +++ b/Resources/ConfigPresets/Build/development.toml @@ -15,6 +15,9 @@ quick_lottery = true [gateway] generator_enabled = false +[movement] +mob_pushing = true + [physics] # Makes mapping annoying grid_splitting = false diff --git a/Resources/ConfigPresets/WizardsDen/wizardsDen.toml b/Resources/ConfigPresets/WizardsDen/wizardsDen.toml index 558e60da69..b29ee87d5c 100644 --- a/Resources/ConfigPresets/WizardsDen/wizardsDen.toml +++ b/Resources/ConfigPresets/WizardsDen/wizardsDen.toml @@ -31,6 +31,9 @@ appeal = "https://appeal.ss14.io" [server] rules_file = "StandardRuleset" +[movement] +mob_pushing = true + [net] max_connections = 1024 diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index 970ec52cd7..f7c2f74411 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -8,6 +8,7 @@ - type: Sprite noRot: true drawdepth: Mobs + - type: MobCollision - type: Physics bodyType: KinematicController - type: Fixtures diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml index 33c4dfbe17..12b3261dfa 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml @@ -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: