diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs index 023cf5fe0f..66141d1b9e 100644 --- a/Content.IntegrationTests/ContentIntegrationTest.cs +++ b/Content.IntegrationTests/ContentIntegrationTest.cs @@ -1,13 +1,20 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Content.Client; using Content.Client.Interfaces.Parallax; using Content.Server; using Content.Server.Interfaces.GameTicking; using NUnit.Framework; +using Robust.Server.Interfaces.Maps; +using Robust.Server.Interfaces.Timing; using Robust.Shared.ContentPack; +using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.UnitTesting; using EntryPoint = Content.Client.EntryPoint; @@ -97,13 +104,72 @@ namespace Content.IntegrationTests return (client, server); } + protected async Task InitializeMap(ServerIntegrationInstance server, string mapPath) + { + await server.WaitIdleAsync(); + + var mapManager = server.ResolveDependency(); + var pauseManager = server.ResolveDependency(); + var mapLoader = server.ResolveDependency(); + + IMapGrid grid = null; + + server.Post(() => + { + var mapId = mapManager.CreateMap(); + + pauseManager.AddUninitializedMap(mapId); + + grid = mapLoader.LoadBlueprint(mapId, mapPath); + + pauseManager.DoMapInitialize(mapId); + }); + + await server.WaitIdleAsync(); + + return grid; + } + + protected async Task TryLoadEntities(IntegrationInstance instance, params string[] yamls) + { + await instance.WaitIdleAsync(); + + var prototypeManager = instance.ResolveDependency(); + + instance.Post(() => + { + foreach (var yaml in yamls) + { + using var reader = new StringReader(yaml); + + prototypeManager.LoadFromStream(reader); + } + }); + + await instance.WaitIdleAsync(); + } + + protected async Task WaitUntil(IntegrationInstance instance, Func predicate, int tickStep = 10, int maxTicks = 600) + { + var ticksAwaited = 0; + + while (!predicate(instance) && ticksAwaited < maxTicks) + { + await instance.WaitIdleAsync(); + instance.RunTicks(tickStep); + ticksAwaited += tickStep; + } + + await instance.WaitIdleAsync(); + } + private static async Task StartConnectedPairShared(ClientIntegrationInstance client, ServerIntegrationInstance server) { await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); client.SetConnectTarget(server); - client.Post(() => IoCManager.Resolve().ClientConnect(null, 0, null)); + client.Post(() => IoCManager.Resolve().ClientConnect(null!, 0, null!)); await RunTicksSync(client, server, 10); } diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs new file mode 100644 index 0000000000..bcf10f7940 --- /dev/null +++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs @@ -0,0 +1,143 @@ +using System.IO; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Doors; +using Content.Shared.Physics; +using NUnit.Framework; +using Robust.Server.Console.Commands; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Physics; +using Robust.Shared.Prototypes; +using static Content.Server.GameObjects.Components.Doors.ServerDoorComponent; + +namespace Content.IntegrationTests.Tests.Doors +{ + [TestFixture] + [TestOf(typeof(AirlockComponent))] + public class AirlockTest : ContentIntegrationTest + { + [Test] + public async Task OpenCloseDestroyTest() + { + var server = StartServerDummyTicker(); + + await server.WaitIdleAsync(); + + var mapManager = server.ResolveDependency(); + var entityManager = server.ResolveDependency(); + + IEntity airlock = null; + AirlockComponent airlockComponent = null; + + server.Assert(() => + { + mapManager.CreateNewMapEntity(MapId.Nullspace); + + airlock = entityManager.SpawnEntity("Airlock", MapCoordinates.Nullspace); + + Assert.True(airlock.TryGetComponent(out airlockComponent)); + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed)); + }); + + await server.WaitIdleAsync(); + + server.Assert(() => + { + airlockComponent.Open(); + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Opening)); + }); + + await server.WaitIdleAsync(); + + await WaitUntil(server, _ => airlockComponent.State == DoorState.Open); + + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Open)); + + server.Assert(() => + { + airlockComponent.Close(); + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closing)); + }); + + await WaitUntil(server, _ => airlockComponent.State == DoorState.Closed); + + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed)); + + server.Assert(() => + { + Assert.DoesNotThrow(() => + { + airlock.Delete(); + }); + }); + + server.RunTicks(5); + + await server.WaitIdleAsync(); + } + + [Test] + public async Task AirlockBlockTest() + { + var server = StartServer(); + + await server.WaitIdleAsync(); + + var mapManager = server.ResolveDependency(); + var entityManager = server.ResolveDependency(); + + IEntity human = null; + IEntity airlock = null; + TestController controller = null; + AirlockComponent airlockComponent = null; + + var humanStartingX = -1; + + server.Assert(() => + { + var mapId = new MapId(1); + mapManager.CreateNewMapEntity(mapId); + + var humanCoordinates = new MapCoordinates((humanStartingX, 0), mapId); + human = entityManager.SpawnEntity("HumanMob_Content", humanCoordinates); + + airlock = entityManager.SpawnEntity("Airlock", new MapCoordinates((0, 0), mapId)); + + Assert.True(human.TryGetComponent(out ICollidableComponent collidable)); + + controller = collidable.EnsureController(); + + Assert.True(airlock.TryGetComponent(out airlockComponent)); + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed)); + }); + + await server.WaitIdleAsync(); + + // Push the human towards the airlock + controller.LinearVelocity = (0.5f, 0); + + for (var i = 0; i < 240; i += 10) + { + // Keep the airlock awake so they collide + airlock.GetComponent().WakeBody(); + + // Ensure that it is still closed + Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed)); + + await server.WaitRunTicks(10); + await server.WaitIdleAsync(); + } + + // Sanity check + Assert.That(human.Transform.MapPosition.X, Is.GreaterThan(humanStartingX)); + + // Blocked by the airlock + Assert.That(human.Transform.MapPosition.X, Is.Negative.Or.Zero); + } + + private class TestController : VirtualController { } + } +} diff --git a/Content.Server/GameObjects/Components/Doors/AirlockComponent.cs b/Content.Server/GameObjects/Components/Doors/AirlockComponent.cs index 398e9230c9..1bc53f6ab6 100644 --- a/Content.Server/GameObjects/Components/Doors/AirlockComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/AirlockComponent.cs @@ -144,9 +144,9 @@ namespace Content.Server.GameObjects.Components.Doors } } - protected override DoorState State + public override DoorState State { - set + protected set { base.State = value; // Only show the maintenance panel if the airlock is closed @@ -171,7 +171,11 @@ namespace Content.Server.GameObjects.Components.Doors public override void OnRemove() { - _powerReceiver.OnPowerStateChanged -= PowerDeviceOnOnPowerStateChanged; + if (Owner.TryGetComponent(out _powerReceiver)) + { + _powerReceiver.OnPowerStateChanged -= PowerDeviceOnOnPowerStateChanged; + } + base.OnRemove(); } diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index ba2bc39377..8df3c2601c 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -33,10 +33,10 @@ namespace Content.Server.GameObjects.Components.Doors private DoorState _state = DoorState.Closed; - protected virtual DoorState State + public virtual DoorState State { get => _state; - set => _state = value; + protected set => _state = value; } protected float OpenTimeCounter; @@ -80,7 +80,7 @@ namespace Content.Server.GameObjects.Components.Doors public override void OnRemove() { - _cancellationTokenSource.Cancel(); + _cancellationTokenSource?.Cancel(); _collidableComponent = null; _appearance = null; @@ -336,7 +336,7 @@ namespace Content.Server.GameObjects.Components.Doors } } - protected enum DoorState + public enum DoorState { Closed, Open, diff --git a/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs b/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs index d70331f71e..96f93bb722 100644 --- a/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Atmos/GasTileOverlaySystem.cs @@ -261,7 +261,12 @@ namespace Content.Server.GameObjects.EntitySystems.Atmos // and if not then we won't bother sending the data. foreach (var (gridId, indices) in _invalidTiles) { - var gridEntityId = _mapManager.GetGrid(gridId).GridEntityId; + if (!_mapManager.TryGetGrid(gridId, out var grid)) + { + return; + } + + var gridEntityId = grid.GridEntityId; if (!EntityManager.GetEntity(gridEntityId).TryGetComponent(out GridAtmosphereComponent? gam)) { diff --git a/Content.Shared/Physics/MoverController.cs b/Content.Shared/Physics/MoverController.cs index bf4372a3ca..5554895988 100644 --- a/Content.Shared/Physics/MoverController.cs +++ b/Content.Shared/Physics/MoverController.cs @@ -10,12 +10,14 @@ namespace Content.Shared.Physics { public class MoverController : VirtualController { + [Dependency] private readonly IPhysicsManager _physicsManager = default!; + public override ICollidableComponent? ControlledComponent { protected get; set; } public void Move(Vector2 velocityDirection, float speed) { if (ControlledComponent?.Owner.HasComponent() == false - && IoCManager.Resolve().IsWeightless(ControlledComponent.Owner.Transform.GridPosition)) + && _physicsManager.IsWeightless(ControlledComponent.Owner.Transform.GridPosition)) { return; }