using System.Linq; using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; public sealed class DetachedParentTest : RobustIntegrationTest { /// /// Check that the client can handle an entity getting attached to an entity that is outside of their PVS range, or /// that they have never seen. Previously this could result in entities with improperly assigned GridUids due to /// an existing/initialized entity being attached to an un-initialized entity on an already initialized grid. /// [Test] public async Task TestDetachedParent() { var server = StartServer(new() {Pool = false}); var client = StartClient(new() {Pool = false}); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); var mapSys = server.System(); var xformSys = server.System(); var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); var netMan = client.ResolveDependency(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Ensure client & server ticks are synced. // Client runs 1 tick ahead { var sTick = (int)server.Timing.CurTick.Value; var cTick = (int)client.Timing.CurTick.Value; var delta = cTick - sTick; if (delta > 1) await server.WaitRunTicks(delta - 1); else if (delta < 1) await client.WaitRunTicks(1 - delta); sTick = (int)server.Timing.CurTick.Value; cTick = (int)client.Timing.CurTick.Value; delta = cTick - sTick; Assert.That(delta, Is.EqualTo(1)); } // Set up map and spawn player MapId mapId = default; EntityUid map = default; EntityUid grid = default; EntityUid parent = default; EntityUid player = default; EntityUid child = default; EntityCoordinates gridCoords = default; EntityCoordinates mapCoords = default; await server.WaitPost(() => { // Cycle through some EntityUids to avoid server-side and client-side uids accidentally matching up. // I made a mistake earlier in this test where I used a server-side uid on the client for (var i = 0; i < 10; i++) { server.EntMan.DeleteEntity(server.EntMan.SpawnEntity(null, MapCoordinates.Nullspace)); } map = mapSys.CreateMap(out mapId); var gridEnt = mapMan.CreateGridEntity(mapId); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); gridCoords = new EntityCoordinates(gridEnt, .5f, .5f); mapCoords = new EntityCoordinates(map, 200, 200); grid = gridEnt.Owner; parent = sEntMan.SpawnEntity(null, gridCoords); player = sEntMan.SpawnEntity(null, gridCoords); child = sEntMan.SpawnEntity(null, mapCoords); // Attach player. var session = sPlayerMan.Sessions.First(); server.PlayerMan.SetAttachedEntity(session, player); sPlayerMan.JoinGame(session); }); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check that transforms are as expected. var childX = server.Transform(child); var parentX = server.Transform(parent); var playerX = server.Transform(player); var gridX = server.Transform(grid); Assert.That(childX.MapID, Is.EqualTo(mapId)); Assert.That(parentX.MapID, Is.EqualTo(mapId)); Assert.That(playerX.MapID, Is.EqualTo(mapId)); Assert.That(gridX.MapID, Is.EqualTo(mapId)); Assert.That(childX.ParentUid, Is.EqualTo(map)); Assert.That(parentX.ParentUid, Is.EqualTo(grid)); Assert.That(playerX.ParentUid, Is.EqualTo(grid)); Assert.That(gridX.ParentUid, Is.EqualTo(map)); Assert.That(childX.GridUid, Is.Null); Assert.That(parentX.GridUid, Is.EqualTo(grid)); Assert.That(playerX.GridUid, Is.EqualTo(grid)); Assert.That(gridX.GridUid, Is.EqualTo(grid)); // Check that the player received the entities, and that their transforms are as expected. // Note that the child entity should be outside of PVS range. var cMap = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map)); var cGrid = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid)); var cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); var cParent = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent)); var cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child)); Assert.That(cMap, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cGrid, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cPlayer, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cParent, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cChild, Is.EqualTo(EntityUid.Invalid)); var cParentX = client.Transform(cParent); var cPlayerX = client.Transform(cPlayer); var cGridX = client.Transform(cGrid); Assert.That(cParentX.MapID, Is.EqualTo(mapId)); Assert.That(cPlayerX.MapID, Is.EqualTo(mapId)); Assert.That(cGridX.MapID, Is.EqualTo(mapId)); Assert.That(cParentX.ParentUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cGrid)); Assert.That(cGridX.ParentUid, Is.EqualTo(cMap)); Assert.That(cParentX.GridUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid)); Assert.That(cGridX.GridUid, Is.EqualTo(cGrid)); // Move the player into pvs range of the child, which will move them outside of the grid & parent's PVS range. await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // the client now knows about the child. cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child)); Assert.That(cChild, Is.Not.EqualTo(EntityUid.Invalid)); var cChildX = client.Transform(cChild); Assert.That(childX.MapID, Is.EqualTo(mapId)); Assert.That(cChildX.ParentUid, Is.EqualTo(cMap)); Assert.That(cChildX.GridUid, Is.Null); // Player transform has updated Assert.That(cPlayerX.GridUid, Is.Null); Assert.That(cPlayerX.MapID, Is.EqualTo(mapId)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cMap)); // But the other entities have left PVS range Assert.That(cParentX.ParentUid, Is.EqualTo(EntityUid.Invalid)); Assert.That(cParentX.MapID, Is.EqualTo(MapId.Nullspace)); Assert.That(cParentX.GridUid, Is.Null); Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) != 0); // Attach the child & player entities to the parent // This is the main step that the test is actually checking var parentCoords = new EntityCoordinates(parent, Vector2.Zero); await server.WaitPost(() => xformSys.SetCoordinates(player, parentCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parentCoords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check that server-side transforms are as expected Assert.That(childX.ParentUid, Is.EqualTo(parent)); Assert.That(parentX.ParentUid, Is.EqualTo(grid)); Assert.That(playerX.ParentUid, Is.EqualTo(parent)); Assert.That(gridX.ParentUid, Is.EqualTo(map)); Assert.That(childX.GridUid, Is.EqualTo(grid)); Assert.That(parentX.GridUid, Is.EqualTo(grid)); Assert.That(playerX.GridUid, Is.EqualTo(grid)); Assert.That(gridX.GridUid, Is.EqualTo(grid)); // Next check the client-side transforms Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) == 0); Assert.That(cChildX.ParentUid, Is.EqualTo(cParent)); Assert.That(cParentX.ParentUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent)); Assert.That(cGridX.ParentUid, Is.EqualTo(cMap)); Assert.That(cChildX.GridUid, Is.EqualTo(cGrid)); Assert.That(cParentX.GridUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid)); Assert.That(cGridX.GridUid, Is.EqualTo(cGrid)); // Repeat the previous test, but this time attaching to an entity that gets spawned outside of PVS range, that // the client never new about previously. await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, mapCoords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Child transform has updated. Assert.That(childX.MapID, Is.EqualTo(mapId)); Assert.That(cChildX.ParentUid, Is.EqualTo(cMap)); Assert.That(cChildX.GridUid, Is.Null); // Player transform has updated Assert.That(cPlayerX.GridUid, Is.Null); Assert.That(cPlayerX.MapID, Is.EqualTo(mapId)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cMap)); // The other entities have left PVS range Assert.That(cParentX.ParentUid, Is.EqualTo(EntityUid.Invalid)); Assert.That(cParentX.MapID, Is.EqualTo(MapId.Nullspace)); Assert.That(cParentX.GridUid, Is.Null); Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) != 0); // Create a new parent entity EntityUid parent2 = default; await server.WaitPost(() => parent2 = sEntMan.SpawnEntity(null, gridCoords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } var parent2X = server.Transform(parent2); Assert.That(parent2X.MapID, Is.EqualTo(mapId)); Assert.That(parent2X.ParentUid, Is.EqualTo(grid)); Assert.That(parent2X.GridUid, Is.EqualTo(grid)); // Client does not know that parent2 exists yet. var cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2)); Assert.That(cParent2, Is.EqualTo(EntityUid.Invalid)); // Attach player & child to the new parent. var parent2Coords = new EntityCoordinates(parent2, Vector2.Zero); await server.WaitPost(() => xformSys.SetCoordinates(player, parent2Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent2Coords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check all the transforms cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2)); Assert.That(cParent2, Is.Not.EqualTo(EntityUid.Invalid)); var cParent2X = client.Transform(cParent2); Assert.That(cChildX.ParentUid, Is.EqualTo(cParent2)); Assert.That(cParent2X.ParentUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent2)); Assert.That(cGridX.ParentUid, Is.EqualTo(cMap)); Assert.That(cParent2X.GridUid, Is.EqualTo(cGrid)); Assert.That(cChildX.GridUid, Is.EqualTo(cGrid)); Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid)); Assert.That(cGridX.GridUid, Is.EqualTo(cGrid)); // Repeat again, but with a new map. // Set up map and spawn player MapId mapId2 = default; EntityUid map2 = default; EntityUid grid2 = default; EntityUid parent3 = default; await server.WaitPost(() => { map2 = mapSys.CreateMap(out mapId2); var gridEnt = mapMan.CreateGridEntity(mapId2); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid2Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid2 = gridEnt.Owner; parent3 = sEntMan.SpawnEntity(null, grid2Coords); }); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check server-side transforms var grid2X = server.Transform(grid2); var parent3X = server.Transform(parent3); Assert.That(parent3X.MapID, Is.EqualTo(mapId2)); Assert.That(grid2X.MapID, Is.EqualTo(mapId2)); Assert.That(parent3X.ParentUid, Is.EqualTo(grid2)); Assert.That(grid2X.ParentUid, Is.EqualTo(map2)); Assert.That(parent3X.GridUid, Is.EqualTo(grid2)); Assert.That(grid2X.GridUid, Is.EqualTo(grid2)); // Client does not know that parent3 exists, but (at least for now) clients always know about all maps and grids. var cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3)); var cGrid2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid2)); var cMap2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map2)); Assert.That(cMap2, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cGrid2, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cParent3, Is.EqualTo(EntityUid.Invalid)); // Attach the entities to the parent on the new map. var parent3Coords = new EntityCoordinates(parent3, Vector2.Zero); await server.WaitPost(() => xformSys.SetCoordinates(player, parent3Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent3Coords)); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check all the transforms cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3)); Assert.That(cParent3, Is.Not.EqualTo(EntityUid.Invalid)); var cParent3X = client.Transform(cParent3); var cGrid2X = client.Transform(cGrid2); Assert.That(cChildX.ParentUid, Is.EqualTo(cParent3)); Assert.That(cParent3X.ParentUid, Is.EqualTo(cGrid2)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent3)); Assert.That(cGrid2X.ParentUid, Is.EqualTo(cMap2)); Assert.That(cParent3X.GridUid, Is.EqualTo(cGrid2)); Assert.That(cChildX.GridUid, Is.EqualTo(cGrid2)); Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid2)); Assert.That(cGrid2X.GridUid, Is.EqualTo(cGrid2)); Assert.That(cParent3X.MapID, Is.EqualTo(mapId2)); Assert.That(cChildX.MapID, Is.EqualTo(mapId2)); Assert.That(cPlayerX.MapID, Is.EqualTo(mapId2)); Assert.That(cGrid2X.MapID, Is.EqualTo(mapId2)); Assert.That(cParent3X.MapUid, Is.EqualTo(cMap2)); Assert.That(cChildX.MapUid, Is.EqualTo(cMap2)); Assert.That(cPlayerX.MapUid, Is.EqualTo(cMap2)); Assert.That(cGrid2X.MapUid, Is.EqualTo(cMap2)); // Create a new map & grid and move entities in the same tick MapId mapId3 = default; EntityUid map3 = default; EntityUid grid3 = default; EntityUid parent4 = default; await server.WaitPost(() => { map3 = mapSys.CreateMap(out mapId3); var gridEnt = mapMan.CreateGridEntity(mapId3); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid3Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid3 = gridEnt.Owner; parent4 = sEntMan.SpawnEntity(null, grid3Coords); var parent4Coords = new EntityCoordinates(parent4, Vector2.Zero); // Move existing entity to new parent xformSys.SetCoordinates(player, parent4Coords); // Move existing parent & child combination to new grid xformSys.SetCoordinates(parent3, grid3Coords); }); for (var i = 0; i < 10; i++) { await server.WaitRunTicks(1); await client.WaitRunTicks(1); } // Check all the transforms var cParent4 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent4)); var cMap3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map3)); var cGrid3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid3)); Assert.That(cParent4, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cMap3, Is.Not.EqualTo(EntityUid.Invalid)); Assert.That(cGrid3, Is.Not.EqualTo(EntityUid.Invalid)); var cParent4X = client.Transform(cParent4); var cGrid3X = client.Transform(cGrid3); Assert.That(cChildX.ParentUid, Is.EqualTo(cParent3)); Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent4)); Assert.That(cParent3X.ParentUid, Is.EqualTo(cGrid3)); Assert.That(cParent4X.ParentUid, Is.EqualTo(cGrid3)); Assert.That(cGrid3X.ParentUid, Is.EqualTo(cMap3)); Assert.That(cChildX.GridUid, Is.EqualTo(cGrid3)); Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid3)); Assert.That(cParent3X.GridUid, Is.EqualTo(cGrid3)); Assert.That(cParent4X.GridUid, Is.EqualTo(cGrid3)); Assert.That(cGrid3X.GridUid, Is.EqualTo(cGrid3)); Assert.That(cChildX.MapID, Is.EqualTo(mapId3)); Assert.That(cPlayerX.MapID, Is.EqualTo(mapId3)); Assert.That(cParent3X.MapID, Is.EqualTo(mapId3)); Assert.That(cParent4X.MapID, Is.EqualTo(mapId3)); Assert.That(cGrid3X.MapID, Is.EqualTo(mapId3)); Assert.That(cChildX.MapUid, Is.EqualTo(cMap3)); Assert.That(cPlayerX.MapUid, Is.EqualTo(cMap3)); Assert.That(cParent3X.MapUid, Is.EqualTo(cMap3)); Assert.That(cParent4X.MapUid, Is.EqualTo(cMap3)); Assert.That(cGrid3X.MapUid, Is.EqualTo(cMap3)); await client.WaitPost(() => netMan.ClientDisconnect("")); await server.WaitRunTicks(5); await client.WaitRunTicks(5); } }