Add chasm integration tests (#40286)

* add chasm integration test

* fix assert

* fix

* more fixes

* review
This commit is contained in:
slarticodefast
2025-09-17 06:19:46 +02:00
committed by GitHub
parent fc89f231a5
commit a4368264f0
5 changed files with 336 additions and 28 deletions

View File

@@ -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);
}
}

View 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));
});
}

View 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();

View 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();