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(