]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add chasm integration tests (#40286)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Wed, 17 Sep 2025 04:19:46 +0000 (06:19 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Sep 2025 04:19:46 +0000 (14:19 +1000)
* add chasm integration test

* fix assert

* fix

* more fixes

* review

Content.IntegrationTests/Tests/Chasm/ChasmTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
Content.IntegrationTests/Tests/Movement/MovementTest.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs

diff --git a/Content.IntegrationTests/Tests/Chasm/ChasmTest.cs b/Content.IntegrationTests/Tests/Chasm/ChasmTest.cs
new file mode 100644 (file)
index 0000000..05c0d6a
--- /dev/null
@@ -0,0 +1,144 @@
+using Content.IntegrationTests.Tests.Movement;
+using Content.Shared.Chasm;
+using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Misc;
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Chasm;
+
+/// <summary>
+/// A test for chasms, which delete entities when a player walks over them.
+/// </summary>
+[TestOf(typeof(ChasmComponent))]
+public sealed class ChasmTest : MovementTest
+{
+    private readonly EntProtoId _chasmProto = "FloorChasmEntity";
+    private readonly EntProtoId _catWalkProto = "Catwalk";
+    private readonly EntProtoId _grapplingGunProto = "WeaponGrapplingGun";
+
+    /// <summary>
+    /// Test that a player falls into the chasm when walking over it.
+    /// </summary>
+    [Test]
+    public async Task ChasmFallTest()
+    {
+        // Spawn a chasm.
+        await SpawnTarget(_chasmProto);
+        Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+        // Attempt (and fail) to walk past the chasm.
+        // If you are modifying the default value of ChasmFallingComponent.DeletionTime this time might need to be adjusted.
+        await Move(DirectionFlag.East, 0.5f);
+
+        // We should be falling right now.
+        Assert.That(TryComp<ChasmFallingComponent>(Player, out var falling), "Player is not falling after walking over a chasm.");
+
+        var fallTime = (float)falling.DeletionTime.TotalSeconds;
+
+        // Wait until we get deleted.
+        await Pair.RunSeconds(fallTime);
+
+        // Check that the player was deleted.
+        AssertDeleted(Player);
+    }
+
+    /// <summary>
+    /// Test that a catwalk placed over a chasm will protect a player from falling.
+    /// </summary>
+    [Test]
+    public async Task ChasmCatwalkTest()
+    {
+        // Spawn a chasm.
+        await SpawnTarget(_chasmProto);
+        Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+        // Spawn a catwalk over the chasm.
+        var catwalk = await Spawn(_catWalkProto);
+
+        // Attempt to walk past the chasm.
+        await Move(DirectionFlag.East, 1f);
+
+        // We should be on the other side.
+        Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a catwalk.");
+
+        // Check that the player is not deleted.
+        AssertExists(Player);
+
+        // Make sure the player is not falling right now.
+        Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after walking over a catwalk.");
+
+        // Delete the catwalk.
+        await Delete(catwalk);
+
+        // Attempt (and fail) to walk past the chasm.
+        await Move(DirectionFlag.West, 1f);
+
+        // Wait until we get deleted.
+        await Pair.RunSeconds(5f);
+
+        // Check that the player was deleted
+        AssertDeleted(Player);
+    }
+
+    /// <summary>
+    /// Tests that a player is able to cross a chasm by using a grappling gun.
+    /// </summary>
+    [Test]
+    public async Task ChasmGrappleTest()
+    {
+        // Spawn a chasm.
+        await SpawnTarget(_chasmProto);
+        Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+        // Give the player a grappling gun.
+        var grapplingGun = await PlaceInHands(_grapplingGunProto);
+        await Pair.RunSeconds(2f); // guns have a cooldown when picking them up
+
+        // Shoot at the wall to the right.
+        Assert.That(WallRight, Is.Not.Null, "No wall to shoot at!");
+        await AttemptShoot(WallRight);
+        await Pair.RunSeconds(2f);
+
+        // Check that the grappling hook is embedded into the wall.
+        Assert.That(TryComp<GrapplingGunComponent>(grapplingGun, out var grapplingGunComp), "Grappling gun did not have GrapplingGunComponent.");
+        Assert.That(grapplingGunComp.Projectile, Is.Not.Null, "Grappling gun projectile does not exist.");
+        Assert.That(SEntMan.TryGetComponent<EmbeddableProjectileComponent>(grapplingGunComp.Projectile, out var embeddable), "Grappling hook was not embeddable.");
+        Assert.That(embeddable.EmbeddedIntoUid, Is.EqualTo(ToServer(WallRight)), "Grappling hook was not embedded into the wall.");
+
+        // Check that the player is hooked.
+        var grapplingSystem = SEntMan.System<SharedGrapplingGunSystem>();
+        Assert.That(grapplingSystem.IsEntityHooked(SPlayer), "Player is not hooked to the wall.");
+        Assert.That(HasComp<JointRelayTargetComponent>(Player), "Player does not have the JointRelayTargetComponent after using a grappling gun.");
+
+        // Attempt to walk past the chasm.
+        await Move(DirectionFlag.East, 1f);
+
+        // We should be on the other side.
+        Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a grappling gun.");
+
+        // Check that the player is not deleted.
+        AssertExists(Player);
+
+        // Make sure the player is not falling right now.
+        Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after moving over a chasm with a grappling gun.");
+
+        // Drop the grappling gun.
+        await Drop();
+
+        // Check that the player no longer hooked.
+        Assert.That(grapplingSystem.IsEntityHooked(SPlayer), Is.False, "Player still hooked after dropping the grappling gun.");
+        Assert.That(HasComp<JointRelayTargetComponent>(Player), Is.False, "Player still has the JointRelayTargetComponent after dropping the grappling gun.");
+
+        // Attempt (and fail) to walk past the chasm.
+        await Move(DirectionFlag.West, 1f);
+
+        // Wait until we get deleted.
+        await Pair.RunSeconds(5f);
+
+        // Check that the player was deleted
+        AssertDeleted(Player);
+    }
+}
index 8a5859fe060831dcf652a2445615340508994d13..c835a36ed5beb1fce818337548c9d7d9bd667d8d 100644 (file)
@@ -10,6 +10,7 @@ using Content.Server.Construction.Components;
 using Content.Server.Gravity;
 using Content.Server.Power.Components;
 using Content.Shared.Atmos;
+using Content.Shared.CombatMode;
 using Content.Shared.Construction.Prototypes;
 using Content.Shared.Gravity;
 using Content.Shared.Item;
@@ -85,7 +86,7 @@ public abstract partial class InteractionTest
     }
 
     /// <summary>
-    /// Spawn an entity entity and set it as the target.
+    /// Spawn an entity at the target coordinates and set it as the target.
     /// </summary>
     [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
 #pragma warning disable CS8774 // Member must have a non-null value when exiting.
@@ -103,6 +104,22 @@ public abstract partial class InteractionTest
     }
 #pragma warning restore CS8774 // Member must have a non-null value when exiting.
 
+    /// <summary>
+    /// Spawn an entity entity at the target coordinates without setting it as the target.
+    /// </summary>
+    protected async Task<NetEntity> Spawn(string prototype)
+    {
+        var entity = NetEntity.Invalid;
+        await Server.WaitPost(() =>
+        {
+            entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
+        });
+
+        await RunTicks(5);
+        AssertPrototype(prototype, entity);
+        return entity;
+    }
+
     /// <summary>
     /// Spawn an entity in preparation for deconstruction
     /// </summary>
@@ -386,6 +403,119 @@ public abstract partial class InteractionTest
 
     #endregion
 
+    # region Combat
+    /// <summary>
+    /// Returns if the player is currently in combat mode.
+    /// </summary>
+    protected bool IsInCombatMode()
+    {
+        if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+        {
+            Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+            return false;
+        }
+
+        return combat.IsInCombatMode;
+    }
+
+    /// <summary>
+    /// Set the combat mode for the player.
+    /// </summary>
+    protected async Task SetCombatMode(bool enabled)
+    {
+        if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+        {
+            Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+            return;
+        }
+
+        await Server.WaitPost(() => SCombatMode.SetInCombatMode(SPlayer, enabled, combat));
+        await RunTicks(1);
+
+        Assert.That(combat.IsInCombatMode, Is.EqualTo(enabled), $"Player could not set combate mode to {enabled}");
+    }
+
+    /// <summary>
+    /// Make the player shoot with their currently held gun.
+    /// The player needs to be able to enter combat mode for this.
+    /// This does not pass a target entity into the GunSystem, meaning that targets that
+    /// need to be aimed at directly won't be hit.
+    /// </summary>
+    /// <remarks>
+    /// Guns have a cooldown when picking them up.
+    /// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
+    /// </remarks>
+    /// <param name="target">The target coordinates to shoot at. Defaults to the current <see cref="TargetCoords"/>.</param>
+    /// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
+    protected async Task AttemptShoot(NetCoordinates? target = null, bool assert = true)
+    {
+        var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
+
+        if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+        {
+            Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+            return;
+        }
+
+        // Enter combat mode before shooting.
+        var wasInCombatMode = IsInCombatMode();
+        await SetCombatMode(true);
+
+        Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+
+        await Server.WaitAssertion(() =>
+        {
+            var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
+            if (assert)
+                Assert.That(success, "Gun failed to shoot.");
+        });
+        await RunTicks(1);
+
+        // If the player was not in combat mode before then disable it again.
+        await SetCombatMode(wasInCombatMode);
+    }
+
+    /// <summary>
+    /// Make the player shoot with their currently held gun.
+    /// The player needs to be able to enter combat mode for this.
+    /// </summary>
+    /// <remarks>
+    /// Guns have a cooldown when picking them up.
+    /// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
+    /// </remarks>
+    /// <param name="target">The target entity to shoot at. Defaults to the current <see cref="Target"/> entity.</param>
+    /// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
+    protected async Task AttemptShoot(NetEntity? target = null, bool assert = true)
+    {
+        var actualTarget = target ?? Target;
+        Assert.That(actualTarget, Is.Not.Null, "No target to shoot at!");
+
+        if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+        {
+            Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+            return;
+        }
+
+        // Enter combat mode before shooting.
+        var wasInCombatMode = IsInCombatMode();
+        await SetCombatMode(true);
+
+        Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+
+        await Server.WaitAssertion(() =>
+        {
+            var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
+            if (assert)
+                Assert.That(success, "Gun failed to shoot.");
+        });
+        await RunTicks(1);
+
+        // If the player was not in combat mode before then disable it again.
+        await SetCombatMode(wasInCombatMode);
+    }
+
+    #endregion
+
     /// <summary>
     /// Wait for any currently active DoAfters to finish.
     /// </summary>
@@ -746,6 +876,18 @@ public abstract partial class InteractionTest
         return SEntMan.GetComponent<T>(ToServer(target!.Value));
     }
 
+    /// <summary>
+    /// Convenience method to check if the target has a component on the server.
+    /// </summary>
+    protected bool HasComp<T>(NetEntity? target = null) where T : IComponent
+    {
+        target ??= Target;
+        if (target == null)
+            Assert.Fail("No target specified");
+
+        return SEntMan.HasComponent<T>(ToServer(target));
+    }
+
     /// <inheritdoc cref="Comp{T}"/>
     protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
     {
@@ -1013,7 +1155,7 @@ public abstract partial class InteractionTest
         }
 
         Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
-        return (TControl) control;
+        return (TControl)control;
     }
 
     /// <summary>
@@ -1177,8 +1319,8 @@ public abstract partial class InteractionTest
         {
             var atmosSystem = SEntMan.System<AtmosphereSystem>();
             var moles = new float[Atmospherics.AdjustedNumberOfGases];
-            moles[(int) Gas.Oxygen] = 21.824779f;
-            moles[(int) Gas.Nitrogen] = 82.10312f;
+            moles[(int)Gas.Oxygen] = 21.824779f;
+            moles[(int)Gas.Nitrogen] = 82.10312f;
             atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
         });
     }
index 0ed42d34764fe9c52a0a46c75ea295b8b32eb227..e523be2bfc3716b1aec261fc54e125f5928a4890 100644 (file)
@@ -7,12 +7,16 @@ using Content.IntegrationTests.Pair;
 using Content.Server.Hands.Systems;
 using Content.Server.Stack;
 using Content.Server.Tools;
+using Content.Shared.CombatMode;
 using Content.Shared.DoAfter;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
+using Content.Shared.Item.ItemToggle;
 using Content.Shared.Mind;
 using Content.Shared.Players;
+using Content.Shared.Weapons.Ranged.Systems;
 using Robust.Client.Input;
+using Robust.Client.State;
 using Robust.Client.UserInterface;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Log;
@@ -21,8 +25,6 @@ using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 using Robust.UnitTesting;
-using Content.Shared.Item.ItemToggle;
-using Robust.Client.State;
 
 namespace Content.IntegrationTests.Tests.Interaction;
 
@@ -107,6 +109,8 @@ public abstract partial class InteractionTest
     protected SharedMapSystem MapSystem = default!;
     protected ISawmill SLogger = default!;
     protected SharedUserInterfaceSystem SUiSys = default!;
+    protected SharedCombatModeSystem SCombatMode = default!;
+    protected SharedGunSystem SGun = default!;
 
     // CLIENT dependencies
     protected IEntityManager CEntMan = default!;
@@ -124,7 +128,7 @@ public abstract partial class InteractionTest
     protected HandsComponent Hands = default!;
     protected DoAfterComponent DoAfters = default!;
 
-    public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
+    public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
 
     // Simple mob that has one hand and can perform misc interactions.
     [TestPrototypes]
@@ -149,6 +153,7 @@ public abstract partial class InteractionTest
     tags:
     - CanPilot
   - type: UserInterface
+  - type: CombatMode
 ";
 
     [SetUp]
@@ -163,6 +168,7 @@ public abstract partial class InteractionTest
         ProtoMan = Server.ResolveDependency<IPrototypeManager>();
         Factory = Server.ResolveDependency<IComponentFactory>();
         STiming = Server.ResolveDependency<IGameTiming>();
+        SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
         HandSys = SEntMan.System<HandsSystem>();
         InteractSys = SEntMan.System<SharedInteractionSystem>();
         ToolSys = SEntMan.System<ToolSystem>();
@@ -173,20 +179,21 @@ public abstract partial class InteractionTest
         SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
         STestSystem = SEntMan.System<InteractionTestSystem>();
         Stack = SEntMan.System<StackSystem>();
-        SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
-        SUiSys = Client.System<SharedUserInterfaceSystem>();
+        SUiSys = SEntMan.System<SharedUserInterfaceSystem>();
+        SCombatMode = SEntMan.System<SharedCombatModeSystem>();
+        SGun = SEntMan.System<SharedGunSystem>();
 
         // client dependencies
         CEntMan = Client.ResolveDependency<IEntityManager>();
         UiMan = Client.ResolveDependency<IUserInterfaceManager>();
         CTiming = Client.ResolveDependency<IGameTiming>();
         InputManager = Client.ResolveDependency<IInputManager>();
+        CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
         InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>();
         CTestSystem = CEntMan.System<InteractionTestSystem>();
         CConSys = CEntMan.System<ConstructionSystem>();
         ExamineSys = CEntMan.System<ExamineSystem>();
-        CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
-        CUiSys = Client.System<SharedUserInterfaceSystem>();
+        CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
 
         // Setup map.
         await Pair.CreateTestMap();
index eba925303887e1b7e629af21c818f06e1986b0a6..44ef02043e83625d990fa9e3537fe2f80d34e4c6 100644 (file)
@@ -24,6 +24,15 @@ public abstract class MovementTest : InteractionTest
     /// </summary>
     protected virtual bool AddWalls => true;
 
+    /// <summary>
+    /// The wall entity on the left side.
+    /// </summary>
+    protected NetEntity? WallLeft;
+    /// <summary>
+    /// The wall entity on the right side.
+    /// </summary>
+    protected NetEntity? WallRight;
+
     [SetUp]
     public override async Task Setup()
     {
@@ -38,8 +47,11 @@ public abstract class MovementTest : InteractionTest
 
         if (AddWalls)
         {
-            await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
-            await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
+            var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
+            var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
+
+            WallLeft = SEntMan.GetNetEntity(sWallLeft);
+            WallRight = SEntMan.GetNetEntity(sWallRight);
         }
 
         await AddGravity();
index 32da51f8bb5ec8ed7e0a5f8c31d5ad42b55f6197..e3fbec0d5dd7f0d0935bec225b1ca5d3a4d61980 100644 (file)
@@ -204,38 +204,40 @@ public abstract partial class SharedGunSystem : EntitySystem
     /// <summary>
     /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
     /// </summary>
-    public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates, EntityUid? target = null)
+    public bool AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates, EntityUid? target = null)
     {
         gun.ShootCoordinates = toCoordinates;
-        AttemptShoot(user, gunUid, gun);
-        gun.ShotCounter = 0;
         gun.Target = target;
+        var result = AttemptShoot(user, gunUid, gun);
+        gun.ShotCounter = 0;
         DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
+        return result;
     }
 
     /// <summary>
     /// Shoots by assuming the gun is the user at default coordinates.
     /// </summary>
-    public void AttemptShoot(EntityUid gunUid, GunComponent gun)
+    public bool AttemptShoot(EntityUid gunUid, GunComponent gun)
     {
         var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
         gun.ShootCoordinates = coordinates;
-        AttemptShoot(gunUid, gunUid, gun);
+        var result = AttemptShoot(gunUid, gunUid, gun);
         gun.ShotCounter = 0;
+        return result;
     }
 
-    private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
+    private bool AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
     {
         if (gun.FireRateModified <= 0f ||
             !_actionBlockerSystem.CanAttack(user))
         {
-            return;
+            return false;
         }
 
         var toCoordinates = gun.ShootCoordinates;
 
         if (toCoordinates == null)
-            return;
+            return false;
 
         var curTime = Timing.CurTime;
 
@@ -247,16 +249,16 @@ public abstract partial class SharedGunSystem : EntitySystem
         };
         RaiseLocalEvent(gunUid, ref prevention);
         if (prevention.Cancelled)
-            return;
+            return false;
 
         RaiseLocalEvent(user, ref prevention);
         if (prevention.Cancelled)
-            return;
+            return false;
 
         // Need to do this to play the clicking sound for empty automatic weapons
         // but not play anything for burst fire.
         if (gun.NextFire > curTime)
-            return;
+            return false;
 
         var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
 
@@ -315,7 +317,7 @@ public abstract partial class SharedGunSystem : EntitySystem
             gun.BurstActivated = false;
             gun.BurstShotsCount = 0;
             gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
-            return;
+            return false;
         }
 
         var fromCoordinates = Transform(user).Coordinates;
@@ -355,10 +357,10 @@ public abstract partial class SharedGunSystem : EntitySystem
                 // May cause prediction issues? Needs more tweaking
                 gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
                 Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
-                return;
+                return false;
             }
 
-            return;
+            return false;
         }
 
         // Handle burstfire
@@ -383,13 +385,14 @@ public abstract partial class SharedGunSystem : EntitySystem
         RaiseLocalEvent(gunUid, ref shotEv);
 
         if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
-            return;
+            return true;
 
         var shooterEv = new ShooterImpulseEvent();
         RaiseLocalEvent(user, ref shooterEv);
 
         if (shooterEv.Push)
             CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
+        return true;
     }
 
     public void Shoot(