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