Files
RobustToolbox/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs
T
f2625bf5c5 Removes IMapManager (#6584)
* Move MapManager queries to SharedMapSystem
Moves all variants of FindGridsIntersecting/TryFindGridAt to SharedMapSystem
Hollows out the MapManager methods and converts them into relays to SharedMapSystem

* Move CreateGrid to SharedMapSystem
Moves the functionality for CreateGrid and its variants to SharedMapSystem
Hollows out the MapManager methods and converts them into relays for the SharedMapSystem methods
Obsoletes them too
Also moves over the GetAllMapGrids and GetAllGrids methods

* Move RaiseOnTileChanged to SharedMapSystem
Also moves the SuppressOnTileChanged flag member to SharedMapSystem
Hollows out and obsoletes the MapManager versions

* Move default value constants to SharedMapSystem

* Converts map pausing events into LocalizedEntityCommands

* Move MapManager related delegates/structs to SharedMapSystem
Moves the GridCreationOptions struct to the same namespace as SharedMapSystem and converts it into a record struct
Move the GridCallback delegates to the same namespace as SharedMapSystem

* Move CullDeletionHistory to SharedMapSystem
Well, that was less painful than I thought it would be

* Actually obsolete the NetworkedMapManager method

* Rename file

* Doc comments for new SharedMapSystem methods

* Doc comment

* Doc comments

* Fix access

* Purge all references to IMapManagerInternal

* Cull easy IMapManager references

* The rest

* Purge IMapManager

* Private internal FindGridsIntersecting method

* please die

* 41

* notes

---------

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2026-07-01 19:32:17 -07:00

172 lines
7.1 KiB
C#

using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Server.GameStates;
public sealed class PvsReEntryTest : RobustIntegrationTest
{
#if DEBUG
/// <summary>
/// Checks that there are no issues when an entity enters, leaves, then enters pvs while the client drops packets.
/// </summary>
[Test]
public async Task TestLossyReEntry()
{
await using var pair = await StartConnectedPair();
var (client, server) = pair;
var sEntMan = server.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var xforms = sEntMan.System<SharedTransformSystem>();
var stateMan = (ClientGameStateManager) client.ResolveDependency<IClientGameStateManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var cPlayerMan = client.ResolveDependency<ISharedPlayerManager>();
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
await RunTicksSync(server, client, 10);
// 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
EntityUid map = default;
NetEntity player = default;
NetEntity entity = default;
EntityCoordinates coords = default;
await server.WaitPost(() =>
{
map = server.System<SharedMapSystem>().CreateMap();
coords = new(map, default);
var playerUid = sEntMan.SpawnEntity(null, coords);
var entUid = sEntMan.SpawnEntity(null, coords);
entity = sEntMan.GetNetEntity(entUid);
player = sEntMan.GetNetEntity(playerUid);
// Attach player.
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, playerUid);
sPlayerMan.JoinGame(session);
});
await RunTicksSync(server, client, 10);
Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid));
Assert.That(entity, Is.Not.EqualTo(NetEntity.Invalid));
// Check player got properly attached, and has received the other entity.
MetaDataComponent? meta = default!;
await client.WaitPost(() =>
{
Assert.That(cEntMan.TryGetEntityData(entity, out _, out meta));
Assert.That(cEntMan.TryGetEntity(player, out var cPlayerUid));
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(cPlayerUid));
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
Assert.That(stateMan.IsQueuedForDetach(entity), Is.False);
});
var lastDirty = meta.LastModifiedTick;
Assert.That(lastDirty, Is.GreaterThan(GameTick.Zero));
// Move the player outside of the entity's PVS range
// note that we move the PLAYER not the entity, as we don't want to dirty the entity.
var farAway = new EntityCoordinates(map, new Vector2(100, 100));
await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
await RunTicksSync(server, client, 10);
// Client should have detached the entity to null space.
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.Detached));
Assert.That(stateMan.IsQueuedForDetach(entity), Is.False);
// Move the player back into range
await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
await RunTicksSync(server, client, 10);
// Entity is back in pvs range
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
Assert.That(stateMan.IsQueuedForDetach(entity), Is.False);
// Oh no, the client is going through a tunnel!
stateMan.DropStates = true;
var timing = client.ResolveDependency<IClientGameTiming>();
var lastRealTick = timing.LastRealTick;
await RunTicksSync(server, client, 5);
// Even though the client is receiving no new states, it will still have applied some from the state buffer.
Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick));
lastRealTick = timing.LastRealTick;
// Move the player outside of pvs range.
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
await RunTicksSync(server, client, 5);
// Client should have exhausted the buffer -- client has not been applying any states.
Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick));
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
// However pvs-leave messages are sent separately, and are sent reliably. So they are still being received.
// Though in a realistic scenario they should probably be delayed somewhat.
Assert.That(stateMan.IsQueuedForDetach(entity), Is.True);
// Move the entity back into range
await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
await RunTicksSync(server, client, 5);
// Still hasn't been applying states.
Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick));
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
// The pvs-leave message has not yet been processed, detaching is still queued.
Assert.That(stateMan.IsQueuedForDetach(entity), Is.True);
// Client clears the tunnel, starts receiving states again.
stateMan.DropStates = false;
await RunTicksSync(server, client, 10);
// Entity should be in PVS range, client should know about it:
Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick));
Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None));
Assert.That(stateMan.IsQueuedForDetach(entity), Is.False);
// The entity itself should not have been dirtied since creation -- only the player has been moving.
// If the test moves the entity instead of the player, then the test doesn't actually work.
Assert.That(meta.LastModifiedTick, Is.EqualTo(lastDirty));
}
#endif
}