using System.Linq; using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.GameState; internal sealed partial class ComponentStateTests : RobustIntegrationTest { /// /// This tests performs a basic check to ensure that there is no issue with entity states referencing other /// entities that the client is not yet aware of. It does this by spawning two entities that reference each other, /// and then ensuring that they get sent to the client one at a time. /// [Test] public async Task UnknownEntityTest() { // Setup auto-comp-states. I hate this. Someone please fix reflection in RobustIntegrationTest var serverOpts = new ServerIntegrationOptions { Pool = false }; var clientOpts = new ClientIntegrationOptions { Pool = false }; var server = StartServer(serverOpts); var client = StartClient(clientOpts); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); var netMan = client.ResolveDependency(); var xforms = server.System(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true)); // Set up map. EntityUid map = default; server.Post(() => { map = server.System().CreateMap(); }); await RunTicks(); // Spawn entities var coordsA = new EntityCoordinates(map, default); var coordsB = new EntityCoordinates(map, new Vector2(100, 100)); EntityUid player = default; EntityUid cPlayer = default; EntityUid serverEntA = default; EntityUid serverEntB = default; NetEntity serverNetA = default; NetEntity serverNetB = default; await server.WaitPost(() => { // Attach player. player = server.EntMan.Spawn(); var session = server.PlayerMan.Sessions.First(); server.PlayerMan.SetAttachedEntity(session, player); server.PlayerMan.JoinGame(session); // Spawn test entities. serverEntA = server.EntMan.SpawnAttachedTo(null, coordsA); serverEntB = server.EntMan.SpawnAttachedTo(null, coordsB); serverNetA = server.EntMan.GetNetEntity(serverEntA); serverNetB = server.EntMan.GetNetEntity(serverEntB); // Setup components var cmp = server.EntMan.EnsureComponent(serverEntA); cmp.Other = serverEntB; server.EntMan.Dirty(serverEntA, cmp); cmp = server.EntMan.EnsureComponent(serverEntB); cmp.Other = serverEntA; server.EntMan.Dirty(serverEntB, cmp); }); await RunTicks(); // Check player got properly attached and only knows about the expected entities await client.WaitPost(() => { cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer)); Assert.That(client.EntMan.EntityExists(cPlayer)); Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False); Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetB)), Is.False); }); // Move the player into PVS range of one of the entities. await server.WaitPost(() => xforms.SetCoordinates(player, coordsB)); await RunTicks(); await client.WaitPost(() => { var clientEntA = client.EntMan.GetEntity(serverNetA); var clientEntB = client.EntMan.GetEntity(serverNetB); Assert.That(client.EntMan.EntityExists(clientEntB), Is.True); Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False); Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp)); Assert.That(cmp?.Other, Is.EqualTo(clientEntA)); }); // Move the player into PVS range of the other entity await server.WaitPost(() => xforms.SetCoordinates(player, coordsA)); await RunTicks(); await client.WaitPost(() => { var clientEntA = client.EntMan.GetEntity(serverNetA); var clientEntB = client.EntMan.GetEntity(serverNetB); Assert.That(client.EntMan.EntityExists(clientEntB), Is.True); Assert.That(client.EntMan.EntityExists(clientEntA), Is.True); Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp)); Assert.That(cmp?.Other, Is.EqualTo(clientEntA)); Assert.That(client.EntMan.TryGetComponent(clientEntA, out cmp)); Assert.That(cmp?.Other, Is.EqualTo(clientEntB)); }); server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, false)); // wait for errors. await RunTicks(); async Task RunTicks() { for (int i = 0; i < 10; i++) { await server!.WaitRunTicks(1); await client!.WaitRunTicks(1); } } await client.WaitPost(() => netMan.ClientDisconnect("")); await server.WaitRunTicks(5); await client.WaitRunTicks(5); } /// /// This is a variant of that deletes one of the entities before the other entity gets sent. /// [Test] public async Task UnknownEntityDeleteTest() { // The first chunk of the test just follows UnknownEntityTest var serverOpts = new ServerIntegrationOptions { Pool = false }; var clientOpts = new ClientIntegrationOptions { Pool = false }; var server = StartServer(serverOpts); var client = StartClient(clientOpts); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); var netMan = client.ResolveDependency(); var xforms = server.System(); Assert.DoesNotThrow(() => client.SetConnectTarget(server)); client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true)); // Set up map. EntityUid map = default; server.Post(() => { map = server.System().CreateMap(); }); await RunTicks(); // Spawn entities var coordsA = new EntityCoordinates(map, default); var coordsB = new EntityCoordinates(map, new Vector2(100, 100)); EntityUid player = default; EntityUid cPlayer = default; EntityUid serverEntA = default; EntityUid serverEntB = default; NetEntity serverNetA = default; NetEntity serverNetB = default; await server.WaitPost(() => { // Attach player. player = server.EntMan.Spawn(); var session = server.PlayerMan.Sessions.First(); server.PlayerMan.SetAttachedEntity(session, player); server.PlayerMan.JoinGame(session); // Spawn test entities. serverEntA = server.EntMan.SpawnAttachedTo(null, coordsA); serverEntB = server.EntMan.SpawnAttachedTo(null, coordsB); serverNetA = server.EntMan.GetNetEntity(serverEntA); serverNetB = server.EntMan.GetNetEntity(serverEntB); // Setup components var cmp = server.EntMan.EnsureComponent(serverEntA); cmp.Other = serverEntB; server.EntMan.Dirty(serverEntA, cmp); cmp = server.EntMan.EnsureComponent(serverEntB); cmp.Other = serverEntA; server.EntMan.Dirty(serverEntB, cmp); }); await RunTicks(); // Check player got properly attached and only knows about the expected entities await client.WaitPost(() => { cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer)); Assert.That(client.EntMan.EntityExists(cPlayer)); Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False); Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetB)), Is.False); }); // Move the player into PVS range of one of the entities. await server.WaitPost(() => xforms.SetCoordinates(player, coordsB)); await RunTicks(); await client.WaitPost(() => { var clientEntA = client.EntMan.GetEntity(serverNetA); var clientEntB = client.EntMan.GetEntity(serverNetB); Assert.That(client.EntMan.EntityExists(clientEntB), Is.True); Assert.That(client.EntMan.EntityExists(clientEntA), Is.False); Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp)); Assert.That(cmp?.Other, Is.EqualTo(clientEntA)); }); // This is where the test difffers from UnknownEntityTest: // We delete the entity that the player knows about before it receives the entity that it references. await server.WaitPost(() => { server.EntMan.DeleteEntity(serverEntB); var comp = server.EntMan.GetComponent(serverEntA); comp.Other = EntityUid.Invalid; server.EntMan.Dirty(serverEntA, comp); }); await RunTicks(); await client.WaitPost(() => { var clientEntA = client.EntMan.GetEntity(serverNetA); var clientEntB = client.EntMan.GetEntity(serverNetB); Assert.That(clientEntA, Is.EqualTo(EntityUid.Invalid)); Assert.That(clientEntB, Is.EqualTo(EntityUid.Invalid)); }); // Move the player into PVS range of the other entity await server.WaitPost(() => xforms.SetCoordinates(player, coordsA)); await RunTicks(); await client.WaitPost(() => { var clientEntA = client.EntMan.GetEntity(serverNetA); var clientEntB = client.EntMan.GetEntity(serverNetB); Assert.That(clientEntB, Is.EqualTo(EntityUid.Invalid)); Assert.That(client.EntMan.EntityExists(clientEntA), Is.True); Assert.That(client.EntMan.TryGetComponent(clientEntA, out UnknownEntityTestComponent? cmp)); Assert.That(cmp?.Other, Is.EqualTo(EntityUid.Invalid)); }); server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, false)); // wait for errors. await RunTicks(); async Task RunTicks() { for (int i = 0; i < 10; i++) { await server!.WaitRunTicks(1); await client!.WaitRunTicks(1); } } await client.WaitPost(() => netMan.ClientDisconnect("")); await server.WaitRunTicks(5); await client.WaitRunTicks(5); } } [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class UnknownEntityTestComponent : Component { [DataField, AutoNetworkedField] public EntityUid? Other; }