From 690bb5a8f2349e572264d37f80da046eae56d966 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:18:38 +0200 Subject: [PATCH] Add integration test for the RCD (#40625) * rcd test * fixes * fix * space * review --- .../Tests/Construction/RCDTest.cs | 243 ++++++++++++++++++ .../Interaction/InteractionTest.Helpers.cs | 38 ++- 2 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Construction/RCDTest.cs diff --git a/Content.IntegrationTests/Tests/Construction/RCDTest.cs b/Content.IntegrationTests/Tests/Construction/RCDTest.cs new file mode 100644 index 0000000000..814f7e89aa --- /dev/null +++ b/Content.IntegrationTests/Tests/Construction/RCDTest.cs @@ -0,0 +1,243 @@ +using System.Numerics; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Charges.Systems; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Construction; + +public sealed class RCDTest : InteractionTest +{ + private static readonly EntProtoId RCDProtoId = "RCD"; + private static readonly ProtoId RCDSettingWall = "WallSolid"; + private static readonly ProtoId RCDSettingAirlock = "Airlock"; + private static readonly ProtoId RCDSettingPlating = "Plating"; + private static readonly ProtoId RCDSettingFloorSteel = "FloorSteel"; + private static readonly ProtoId RCDSettingDeconstruct = "Deconstruct"; + private static readonly ProtoId RCDSettingDeconstructTile = "DeconstructTile"; + private static readonly ProtoId RCDSettingDeconstructLattice = "DeconstructLattice"; + + /// + /// Tests RCD construction and deconstruction, as well as selecting options from the radial menu. + /// + [Test] + public async Task RCDConstructionDeconstructionTest() + { + // Place some tiles around the player so that we have space to build. + var pNorth = new EntityCoordinates(SPlayer, new Vector2(0, 1)); + var pSouth = new EntityCoordinates(SPlayer, new Vector2(0, -1)); + var pEast = new EntityCoordinates(SPlayer, new Vector2(1, 0)); + var pWest = new EntityCoordinates(SPlayer, new Vector2(-1, 0)); + + // Use EntityCoordinates relative to the grid because the player turns around when interacting. + pNorth = Transform.WithEntityId(pNorth, MapData.Grid); + pSouth = Transform.WithEntityId(pSouth, MapData.Grid); + pEast = Transform.WithEntityId(pEast, MapData.Grid); + pWest = Transform.WithEntityId(pWest, MapData.Grid); + + await SetTile(Plating, SEntMan.GetNetCoordinates(pNorth), MapData.Grid); + await SetTile(Plating, SEntMan.GetNetCoordinates(pSouth), MapData.Grid); + await SetTile(Plating, SEntMan.GetNetCoordinates(pEast), MapData.Grid); + await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid); + + Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}."); + Assert.That(settingWall.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingAirlock, out var settingAirlock), $"RCDPrototype not found: {RCDSettingAirlock}."); + Assert.That(settingAirlock.Prototype, Is.Not.Null, $"RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingPlating, out var settingPlating), $"RCDPrototype not found: {RCDSettingPlating}."); + Assert.That(settingPlating.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingFloorSteel, out var settingFloorSteel), $"RCDPrototype not found: {RCDSettingFloorSteel}."); + Assert.That(settingFloorSteel.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingDeconstructTile, out var settingDeconstructTile), $"RCDPrototype not found: {RCDSettingDeconstructTile}."); + Assert.That(ProtoMan.TryIndex(RCDSettingDeconstructLattice, out var settingDeconstructLattice), $"RCDPrototype not found: {RCDSettingDeconstructLattice}."); + + var rcd = await PlaceInHands(RCDProtoId); + + // Give the RCD enough charges to do everything. + var sCharges = SEntMan.System(); + await Server.WaitPost(() => + { + sCharges.SetMaxCharges(ToServer(rcd), 10000); + sCharges.SetCharges(ToServer(rcd), 10000); + }); + var initialCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(10000), "RCD did not have the correct amount of charges."); + + // Make sure that picking it up did not open the UI. + Assert.That(IsUiOpen(RcdUiKey.Key), Is.False, "RCD UI was opened when picking it up."); + + // Switch to building walls. + await SetRcdProto(rcd, RCDSettingWall); + + // Build a wall next to the player. + await Interact(null, pNorth); + + // Check that there is exactly one wall. + await RunSeconds(settingWall.Delay + 1); // wait for the construction to finish + await AssertEntityLookup((settingWall.Prototype, 1)); + + // Check that the wall is in the correct tile. + var wallUid = await FindEntity(settingWall.Prototype); + var wallNetUid = FromServer(wallUid); + AssertLocation(wallNetUid, FromServer(pNorth)); + + // Check that the cost of the wall was subtracted from the current charges. + var newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingWall.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Try building another wall in the same spot. + await Interact(null, pNorth); + await RunSeconds(settingWall.Delay + 1); // wait for the construction to finish + + // Check that there is still exactly one wall. + await AssertEntityLookup((settingWall.Prototype, 1)); + + // Check that the failed construction did not cost us any charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(newCharges), "RCD has wrong amount of charges after failing to build something."); + + // Switch to building airlocks. + await SetRcdProto(rcd, RCDSettingAirlock); + + // Build an airlock next to the player. + await Interact(null, pSouth); + + // Check that there is exactly one airlock. + await RunSeconds(settingAirlock.Delay + 1); // wait for the construction to finish + await AssertEntityLookup( + (settingWall.Prototype, 1), + (settingAirlock.Prototype, 1) + ); + + // Check that the airlock is in the correct tile. + var airlockUid = await FindEntity(settingAirlock.Prototype); + var airlockNetUid = FromServer(airlockUid); + AssertLocation(airlockNetUid, FromServer(pSouth)); + + // Check that the cost of the airlock was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingAirlock.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to building plating. + await SetRcdProto(rcd, RCDSettingPlating); + + // Try building plating on existing plating. + await AssertTile(settingPlating.Prototype, FromServer(pEast)); + await Interact(null, pEast); + + // Check that the tile did not change. + await AssertTile(settingPlating.Prototype, FromServer(pEast)); + + // Check that the failed construction did not cost us any charges. + await RunSeconds(settingPlating.Delay + 1); // wait for the construction to finish + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(newCharges), "RCD has wrong amount of charges after failing to build something."); + + // Try building plating on top of lattice. + await AssertTile(Lattice, FromServer(pWest)); + await Interact(null, pWest); + await RunSeconds(settingPlating.Delay + 1); // wait for the construction to finish + + // Check that the tile is now plating. + await AssertTile(settingPlating.Prototype, FromServer(pWest)); + + // Check that the cost of the plating was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingPlating.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to building steel tiles. + await SetRcdProto(rcd, RCDSettingFloorSteel); + + // Try building a steel tile on top of plating. + await Interact(null, pEast); + + // Check that the tile is now a steel tile. + await RunSeconds(settingFloorSteel.Delay + 1); // wait for the construction to finish + await AssertTile(settingFloorSteel.Prototype, FromServer(pEast)); + + // Check that the cost of the plating was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingFloorSteel.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to deconstruction mode. + await SetRcdProto(rcd, RCDSettingDeconstruct); + + // Deconstruct the wall. + Assert.That(SEntMan.TryGetComponent(wallUid, out var wallComp), "Wall entity did not have the RCDDeconstructableComponent."); + await Interact(wallUid, pNorth); + await RunSeconds(wallComp.Delay + 1); // wait for the deconstruction to finish + AssertDeleted(wallNetUid); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - wallComp.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the airlock. + Assert.That(SEntMan.TryGetComponent(airlockUid, out var airlockComp), "Wall entity did not have the RCDDeconstructableComponent."); + await Interact(airlockUid, pSouth); + await RunSeconds(airlockComp.Delay + 1); // wait for the deconstruction to finish + AssertDeleted(airlockNetUid); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - airlockComp.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the steel tile. + await Interact(null, pEast); + await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish + await AssertTile(Lattice, FromServer(pEast)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructTile.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the plating. + await Interact(null, pWest); + await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish + await AssertTile(Lattice, FromServer(pWest)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructTile.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the lattice. + await Interact(null, pWest); + await RunSeconds(settingDeconstructLattice.Delay + 1); // wait for the deconstruction to finish + await AssertTile(null, FromServer(pWest)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructLattice.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + + // Wait for the visual effect to disappear. + await RunSeconds(3); + + // Check that there are no entities left. + await AssertEntityLookup(); + } + + private async Task SetRcdProto(NetEntity rcd, ProtoId protoId) + { + await UseInHand(); + await RunTicks(3); + Assert.That(IsUiOpen(RcdUiKey.Key), Is.True, "RCD UI was not opened when using the RCD while holding it."); + + // Simulating a click on the right control for nested radial menus is very complicated. + // So we just manually send a networking message from the client to tell the server we selected an option. + // TODO: Write a separate test for clicking through a SimpleRadialMenu. + await SendBui(RcdUiKey.Key, new RCDSystemMessage(protoId), rcd); + await CloseBui(RcdUiKey.Key, rcd); + Assert.That(IsUiOpen(RcdUiKey.Key), Is.False, "RCD UI is still open."); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index e92ccf64df..fa16730dd5 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -726,7 +726,7 @@ public abstract partial class InteractionTest tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile; }); - Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId)); + Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId), $"Expected tile at NetCoordinates {coords}: {TileMan[targetTile.TypeId].Name}. But was: {TileMan[tile.TypeId].Name}"); } protected void AssertGridCount(int value) @@ -742,6 +742,20 @@ public abstract partial class InteractionTest Assert.That(count, Is.EqualTo(value)); } + /// + /// Check that some entity is close to a certain coordinate location. + /// + /// The entity to check the location for. Defaults to + /// The coordinates the entity should be at. + /// The maximum allowed distance from the target coords + protected void AssertLocation(NetEntity? target, NetCoordinates coords, float radius = 0.01f) + { + target ??= Target; + Assert.That(target, Is.Not.Null, "No target specified"); + Assert.That(Position(target!.Value).TryDelta(SEntMan, Transform, ToServer(coords), out var delta), "Could not calculate distance between coordinates."); + Assert.That(delta.Length(), Is.LessThanOrEqualTo(radius), $"{SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} was not at the intended location. Distance: {delta}, allowed distance: {radius}"); + } + #endregion #region Entity lookups @@ -986,9 +1000,9 @@ public abstract partial class InteractionTest /// /// Sends a bui message using the given bui key. /// - protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? _ = null) + protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, NetEntity? target = null) { - if (!TryGetBui(key, out var bui)) + if (!TryGetBui(key, out var bui, target)) return; await Client.WaitPost(() => bui.SendMessage(msg)); @@ -1000,9 +1014,9 @@ public abstract partial class InteractionTest /// /// Sends a bui message using the given bui key. /// - protected async Task CloseBui(Enum key, EntityUid? _ = null) + protected async Task CloseBui(Enum key, NetEntity? target = null) { - if (!TryGetBui(key, out var bui)) + if (!TryGetBui(key, out var bui, target)) return; await Client.WaitPost(() => bui.Close()); @@ -1424,15 +1438,25 @@ public abstract partial class InteractionTest protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent); protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent); protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid)); - protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid)); + protected EntityUid ToClient(EntityUid suid) => CEntMan.GetEntity(SEntMan.GetNetEntity(suid)); protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid)); - protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid)); + protected EntityUid? ToClient(EntityUid? suid) => CEntMan.GetEntity(SEntMan.GetNetEntity(suid)); protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords); protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords); protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords); protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords); + protected NetEntity FromServer(EntityUid suid) => SEntMan.GetNetEntity(suid); + protected NetEntity FromClient(EntityUid cuid) => CEntMan.GetNetEntity(cuid); + protected NetEntity? FromServer(EntityUid? suid) => SEntMan.GetNetEntity(suid); + protected NetEntity? FromClient(EntityUid? cuid) => CEntMan.GetNetEntity(cuid); + + protected NetCoordinates FromServer(EntityCoordinates scoords) => SEntMan.GetNetCoordinates(scoords); + protected NetCoordinates FromClient(EntityCoordinates ccoords) => CEntMan.GetNetCoordinates(ccoords); + protected NetCoordinates? FromServer(EntityCoordinates? scoords) => SEntMan.GetNetCoordinates(scoords); + protected NetCoordinates? FromClient(EntityCoordinates? ccoords) => CEntMan.GetNetCoordinates(ccoords); + #endregion #region Metadata & Transforms