From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Wed, 17 Sep 2025 04:19:46 +0000 (+0200)
Subject: Add chasm integration tests (#40286)
X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=a4368264f0bc14fc96f13099a0910da0a752a3ad;p=space-station-14.git
Add chasm integration tests (#40286)
* add chasm integration test
* fix assert
* fix
* more fixes
* review
---
diff --git a/Content.IntegrationTests/Tests/Chasm/ChasmTest.cs b/Content.IntegrationTests/Tests/Chasm/ChasmTest.cs
new file mode 100644
index 0000000000..05c0d6a829
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Chasm/ChasmTest.cs
@@ -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;
+
+///
+/// A test for chasms, which delete entities when a player walks over them.
+///
+[TestOf(typeof(ChasmComponent))]
+public sealed class ChasmTest : MovementTest
+{
+ private readonly EntProtoId _chasmProto = "FloorChasmEntity";
+ private readonly EntProtoId _catWalkProto = "Catwalk";
+ private readonly EntProtoId _grapplingGunProto = "WeaponGrapplingGun";
+
+ ///
+ /// Test that a player falls into the chasm when walking over it.
+ ///
+ [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(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);
+ }
+
+ ///
+ /// Test that a catwalk placed over a chasm will protect a player from falling.
+ ///
+ [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(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);
+ }
+
+ ///
+ /// Tests that a player is able to cross a chasm by using a grappling gun.
+ ///
+ [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(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(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();
+ Assert.That(grapplingSystem.IsEntityHooked(SPlayer), "Player is not hooked to the wall.");
+ Assert.That(HasComp(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(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(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);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index 8a5859fe06..c835a36ed5 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -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
}
///
- /// Spawn an entity entity and set it as the target.
+ /// Spawn an entity at the target coordinates and set it as the target.
///
[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.
+ ///
+ /// Spawn an entity entity at the target coordinates without setting it as the target.
+ ///
+ protected async Task 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;
+ }
+
///
/// Spawn an entity in preparation for deconstruction
///
@@ -386,6 +403,119 @@ public abstract partial class InteractionTest
#endregion
+ # region Combat
+ ///
+ /// Returns if the player is currently in combat mode.
+ ///
+ 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;
+ }
+
+ ///
+ /// Set the combat mode for the player.
+ ///
+ 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}");
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The target coordinates to shoot at. Defaults to the current .
+ /// If true this method will assert that the gun was successfully fired.
+ 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);
+ }
+
+ ///
+ /// Make the player shoot with their currently held gun.
+ /// The player needs to be able to enter combat mode for this.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The target entity to shoot at. Defaults to the current entity.
+ /// If true this method will assert that the gun was successfully fired.
+ 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
+
///
/// Wait for any currently active DoAfters to finish.
///
@@ -746,6 +876,18 @@ public abstract partial class InteractionTest
return SEntMan.GetComponent(ToServer(target!.Value));
}
+ ///
+ /// Convenience method to check if the target has a component on the server.
+ ///
+ protected bool HasComp(NetEntity? target = null) where T : IComponent
+ {
+ target ??= Target;
+ if (target == null)
+ Assert.Fail("No target specified");
+
+ return SEntMan.HasComponent(ToServer(target));
+ }
+
///
protected bool TryComp(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;
}
///
@@ -1177,8 +1319,8 @@ public abstract partial class InteractionTest
{
var atmosSystem = SEntMan.System();
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));
});
}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 0ed42d3476..e523be2bfc 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -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();
Factory = Server.ResolveDependency();
STiming = Server.ResolveDependency();
+ SLogger = Server.ResolveDependency().RootSawmill;
HandSys = SEntMan.System();
InteractSys = SEntMan.System();
ToolSys = SEntMan.System();
@@ -173,20 +179,21 @@ public abstract partial class InteractionTest
SConstruction = SEntMan.System();
STestSystem = SEntMan.System();
Stack = SEntMan.System();
- SLogger = Server.ResolveDependency().RootSawmill;
- SUiSys = Client.System();
+ SUiSys = SEntMan.System();
+ SCombatMode = SEntMan.System();
+ SGun = SEntMan.System();
// client dependencies
CEntMan = Client.ResolveDependency();
UiMan = Client.ResolveDependency();
CTiming = Client.ResolveDependency();
InputManager = Client.ResolveDependency();
+ CLogger = Client.ResolveDependency().RootSawmill;
InputSystem = CEntMan.System();
CTestSystem = CEntMan.System();
CConSys = CEntMan.System();
ExamineSys = CEntMan.System();
- CLogger = Client.ResolveDependency().RootSawmill;
- CUiSys = Client.System();
+ CUiSys = CEntMan.System();
// Setup map.
await Pair.CreateTestMap();
diff --git a/Content.IntegrationTests/Tests/Movement/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs
index eba9253038..44ef02043e 100644
--- a/Content.IntegrationTests/Tests/Movement/MovementTest.cs
+++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs
@@ -24,6 +24,15 @@ public abstract class MovementTest : InteractionTest
///
protected virtual bool AddWalls => true;
+ ///
+ /// The wall entity on the left side.
+ ///
+ protected NetEntity? WallLeft;
+ ///
+ /// The wall entity on the right side.
+ ///
+ 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();
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
index 32da51f8bb..e3fbec0d5d 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
@@ -204,38 +204,40 @@ public abstract partial class SharedGunSystem : EntitySystem
///
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
///
- 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;
}
///
/// Shoots by assuming the gun is the user at default coordinates.
///
- 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(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(