Files
RobustToolbox/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs

237 lines
9.8 KiB
C#

using System;
using System.Collections.Generic;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using System.Linq;
using System.Threading.Tasks;
using Robust.Shared.Analyzers;
using static Robust.UnitTesting.Shared.GameState.ExampleAutogeneratedComponent;
namespace Robust.UnitTesting.Shared.GameState;
/// <summary>
/// This is a test of test engine <see cref="RobustIntegrationTest"/>. Not a test of game engine.
/// </summary>
internal sealed partial class NoSharedReferencesTest : RobustIntegrationTest
{
/// <summary>
/// The test performs a basic check to ensure that there is no issue with server's object references leaking to client.
/// It accomplishments this by testing two things: 1) That the reference object on both sides is not the same; and
/// 2) That the client-side copy of server's component state (used for prediction resetting) is not the same.
/// </summary>
[Test]
public async Task ReferencesAreNotShared()
{
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
await using var pair = await StartConnectedPair(serverOpts, clientOpts);
var server = pair.Server;
var client = pair.Client;
var clientGameStateManager = client.ResolveDependency<IClientGameStateManager>();
// Set up map.
server.Post(() =>
{
server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
EntityUid sPlayer = default;
EntityUid cPlayer = default;
ExampleObject serverObject = default!;
var sEntMan = server.EntMan;
// Set up test entity (player).
await server.WaitPost(() =>
{
sPlayer = sEntMan.Spawn();
serverObject = new ExampleObject(5);
var comp = new ExampleAutogeneratedComponent { ReferenceObject = serverObject };
sEntMan.AddComponent(sPlayer, comp);
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, sPlayer);
server.PlayerMan.JoinGame(session);
});
// Let Client sync state with Server
await RunTicks();
// Assert that Client's object and client-side server state objects are different to Server's object
Assert.Multiple(() =>
{
// Player attached assertions
var cEntMan = client.EntMan;
cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer));
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(cEntMan.EntityExists(cPlayer));
// Assert client and server have different objects of same values
Assert.That(cEntMan.TryGetComponent(cPlayer, out ExampleAutogeneratedComponent? comp));
var clientObject = comp?.ReferenceObject;
Assert.That(clientObject, Is.EqualTo(serverObject));
Assert.That(ReferenceEquals(clientObject, serverObject), Is.False);
// Assert that client-side dictionary of server component state also isn't contaminated by server references
var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)];
var clientLastTickStateObject = ((ExampleAutogeneratedComponent_AutoState)componentStates.First(x => x.Value is ExampleAutogeneratedComponent_AutoState).Value!).ReferenceObject;
Assert.That(clientLastTickStateObject, Is.Not.Null);
Assert.That(ReferenceEquals(clientLastTickStateObject, serverObject), Is.False);
});
// wait for errors.
await RunTicks();
Task RunTicks() => RunTicksSync(server, client, 10);
}
[Test]
public async Task DeltaStateCollectionReferencesAreNotShared()
{
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
await using var pair = await StartConnectedPair(serverOpts, clientOpts);
var server = pair.Server;
var client = pair.Client;
var clientGameStateManager = client.ResolveDependency<IClientGameStateManager>();
server.Post(() =>
{
server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
EntityUid sPlayer = default;
EntityUid cPlayer = default;
List<int> clientValues = default!;
DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState oldState = default!;
var sEntMan = server.EntMan;
await server.WaitPost(() =>
{
sPlayer = sEntMan.Spawn();
sEntMan.AddComponent(sPlayer, new DeltaCollectionAutogeneratedComponent());
sEntMan.AddComponent(sPlayer, new SingleDeltaCollectionAutogeneratedComponent());
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, sPlayer);
server.PlayerMan.JoinGame(session);
});
await server.WaitPost(() =>
{
var comp = sEntMan.GetComponent<DeltaCollectionAutogeneratedComponent>(sPlayer);
var state = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) sEntMan
.GetComponentState(sEntMan.EventBus, comp, null, GameTick.Zero)!;
Assert.That(state.Values, Is.EqualTo(comp.Values));
Assert.That(ReferenceEquals(state.Values, comp.Values), Is.True);
});
await RunTicks();
await client.WaitPost(() =>
{
var cEntMan = client.EntMan;
cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer));
Assert.That(cEntMan.TryGetComponent(cPlayer, out DeltaCollectionAutogeneratedComponent? comp));
var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)];
oldState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) componentStates
.First(x => x.Value is DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState)
.Value!;
Assert.That(comp!.Values, Is.EqualTo(oldState.Values));
Assert.That(ReferenceEquals(comp.Values, oldState.Values), Is.False);
clientValues = comp.Values;
var localState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) cEntMan
.GetComponentState(cEntMan.EventBus, comp, null, GameTick.Zero)!;
Assert.That(localState.Values, Is.EqualTo(comp.Values));
Assert.That(ReferenceEquals(localState.Values, comp.Values), Is.False);
Assert.That(cEntMan.TryGetComponent(cPlayer, out SingleDeltaCollectionAutogeneratedComponent? singleComp));
var singleState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) componentStates
.First(x => x.Value is SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState)
.Value!;
Assert.That(singleComp!.Values, Is.EqualTo(singleState.Values));
Assert.That(ReferenceEquals(singleComp.Values, singleState.Values), Is.False);
var singleLocalState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) cEntMan
.GetComponentState(cEntMan.EventBus, singleComp, null, GameTick.Zero)!;
Assert.That(singleLocalState.Values, Is.EqualTo(singleComp.Values));
Assert.That(ReferenceEquals(singleLocalState.Values, singleComp.Values), Is.False);
});
await client.WaitPost(() =>
{
var deltaState = new DeltaCollectionAutogeneratedComponent.Counter_FieldComponentState
{
Counter = 1
};
var newState = deltaState.CreateNewFullState(oldState);
Assert.That(newState, Is.Not.SameAs(oldState));
Assert.That(newState.Counter, Is.EqualTo(1));
Assert.That(newState.Values, Is.EqualTo(oldState.Values));
Assert.That(ReferenceEquals(newState.Values, oldState.Values), Is.False);
});
await server.WaitPost(() =>
{
var comp = sEntMan.GetComponent<DeltaCollectionAutogeneratedComponent>(sPlayer);
comp.Values.Clear();
comp.Values.Add(4);
sEntMan.Dirty(sPlayer, comp);
});
await RunTicks();
await client.WaitPost(() =>
{
var comp = client.EntMan.GetComponent<DeltaCollectionAutogeneratedComponent>(cPlayer);
Assert.That(comp.Values, Is.SameAs(clientValues));
Assert.That(comp.Values, Is.EqualTo(new[] { 4 }));
});
Task RunTicks() => RunTicksSync(server, client, 10);
}
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExampleAutogeneratedComponent : Component
{
[AutoNetworkedField]
public ExampleObject ReferenceObject;
[Serializable, NetSerializable]
public sealed record ExampleObject(int Value);
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)]
public sealed partial class DeltaCollectionAutogeneratedComponent : Component
{
[AutoNetworkedField]
public List<int> Values = new() { 1, 2, 3 };
[AutoNetworkedField]
public int Counter;
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)]
public sealed partial class SingleDeltaCollectionAutogeneratedComponent : Component
{
[AutoNetworkedField]
public List<int> Values = new() { 1, 2, 3 };
}