Files

205 lines
7.9 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Server.GameStates;
public sealed class ChunkEntityPvsTest : RobustIntegrationTest
{
[Test]
public async Task ChunkEntityLeaveBeforeCreateDetachesOlderInitialState()
{
await using var pair = await StartConnectedPair();
var (client, server) = pair;
var sEntMan = server.ResolveDependency<IEntityManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cNetMan = client.ResolveDependency<IClientNetManager>();
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
await RunTicksSync(server, client, 10);
NetEntity mapNet = default;
EntityUid map = default;
EntityUid playerUid = default;
EntityCoordinates origin = default;
await server.WaitPost(() =>
{
map = server.System<SharedMapSystem>().CreateMap();
origin = new EntityCoordinates(map, default);
mapNet = sEntMan.GetNetEntity(map);
playerUid = sEntMan.SpawnEntity(null, origin);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, playerUid);
sPlayerMan.JoinGame(session);
});
await RunTicksSync(server, client, 10);
NetEntity chunkEntity = default;
GameTick leaveTick = default;
await server.WaitPost(() =>
{
var chunk = sEntMan.System<ChunkEntitySystem>().GetOrCreateChunk(map, Vector2i.Zero);
chunkEntity = sEntMan.GetNetEntity(chunk.Owner);
// Pick a tick newer than the first entity state the client will receive for this chunk.
leaveTick = new GameTick(server.Timing.CurTick.Value + 100);
});
await client.WaitPost(() =>
{
Assert.That(cEntMan.TryGetEntityData(chunkEntity, out _, out _), Is.False);
cNetMan.DispatchLocalNetMessage(new MsgStateLeavePvs
{
Tick = leaveTick,
ChunkEntities = new List<NetEntity> { chunkEntity },
});
});
await RunTicksSync(server, client, 20);
await client.WaitPost(() =>
{
Assert.That(cEntMan.TryGetEntityData(chunkEntity, out _, out var chunkMeta), Is.True);
Assert.That(chunkMeta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.Detached));
Assert.That(cEntMan.TryGetEntity(mapNet, out var cMap), Is.True);
Assert.That(cEntMan.System<ChunkEntitySystem>().TryGetChunk(cMap!.Value, Vector2i.Zero, out _), Is.False);
});
}
/// <summary>
/// Verifies that chunk entities follow normal PVS visibility: they are exposed while in range, filtered while
/// detached, and exposed again after re-entering range.
/// </summary>
[Test]
public async Task ChunkEntityDetachesAndReattachesWithPvs()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var sEntMan = server.ResolveDependency<IEntityManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var netMan = client.ResolveDependency<IClientNetManager>();
var xforms = sEntMan.System<SharedTransformSystem>();
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);
}
NetEntity player = default;
NetEntity chunkEntity = default;
NetEntity mapNet = default;
EntityUid map = default;
EntityCoordinates origin = default;
await server.WaitPost(() =>
{
// Create a player and a chunk entity on the same map chunk so the client initially receives it via PVS.
map = server.System<SharedMapSystem>().CreateMap();
origin = new EntityCoordinates(map, default);
mapNet = sEntMan.GetNetEntity(map);
var playerUid = sEntMan.SpawnEntity(null, origin);
player = sEntMan.GetNetEntity(playerUid);
var chunk = sEntMan.System<ChunkEntitySystem>().GetOrCreateChunk(map, Vector2i.Zero);
chunkEntity = sEntMan.GetNetEntity(chunk.Owner);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, playerUid);
sPlayerMan.JoinGame(session);
});
for (var i = 0; i < 20; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
MetaDataComponent? chunkMeta = null;
await client.WaitPost(() =>
{
// The chunk starts in range, so it should be registered and visible to TryGetChunk.
Assert.That(cEntMan.TryGetEntityData(chunkEntity, out _, out chunkMeta), Is.True);
Assert.That(cEntMan.TryGetEntity(mapNet, out var cMap), Is.True);
Assert.That(cEntMan.System<ChunkEntitySystem>().TryGetChunk(cMap!.Value, Vector2i.Zero, out var chunk), Is.True);
Assert.That(chunk!.Value.Owner, Is.EqualTo(cEntMan.GetEntity(chunkEntity)));
Assert.That(chunkMeta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
});
await server.WaitPost(() =>
{
// Move the player far enough away that the original map chunk leaves their PVS.
xforms.SetCoordinates(sEntMan.GetEntity(player), new EntityCoordinates(map, new Vector2(1000, 1000)));
});
for (var i = 0; i < 20; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await client.WaitPost(() =>
{
// Detached chunk entities must be filtered out so client-side chunk lookups don't expose it anymore.
Assert.That(chunkMeta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.Detached));
Assert.That(cEntMan.TryGetEntity(mapNet, out var cMap), Is.True);
Assert.That(cEntMan.System<ChunkEntitySystem>().TryGetChunk(cMap!.Value, Vector2i.Zero, out _), Is.False);
});
await server.WaitPost(() =>
{
// Moving back into range should reattach the same chunk entity and make it queryable again.
xforms.SetCoordinates(sEntMan.GetEntity(player), origin);
});
for (var i = 0; i < 20; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await client.WaitPost(() =>
{
Assert.That(chunkMeta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
Assert.That(cEntMan.TryGetEntity(mapNet, out var cMap), Is.True);
Assert.That(cEntMan.System<ChunkEntitySystem>().TryGetChunk(cMap!.Value, Vector2i.Zero, out var chunk), Is.True);
Assert.That(chunk!.Value.Owner, Is.EqualTo(cEntMan.GetEntity(chunkEntity)));
});
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}