com.unity.netcode.gameobjects@2.0.0-exp.2
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [2.0.0-exp.2] - 2024-04-02 ### Added - Added updates to all internal messages to account for a distributed authority network session connection. (#2863) - Added `NetworkRigidbodyBase` that provides users with a more customizable network rigidbody, handles both `Rigidbody` and `Rigidbody2D`, and provides an option to make `NetworkTransform` use the rigid body for motion. (#2863) - For a customized `NetworkRigidbodyBase` class: - `NetworkRigidbodyBase.AutoUpdateKinematicState` provides control on whether the kinematic setting will be automatically set or not when ownership changes. - `NetworkRigidbodyBase.AutoSetKinematicOnDespawn` provides control on whether isKinematic will automatically be set to true when the associated `NetworkObject` is despawned. - `NetworkRigidbodyBase.Initialize` is a protected method that, when invoked, will initialize the instance. This includes options to: - Set whether using a `RigidbodyTypes.Rigidbody` or `RigidbodyTypes.Rigidbody2D`. - Includes additional optional parameters to set the `NetworkTransform`, `Rigidbody`, and `Rigidbody2d` to use. - Provides additional public methods: - `NetworkRigidbodyBase.GetPosition` to return the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.GetRotation` to return the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.MovePosition` to move to the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.MoveRotation` to move to the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.Move` to move to the position and rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.Move` to move to the position and rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.SetPosition` to set the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.SetRotation` to set the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting). - `NetworkRigidbodyBase.ApplyCurrentTransform` to set the position and rotation of the `Rigidbody` or `Rigidbody2d` based on the associated `GameObject` transform (depending upon its initialized setting). - `NetworkRigidbodyBase.WakeIfSleeping` to wake up the rigid body if sleeping. - `NetworkRigidbodyBase.SleepRigidbody` to put the rigid body to sleep. - `NetworkRigidbodyBase.IsKinematic` to determine if the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) is currently kinematic. - `NetworkRigidbodyBase.SetIsKinematic` to set the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) current kinematic state. - `NetworkRigidbodyBase.ResetInterpolation` to reset the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) back to its original interpolation value when initialized. - Now includes a `MonoBehaviour.FixedUpdate` implementation that will update the assigned `NetworkTransform` when `NetworkRigidbodyBase.UseRigidBodyForMotion` is true. (#2863) - Added `RigidbodyContactEventManager` that provides a more optimized way to process collision enter and collision stay events as opposed to the `Monobehaviour` approach. (#2863) - Can be used in client-server and distributed authority modes, but is particularly useful in distributed authority. - Added rigid body motion updates to `NetworkTransform` which allows users to set interolation on rigid bodies. (#2863) - Extrapolation is only allowed on authoritative instances, but custom class derived from `NetworkRigidbodyBase` or `NetworkRigidbody` or `NetworkRigidbody2D` automatically switches non-authoritative instances to interpolation if set to extrapolation. - Added distributed authority mode support to `NetworkAnimator`. (#2863) - Added session mode selection to `NetworkManager` inspector view. (#2863) - Added distributed authority permissions feature. (#2863) - Added distributed authority mode specific `NetworkObject` permissions flags (Distributable, Transferable, and RequestRequired). (#2863) - Added distributed authority mode specific `NetworkObject.SetOwnershipStatus` method that applies one or more `NetworkObject` instance's ownership flags. If updated when spawned, the ownership permission changes are synchronized with the other connected clients. (#2863) - Added distributed authority mode specific `NetworkObject.RemoveOwnershipStatus` method that removes one or more `NetworkObject` instance's ownership flags. If updated when spawned, the ownership permission changes are synchronized with the other connected clients. (#2863) - Added distributed authority mode specific `NetworkObject.HasOwnershipStatus` method that will return (true or false) whether one or more ownership flags is set. (#2863) - Added distributed authority mode specific `NetworkObject.SetOwnershipLock` method that locks ownership of a `NetworkObject` to prevent ownership from changing until the current owner releases the lock. (#2863) - Added distributed authority mode specific `NetworkObject.RequestOwnership` method that sends an ownership request to the current owner of a spawned `NetworkObject` instance. (#2863) - Added distributed authority mode specific `NetworkObject.OnOwnershipRequested` callback handler that is invoked on the owner/authoritative side when a non-owner requests ownership. Depending upon the boolean returned value depends upon whether the request is approved or denied. (#2863) - Added distributed authority mode specific `NetworkObject.OnOwnershipRequestResponse` callback handler that is invoked when a non-owner's request has been processed. This callback includes a `NetworkObjet.OwnershipRequestResponseStatus` response parameter that describes whether the request was approved or the reason why it was not approved. (#2863) - Added distributed authority mode specific `NetworkObject.DeferDespawn` method that defers the despawning of `NetworkObject` instances on non-authoritative clients based on the tick offset parameter. (#2863) - Added distributed authority mode specific `NetworkObject.OnDeferredDespawnComplete` callback handler that can be used to further control when deferring the despawning of a `NetworkObject` on non-authoritative instances. (#2863) - Added `NetworkClient.SessionModeType` as one way to determine the current session mode of the network session a client is connected to. (#2863) - Added distributed authority mode specific `NetworkClient.IsSessionOwner` property to determine if the current local client is the current session owner of a distributed authority session. (#2863) - Added distributed authority mode specific client side spawning capabilities. When running in distributed authority mode, clients can instantiate and spawn `NetworkObject` instances (the local client is authomatically the owner of the spawned object). (#2863) - This is useful to better visually synchronize owner authoritative motion models and newly spawned `NetworkObject` instances (i.e. projectiles for example). - Added distributed authority mode specific client side player spawning capabilities. Clients will automatically spawn their associated player object locally. (#2863) - Added distributed authority mode specific `NetworkConfig.AutoSpawnPlayerPrefabClientSide` property (default is true) to provide control over the automatic spawning of player prefabs on the local client side. (#2863) - Added distributed authority mode specific `NetworkManager.OnFetchLocalPlayerPrefabToSpawn` callback that, when assigned, will allow the local client to provide the player prefab to be spawned for the local client. (#2863) - This is only invoked if the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` property is set to true. - Added distributed authority mode specific `NetworkBehaviour.HasAuthority` property that determines if the local client has authority over the associated `NetworkObject` instance (typical use case is within a `NetworkBehaviour` script much like that of `IsServer` or `IsClient`). (#2863) - Added distributed authority mode specific `NetworkBehaviour.IsSessionOwner` property that determines if the local client is the session owner (typical use case would be to determine if the local client can has scene management authority within a `NetworkBehaviour` script). (#2863) - Added support for distributed authority mode scene management where the currently assigned session owner can start scene events (i.e. scene loading and scene unloading). (#2863) ### Fixed - Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822) - Fixed issue where NetworkTransform could potentially attempt to "unregister" a named message prior to it being registered. (#2807) - Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796) ### Changed - Changed client side awareness of other clients is now the same as a server or host. (#2863) - Changed `NetworkManager.ConnectedClients` can now be accessed by both server and clients. (#2863) - Changed `NetworkManager.ConnectedClientsList` can now be accessed by both server and clients. (#2863) - Changed `NetworkTransform` defaults to owner authoritative when connected to a distributed authority session. (#2863) - Changed `NetworkVariable` defaults to owner write and everyone read permissions when connected to a distributed authority session (even if declared with server read or write permissions). (#2863) - Changed `NetworkObject` no longer implements the `MonoBehaviour.Update` method in order to determine whether a `NetworkObject` instance has been migrated to a different scene. Instead, only `NetworkObjects` with the `SceneMigrationSynchronization` property set will be updated internally during the `NetworkUpdateStage.PostLateUpdate` by `NetworkManager`. (#2863) - Changed `NetworkManager` inspector view layout where properties are now organized by category. (#2863) - Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810) - Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807)
This commit is contained in:
266
Tests/Runtime/DistributedAuthority/DeferredDespawningTests.cs
Normal file
266
Tests/Runtime/DistributedAuthority/DeferredDespawningTests.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Unity.Netcode.TestHelpers.Runtime;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
|
||||
namespace Unity.Netcode.RuntimeTests
|
||||
{
|
||||
public class DeferredDespawningTests : IntegrationTestWithApproximation
|
||||
{
|
||||
private const int k_DaisyChainedCount = 5;
|
||||
protected override int NumberOfClients => 2;
|
||||
private List<GameObject> m_DaisyChainedDespawnObjects = new List<GameObject>();
|
||||
private List<ulong> m_HasReachedEnd = new List<ulong>();
|
||||
|
||||
public DeferredDespawningTests() : base(HostOrServer.DAHost)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnServerAndClientsCreated()
|
||||
{
|
||||
var daisyChainPrevious = (DeferredDespawnDaisyChained)null;
|
||||
for (int i = 0; i < k_DaisyChainedCount; i++)
|
||||
{
|
||||
var daisyChainNode = CreateNetworkObjectPrefab($"Daisy-{i}");
|
||||
var daisyChainBehaviour = daisyChainNode.AddComponent<DeferredDespawnDaisyChained>();
|
||||
daisyChainBehaviour.IsRoot = i == 0;
|
||||
if (daisyChainPrevious != null)
|
||||
{
|
||||
daisyChainPrevious.PrefabToSpawnWhenDespawned = daisyChainBehaviour.gameObject;
|
||||
}
|
||||
m_DaisyChainedDespawnObjects.Add(daisyChainNode);
|
||||
|
||||
daisyChainPrevious = daisyChainBehaviour;
|
||||
}
|
||||
|
||||
base.OnServerAndClientsCreated();
|
||||
}
|
||||
|
||||
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DeferredDespawning()
|
||||
{
|
||||
DeferredDespawnDaisyChained.EnableVerbose = m_EnableVerboseDebug;
|
||||
var rootInstance = SpawnObject(m_DaisyChainedDespawnObjects[0], m_ServerNetworkManager);
|
||||
DeferredDespawnDaisyChained.ReachedLastChainInstance = ReachedLastChainObject;
|
||||
var timeoutHelper = new TimeoutHelper(300);
|
||||
yield return WaitForConditionOrTimeOut(HaveAllClientsReachedEndOfChain, timeoutHelper);
|
||||
AssertOnTimeout($"Timed out waiting for all children to reach the end of their chained deferred despawns!", timeoutHelper);
|
||||
}
|
||||
|
||||
private bool HaveAllClientsReachedEndOfChain()
|
||||
{
|
||||
if (!m_HasReachedEnd.Contains(m_ServerNetworkManager.LocalClientId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
if (!m_HasReachedEnd.Contains(client.LocalClientId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReachedLastChainObject(ulong clientId)
|
||||
{
|
||||
m_HasReachedEnd.Add(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This helper behaviour handles the majority of the validation for deferred despawning.
|
||||
/// Each instance triggers a series of deferred despawns where the owner validates the
|
||||
/// NetworkVariables are updated and spawns another prefab prior to despawning locally
|
||||
/// and the non-owners validate receiving the NetworkVariable change notification which
|
||||
/// contains a reference to a DeferredDespawnDaisyChained component on the newly spawned
|
||||
/// prefab driven by the authority. This repeats for the number specified in the integration
|
||||
/// test.
|
||||
/// </summary>
|
||||
public class DeferredDespawnDaisyChained : NetworkBehaviour
|
||||
{
|
||||
public static bool EnableVerbose;
|
||||
public static Action<ulong> ReachedLastChainInstance;
|
||||
private const int k_StartingDeferTick = 4;
|
||||
public static Dictionary<ulong, Dictionary<ulong, DeferredDespawnDaisyChained>> ClientRelativeInstances = new Dictionary<ulong, Dictionary<ulong, DeferredDespawnDaisyChained>>();
|
||||
public bool IsRoot;
|
||||
public GameObject PrefabToSpawnWhenDespawned;
|
||||
public bool WasContactedByPeviousChainMember { get; private set; }
|
||||
public int DeferDespawnTick { get; private set; }
|
||||
|
||||
private void PingInstance()
|
||||
{
|
||||
WasContactedByPeviousChainMember = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This hits two birds with one NetworkVariable:
|
||||
/// - Validates that NetworkVariables modified while the authority is in the middle of deferring a despawn are serialized and received by non-authority instances.
|
||||
/// - Validates that the non-authority instances receive the updates within the deferred tick period of time and can use them to handle other visual synchronization
|
||||
/// realted tasks (or the like).
|
||||
/// </summary>
|
||||
private NetworkVariable<NetworkBehaviourReference> m_ValidateDirtyNetworkVarUpdate = new NetworkVariable<NetworkBehaviourReference>();
|
||||
|
||||
private DeferredDespawnDaisyChained m_NextNodeSpawned = null;
|
||||
|
||||
private void FailTest(string msg)
|
||||
{
|
||||
Assert.Fail($"[{nameof(DeferredDespawnDaisyChained)}][Client-{NetworkManager.LocalClientId}] {msg}");
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
var localId = NetworkManager.LocalClientId;
|
||||
if (!ClientRelativeInstances.ContainsKey(localId))
|
||||
{
|
||||
ClientRelativeInstances.Add(localId, new Dictionary<ulong, DeferredDespawnDaisyChained>());
|
||||
}
|
||||
|
||||
if (ClientRelativeInstances[localId].ContainsKey(NetworkObject.NetworkObjectId))
|
||||
{
|
||||
FailTest($"[{nameof(OnNetworkSpawn)}] Client already has a table entry for NetworkObject-{NetworkObject.NetworkObjectId} | {name}!");
|
||||
}
|
||||
|
||||
ClientRelativeInstances[localId].Add(NetworkObject.NetworkObjectId, this);
|
||||
|
||||
if (!HasAuthority)
|
||||
{
|
||||
m_ValidateDirtyNetworkVarUpdate.OnValueChanged += OnValidateDirtyChanged;
|
||||
}
|
||||
|
||||
if (HasAuthority && IsRoot)
|
||||
{
|
||||
DeferDespawnTick = k_StartingDeferTick;
|
||||
}
|
||||
|
||||
base.OnNetworkSpawn();
|
||||
}
|
||||
|
||||
private void OnValidateDirtyChanged(NetworkBehaviourReference previous, NetworkBehaviourReference current)
|
||||
{
|
||||
if (!HasAuthority)
|
||||
{
|
||||
if (!current.TryGet(out m_NextNodeSpawned, NetworkManager))
|
||||
{
|
||||
FailTest($"[{nameof(OnValidateDirtyChanged)}][{nameof(NetworkBehaviourReference)}] Failed to get the {nameof(DeferredDespawnDaisyChained)} behaviour from the {nameof(NetworkBehaviourReference)}!");
|
||||
}
|
||||
|
||||
if (m_NextNodeSpawned.NetworkManager != NetworkManager)
|
||||
{
|
||||
FailTest($"[{nameof(NetworkManager)}][{nameof(NetworkBehaviourReference.TryGet)}] The {nameof(NetworkManager)} of {nameof(m_NextNodeSpawned)} does not match the local relative {nameof(NetworkManager)} instance!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (!HasAuthority && !NetworkManager.ShutdownInProgress)
|
||||
{
|
||||
if (PrefabToSpawnWhenDespawned != null)
|
||||
{
|
||||
m_NextNodeSpawned.PingInstance();
|
||||
}
|
||||
else
|
||||
{
|
||||
ReachedLastChainInstance?.Invoke(NetworkManager.LocalClientId);
|
||||
}
|
||||
}
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
|
||||
private void InvokeDespawn()
|
||||
{
|
||||
if (!HasAuthority)
|
||||
{
|
||||
FailTest($"[{nameof(InvokeDespawn)}] Client is not the authority but this was invoked (integration test logic issue)!");
|
||||
}
|
||||
NetworkObject.DeferDespawn(DeferDespawnTick);
|
||||
}
|
||||
|
||||
public override void OnDeferringDespawn(int despawnTick)
|
||||
{
|
||||
if (!HasAuthority)
|
||||
{
|
||||
FailTest($"[{nameof(OnDeferringDespawn)}] Client is not the authority but this was invoked (integration test logic issue)!");
|
||||
}
|
||||
|
||||
if (despawnTick != (DeferDespawnTick + NetworkManager.ServerTime.Tick))
|
||||
{
|
||||
FailTest($"[{nameof(OnDeferringDespawn)}] The passed in {despawnTick} parameter ({despawnTick}) does not equal the expected value of ({DeferDespawnTick + NetworkManager.ServerTime.Tick})!");
|
||||
}
|
||||
|
||||
if (PrefabToSpawnWhenDespawned != null)
|
||||
{
|
||||
var deferNetworkObject = PrefabToSpawnWhenDespawned.GetComponent<NetworkObject>().InstantiateAndSpawn(NetworkManager);
|
||||
var deferComponent = deferNetworkObject.GetComponent<DeferredDespawnDaisyChained>();
|
||||
// Slowly increment the despawn tick count as we process the chain of deferred despawns
|
||||
deferComponent.DeferDespawnTick = DeferDespawnTick + 1;
|
||||
// This should get updated on all non-authority instances before they despawn
|
||||
m_ValidateDirtyNetworkVarUpdate.Value = new NetworkBehaviourReference(deferComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReachedLastChainInstance?.Invoke(NetworkManager.LocalClientId);
|
||||
}
|
||||
base.OnDeferringDespawn(despawnTick);
|
||||
}
|
||||
|
||||
private bool m_DeferredDespawn;
|
||||
private void Update()
|
||||
{
|
||||
if (!IsSpawned || !HasAuthority || m_DeferredDespawn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until all clients have this instance
|
||||
foreach (var clientId in NetworkManager.ConnectedClientsIds)
|
||||
{
|
||||
if (!ClientRelativeInstances.ContainsKey(clientId))
|
||||
{
|
||||
// exit early if the client doesn't exist yet
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ClientRelativeInstances[clientId].ContainsKey(NetworkObjectId))
|
||||
{
|
||||
// exit early if the client hasn't spawned a clone of this instance yet
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientId == NetworkManager.LocalClientId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// This should happen shortly afte the instances spawns (based on the deferred despawn count)
|
||||
if (!IsRoot && !ClientRelativeInstances[clientId][NetworkObjectId].WasContactedByPeviousChainMember)
|
||||
{
|
||||
// exit early if the non-authority instance has not been contacted yet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it here, then defer despawn this instance
|
||||
InvokeDespawn();
|
||||
m_DeferredDespawn = true;
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
if (!EnableVerbose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[{name}][Client-{NetworkManager.LocalClientId}][{NetworkObjectId}] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7b700919b058f446a75a398d5be9af4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
520
Tests/Runtime/DistributedAuthority/DistributeObjectsTests.cs
Normal file
520
Tests/Runtime/DistributedAuthority/DistributeObjectsTests.cs
Normal file
@@ -0,0 +1,520 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using Unity.Netcode.Components;
|
||||
using Unity.Netcode.TestHelpers.Runtime;
|
||||
using Unity.Netcode.Transports.UTP;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Unity.Netcode.RuntimeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that distributable NetworkObjects are distributed upon
|
||||
/// a client connecting or disconnecting.
|
||||
/// </summary>
|
||||
public class DistributeObjectsTests : IntegrationTestWithApproximation
|
||||
{
|
||||
private GameObject m_DistributeObject;
|
||||
|
||||
private StringBuilder m_ErrorLog = new StringBuilder();
|
||||
|
||||
private const int k_LateJoinClientCount = 4;
|
||||
protected override int NumberOfClients => 0;
|
||||
|
||||
public DistributeObjectsTests() : base(HostOrServer.DAHost)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerator OnSetup()
|
||||
{
|
||||
m_ObjectToValidate = null;
|
||||
return base.OnSetup();
|
||||
}
|
||||
|
||||
protected override void OnServerAndClientsCreated()
|
||||
{
|
||||
var serverTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as UnityTransport;
|
||||
// I hate having to add time to our tests, but in case a VM is running slow the disconnect timeout needs to be reasonably high
|
||||
serverTransport.DisconnectTimeoutMS = 1000;
|
||||
m_DistributeObject = CreateNetworkObjectPrefab("DisObject");
|
||||
m_DistributeObject.AddComponent<DistributeObjectsTestHelper>();
|
||||
m_DistributeObject.AddComponent<DistributeTestTransform>();
|
||||
|
||||
// Set baseline to be distributable
|
||||
var networkObject = m_DistributeObject.GetComponent<NetworkObject>();
|
||||
networkObject.SetOwnershipStatus(NetworkObject.OwnershipStatus.Distributable);
|
||||
networkObject.DontDestroyWithOwner = true;
|
||||
base.OnServerAndClientsCreated();
|
||||
}
|
||||
|
||||
protected override IEnumerator OnServerAndClientsConnected()
|
||||
{
|
||||
m_ServerNetworkManager.SpawnManager.EnableDistributeLogging = m_EnableVerboseDebug;
|
||||
m_ServerNetworkManager.ConnectionManager.EnableDistributeLogging = m_EnableVerboseDebug;
|
||||
return base.OnServerAndClientsConnected();
|
||||
}
|
||||
|
||||
private NetworkObject m_ObjectToValidate;
|
||||
|
||||
private bool ValidateObjectSpawnedOnAllClients()
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
|
||||
var networkObjectId = m_ObjectToValidate.NetworkObjectId;
|
||||
var name = m_ObjectToValidate.name;
|
||||
if (!UseCMBService() && !m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{m_ServerNetworkManager.LocalClientId} has not spawned {name}!");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
if (!client.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{client.LocalClientId} has not spawned {name}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private const int k_ObjectCount = 20;
|
||||
|
||||
private bool ValidateDistributedObjectsSpawned(bool lateJoining)
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
var hostId = m_ServerNetworkManager.LocalClientId;
|
||||
if (!DistributeObjectsTestHelper.DistributedObjects.ContainsKey(hostId))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{hostId}] Does not have an entry in the root of the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
var daHostObjectTracking = DistributeObjectsTestHelper.DistributedObjects[hostId];
|
||||
if (!daHostObjectTracking.ContainsKey(hostId))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{hostId}] Does not have a local an entry in the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var daHostObjects = daHostObjectTracking[hostId];
|
||||
var expected = 0;
|
||||
if (lateJoining)
|
||||
{
|
||||
expected = k_ObjectCount / (m_ClientNetworkManagers.Count() + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
expected = k_ObjectCount / (m_ClientNetworkManagers.Where((c) => c.IsConnectedClient).Count() + 1);
|
||||
}
|
||||
|
||||
// It should theoretically be the expected or...
|
||||
if (daHostObjects.Count != expected)
|
||||
{
|
||||
// due to not rounding one more than the expected
|
||||
expected++;
|
||||
if (daHostObjects.Count != expected)
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{hostId}][General] Expected {expected} spawned objects, but only {daHostObjects.Count} exist!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var networkObject in daHostObjects)
|
||||
{
|
||||
m_ObjectToValidate = networkObject.Value;
|
||||
if (!ValidateObjectSpawnedOnAllClients())
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[{m_ObjectToValidate.name}] Was not spawned on all clients!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateOwnershipTablesMatch()
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
var hostId = m_ServerNetworkManager.LocalClientId;
|
||||
var expectedEntries = m_ClientNetworkManagers.Where((c) => c.IsListening && c.IsConnectedClient).Count() + 1;
|
||||
// Make sure all clients have an table created
|
||||
if (DistributeObjectsTestHelper.DistributedObjects.Count < expectedEntries)
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[General] Expected {expectedEntries} entries in the root of the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table but only {DistributeObjectsTestHelper.DistributedObjects.Count} exist!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DistributeObjectsTestHelper.DistributedObjects.ContainsKey(hostId))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{hostId}] Does not have an entry in the root of the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
var daHostEntries = DistributeObjectsTestHelper.DistributedObjects[hostId];
|
||||
if (!daHostEntries.ContainsKey(hostId))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{hostId}] Does not have a local an entry in the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
var clients = m_ServerNetworkManager.ConnectedClientsIds.ToList();
|
||||
clients.Remove(0);
|
||||
|
||||
// Cycle through each client's entry on the DAHost to run a comparison
|
||||
foreach (var hostClientEntry in daHostEntries)
|
||||
{
|
||||
foreach (var ownerEntry in hostClientEntry.Value)
|
||||
{
|
||||
foreach (var client in clients)
|
||||
{
|
||||
var clientOwnerTable = DistributeObjectsTestHelper.DistributedObjects[client];
|
||||
if (!clientOwnerTable.ContainsKey(hostClientEntry.Key))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{client}] No ownership table exists the client relative section of the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
var clientEntry = clientOwnerTable[hostClientEntry.Key];
|
||||
if (!clientEntry.ContainsKey(ownerEntry.Key))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{client}] {ownerEntry.Value.name} does not exists in Client-{client}'s sub-section for Owner-{hostClientEntry.Key} relative section of the {nameof(DistributeObjectsTestHelper.DistributedObjects)} table!");
|
||||
return false;
|
||||
}
|
||||
var clientObjectEntry = clientEntry[ownerEntry.Key];
|
||||
if (clientObjectEntry.OwnerClientId != ownerEntry.Value.OwnerClientId)
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{client}][Owner Mismatch] {clientObjectEntry.OwnerClientId} does equal {ownerEntry.Value.OwnerClientId}!");
|
||||
return false;
|
||||
}
|
||||
// Assure the observers match
|
||||
foreach (var observer in ownerEntry.Value.Observers)
|
||||
{
|
||||
if (!clientObjectEntry.Observers.Contains(observer))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{client}][Observer Mismatch] {nameof(NetworkObject)} {clientObjectEntry.name}'s observers does not contain {observer}, but the authority instance does!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateTransformsMatch()
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
var hostId = m_ServerNetworkManager.LocalClientId;
|
||||
var daHostEntries = DistributeObjectsTestHelper.DistributedObjects[hostId];
|
||||
var clients = m_ServerNetworkManager.ConnectedClientsIds.ToList();
|
||||
foreach (var clientOwner in daHostEntries.Keys)
|
||||
{
|
||||
// Cycle through the owner's objects
|
||||
foreach (var entry in DistributeObjectsTestHelper.DistributedObjects[clientOwner][clientOwner].Values)
|
||||
{
|
||||
var ownerTestTransform = entry.GetComponent<DistributeTestTransform>();
|
||||
// Compare against the other client instances of that object
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (client == clientOwner)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var clientObjectInstance = DistributeObjectsTestHelper.DistributedObjects[client][clientOwner][entry.NetworkObjectId];
|
||||
if (!ownerTestTransform.IsPositionClose(clientObjectInstance.transform.position))
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Position Mismatch] Client-{client} Instance: {GetVector3Values(clientObjectInstance.transform.position)} != Owner Instance: {GetVector3Values(ownerTestTransform.transform.position)}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnNewClientCreated(NetworkManager networkManager)
|
||||
{
|
||||
networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs;
|
||||
base.OnNewClientCreated(networkManager);
|
||||
}
|
||||
|
||||
private bool SpawnCountsMatch()
|
||||
{
|
||||
var passed = true;
|
||||
var spawnCount = 0;
|
||||
m_ErrorLog.Clear();
|
||||
if (!UseCMBService())
|
||||
{
|
||||
spawnCount = m_ServerNetworkManager.SpawnManager.SpawnedObjects.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnCount = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects.Count;
|
||||
}
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
var clientCount = client.SpawnManager.SpawnedObjects.Count;
|
||||
if (clientCount != spawnCount)
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{client.LocalClientId}] Has a spawn count of {clientCount} but {spawnCount} was expected!");
|
||||
passed = false;
|
||||
}
|
||||
}
|
||||
return passed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a straight forward validation for the distribution of NetworkObjects
|
||||
/// upon a client connecting or disconnecting. It also validates that the observers
|
||||
/// on each non-authority instance matches the authority instance's. Finally, it
|
||||
/// also includes validation that NetworkTransform updates continue to update and
|
||||
/// synchronize properly after ownership for a set number of objects has changed.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator DistributeNetworkObjects()
|
||||
{
|
||||
for (int i = 0; i < k_ObjectCount; i++)
|
||||
{
|
||||
SpawnObject(m_DistributeObject, m_ServerNetworkManager);
|
||||
}
|
||||
|
||||
// Validate NetworkObjects get redistributed properly when a client joins
|
||||
for (int j = 0; j < k_LateJoinClientCount; j++)
|
||||
{
|
||||
yield return CreateAndStartNewClient();
|
||||
|
||||
yield return WaitForConditionOrTimeOut(() => ValidateDistributedObjectsSpawned(true));
|
||||
AssertOnTimeout($"[Client-{j + 1}][Initial Spawn] Not all clients spawned all objects!\n {m_ErrorLog}");
|
||||
|
||||
yield return WaitForConditionOrTimeOut(ValidateOwnershipTablesMatch);
|
||||
AssertOnTimeout($"[Client-{j + 1}][OnwershipTable Mismatch] {m_ErrorLog}");
|
||||
|
||||
// When ownership changes, the new owner will randomly pick a new target to move towards and will move towards the target.
|
||||
// Validate all other instances of the NetworkObjects that have had newly assigned owners have matching positions to the
|
||||
// newly assigned owenr's instance.
|
||||
yield return WaitForConditionOrTimeOut(ValidateTransformsMatch);
|
||||
AssertOnTimeout($"[Client-{j + 1}][Transform Mismatch] {m_ErrorLog}");
|
||||
DisplayOwnership();
|
||||
|
||||
yield return WaitForConditionOrTimeOut(SpawnCountsMatch);
|
||||
AssertOnTimeout($"[Spawn Count Mismatch] {m_ErrorLog}");
|
||||
}
|
||||
|
||||
// Validate NetworkObjects get redistributed properly when a client disconnects
|
||||
for (int j = k_LateJoinClientCount - 1; j >= 0; j--)
|
||||
{
|
||||
var client = m_ClientNetworkManagers[j];
|
||||
|
||||
// Remove the client from the other clients' ownership tracking table
|
||||
DistributeObjectsTestHelper.RemoveClient(client.LocalClientId);
|
||||
|
||||
// Disconnect the client
|
||||
yield return StopOneClient(client, true);
|
||||
|
||||
//yield return new WaitForSeconds(0.1f);
|
||||
|
||||
// Validate all tables match
|
||||
yield return WaitForConditionOrTimeOut(ValidateOwnershipTablesMatch);
|
||||
|
||||
AssertOnTimeout($"[Client-{j + 1}][OnwershipTable Mismatch] {m_ErrorLog}");
|
||||
|
||||
// When ownership changes, the new owner will randomly pick a new target to move towards and will move towards the target.
|
||||
// Validate all other instances of the NetworkObjects that have had newly assigned owners have matching positions to the
|
||||
// newly assigned owenr's instance.
|
||||
yield return WaitForConditionOrTimeOut(ValidateTransformsMatch);
|
||||
AssertOnTimeout($"[Client-{j + 1}][Transform Mismatch] {m_ErrorLog}");
|
||||
|
||||
// DANGO-TODO: Make this tied to verbose mode once we know the CMB Service integration works properly
|
||||
DisplayOwnership();
|
||||
|
||||
yield return WaitForConditionOrTimeOut(SpawnCountsMatch);
|
||||
AssertOnTimeout($"[Spawn Count Mismatch] {m_ErrorLog}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplayOwnership()
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
var daHostEntries = DistributeObjectsTestHelper.DistributedObjects[0];
|
||||
|
||||
foreach (var entry in daHostEntries)
|
||||
{
|
||||
m_ErrorLog.AppendLine($"[Client-{entry.Key}][Owned Objects: {entry.Value.Count}]");
|
||||
}
|
||||
|
||||
VerboseDebug($"{m_ErrorLog}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This keeps track of each clients perspective of which NetworkObjects are owned by which client.
|
||||
/// It is used to validate that all clients are in synch with ownership updates.
|
||||
/// </summary>
|
||||
public class DistributeObjectsTestHelper : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// [Client Context][Client Owners][NetworkObjectId][NetworkObject]
|
||||
/// </summary>
|
||||
public static Dictionary<ulong, Dictionary<ulong, Dictionary<ulong, NetworkObject>>> DistributedObjects = new Dictionary<ulong, Dictionary<ulong, Dictionary<ulong, NetworkObject>>>();
|
||||
|
||||
public static void RemoveClient(ulong clientId)
|
||||
{
|
||||
foreach (var clients in DistributedObjects.Values)
|
||||
{
|
||||
clients.Remove(clientId);
|
||||
}
|
||||
DistributedObjects.Remove(clientId);
|
||||
}
|
||||
|
||||
internal ulong ClientId;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
ClientId = NetworkManager.LocalClientId;
|
||||
UpdateOwnerTableAdd();
|
||||
base.OnNetworkSpawn();
|
||||
}
|
||||
|
||||
private void UpdateOwnerTableAdd()
|
||||
{
|
||||
if (!DistributedObjects.ContainsKey(ClientId))
|
||||
{
|
||||
DistributedObjects.Add(ClientId, new Dictionary<ulong, Dictionary<ulong, NetworkObject>>());
|
||||
}
|
||||
if (!DistributedObjects[ClientId].ContainsKey(OwnerClientId))
|
||||
{
|
||||
DistributedObjects[ClientId].Add(OwnerClientId, new Dictionary<ulong, NetworkObject>());
|
||||
}
|
||||
|
||||
if (DistributedObjects[ClientId][OwnerClientId].ContainsKey(NetworkObject.NetworkObjectId))
|
||||
{
|
||||
throw new Exception($"[Client-{ClientId}][{name}] {nameof(NetworkObject)} already exists in Client-{ClientId}'s " +
|
||||
$"DistributedObjects being tracking under Client-{OwnerClientId}'s list of owned {nameof(NetworkObject)}s!");
|
||||
}
|
||||
DistributedObjects[ClientId][OwnerClientId].Add(NetworkObject.NetworkObjectId, NetworkObject);
|
||||
}
|
||||
|
||||
private void UpdateOwnerTableRemove(ulong previous)
|
||||
{
|
||||
// This does not need to exist when first starting, but will (at one point in testing)
|
||||
// become valid.
|
||||
if (DistributedObjects[ClientId].ContainsKey(previous))
|
||||
{
|
||||
if (DistributedObjects[ClientId][previous].ContainsKey(NetworkObject.NetworkObjectId))
|
||||
{
|
||||
DistributedObjects[ClientId][previous].Remove(NetworkObject.NetworkObjectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnOwnershipChanged(ulong previous, ulong current)
|
||||
{
|
||||
// At start, if NetworkSpawn has not been completed the local client ignores this
|
||||
if (!DistributedObjects.ContainsKey(ClientId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
UpdateOwnerTableRemove(previous);
|
||||
UpdateOwnerTableAdd();
|
||||
base.OnOwnershipChanged(previous, current);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is used to validate that upon distributed ownership changes NetworkTransform sycnhronization
|
||||
/// still works properly.
|
||||
/// </summary>
|
||||
public class DistributeTestTransform : NetworkTransform
|
||||
{
|
||||
private float m_DeltaVarPosition = 0.15f;
|
||||
private float m_DeltaVarQauternion = 0.015f;
|
||||
protected Vector3 GetRandomVector3(float min, float max, Vector3 baseLine, bool randomlyApplySign = false)
|
||||
{
|
||||
var retValue = new Vector3(baseLine.x * Random.Range(min, max), baseLine.y * Random.Range(min, max), baseLine.z * Random.Range(min, max));
|
||||
if (!randomlyApplySign)
|
||||
{
|
||||
return retValue;
|
||||
}
|
||||
|
||||
retValue.x *= Random.Range(1, 100) >= 50 ? -1 : 1;
|
||||
retValue.y *= Random.Range(1, 100) >= 50 ? -1 : 1;
|
||||
retValue.z *= Random.Range(1, 100) >= 50 ? -1 : 1;
|
||||
return retValue;
|
||||
}
|
||||
|
||||
protected override bool OnIsServerAuthoritative()
|
||||
{
|
||||
var isOwnerAuth = base.OnIsServerAuthoritative();
|
||||
Assert.IsFalse(isOwnerAuth, $"Base {nameof(NetworkTransform)} did not automatically return false in distributed authority mode!");
|
||||
return isOwnerAuth;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
if (CanCommitToTransform)
|
||||
{
|
||||
var randomPos = GetRandomVector3(1.0f, 10.0f, Vector3.one, true);
|
||||
SetState(randomPos, null, null, false);
|
||||
m_TargetPosition = randomPos;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 m_TargetPosition;
|
||||
private Vector3 m_DirToTarget;
|
||||
private bool m_ReachedTarget;
|
||||
|
||||
protected override void OnOwnershipChanged(ulong previous, ulong current)
|
||||
{
|
||||
base.OnOwnershipChanged(previous, current);
|
||||
m_TargetPosition = transform.position + GetRandomVector3(4.0f, 8.0f, Vector3.one, true);
|
||||
m_DirToTarget = (m_TargetPosition - transform.position).normalized;
|
||||
m_ReachedTarget = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (CanCommitToTransform)
|
||||
{
|
||||
if (!m_ReachedTarget)
|
||||
{
|
||||
var distance = Vector3.Distance(transform.position, m_TargetPosition);
|
||||
|
||||
var speed = Mathf.Clamp(distance, 0.10f, 2.0f);
|
||||
|
||||
transform.position += m_DirToTarget * speed * Time.deltaTime;
|
||||
|
||||
m_ReachedTarget = IsPositionClose(m_TargetPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPositionClose(Vector3 position)
|
||||
{
|
||||
return Approximately(transform.position, position);
|
||||
}
|
||||
|
||||
protected bool Approximately(Vector3 a, Vector3 b)
|
||||
{
|
||||
var deltaVariance = m_DeltaVarPosition;
|
||||
return Math.Round(Mathf.Abs(a.x - b.x), 2) <= deltaVariance &&
|
||||
Math.Round(Mathf.Abs(a.y - b.y), 2) <= deltaVariance &&
|
||||
Math.Round(Mathf.Abs(a.z - b.z), 2) <= deltaVariance;
|
||||
}
|
||||
|
||||
protected bool Approximately(Quaternion a, Quaternion b)
|
||||
{
|
||||
var deltaVariance = m_DeltaVarQauternion;
|
||||
return Mathf.Abs(a.x - b.x) <= deltaVariance &&
|
||||
Mathf.Abs(a.y - b.y) <= deltaVariance &&
|
||||
Mathf.Abs(a.z - b.z) <= deltaVariance &&
|
||||
Mathf.Abs(a.w - b.w) <= deltaVariance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a759aeb53d12d842899381b411f3d2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,718 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NUnit.Framework;
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode.TestHelpers.Runtime;
|
||||
using Unity.Netcode.Transports.UTP;
|
||||
#if UTP_TRANSPORT_2_0_ABOVE
|
||||
using Unity.Networking.Transport;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Netcode.RuntimeTests
|
||||
{
|
||||
public class DistributedAuthorityCodecTests : NetcodeIntegrationTest
|
||||
{
|
||||
protected override int NumberOfClients => 1;
|
||||
|
||||
// Use the CMB Service for all tests
|
||||
protected override bool UseCMBService() => true;
|
||||
|
||||
// Set the session mode to distributed authority for all tests
|
||||
protected override SessionModeTypes OnGetSessionmode() => SessionModeTypes.DistributedAuthority;
|
||||
|
||||
private CodecTestHooks m_ClientCodecHook;
|
||||
private NetworkManager Client => m_ClientNetworkManagers[0];
|
||||
|
||||
private string m_TransportHost = Environment.GetEnvironmentVariable("NGO_HOST") ?? "127.0.0.1";
|
||||
private const int k_TransportPort = 7777;
|
||||
private const int k_ClientId = 0;
|
||||
|
||||
private GameObject m_SpawnObject;
|
||||
|
||||
public class TestNetworkComponent : NetworkBehaviour
|
||||
{
|
||||
public NetworkList<int> MyNetworkList = new NetworkList<int>(new List<int> { 1, 2, 3 });
|
||||
|
||||
[Rpc(SendTo.NotAuthority)]
|
||||
public void TestNotAuthorityRpc(byte[] _)
|
||||
{
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Authority)]
|
||||
public void TestAuthorityRpc(byte[] _)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnOneTimeSetup()
|
||||
{
|
||||
// Prevents the tests from running if no CMB Service is detected
|
||||
#if !UTP_TRANSPORT_2_0_ABOVE
|
||||
Assert.Ignore("ignoring DA codec tests because UTP transport must be 2.0");
|
||||
#else
|
||||
if (!CanConnectToServer(m_TransportHost, k_TransportPort))
|
||||
{
|
||||
Assert.Ignore("ignoring DA codec tests because UTP transport cannot connect to the runtime");
|
||||
}
|
||||
#endif
|
||||
base.OnOneTimeSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add any additional components to default player prefab
|
||||
/// </summary>
|
||||
protected override void OnCreatePlayerPrefab()
|
||||
{
|
||||
m_PlayerPrefab.AddComponent<TestNetworkComponent>();
|
||||
base.OnCreatePlayerPrefab();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify NetworkManager instances for settings specific to tests
|
||||
/// </summary>
|
||||
protected override void OnServerAndClientsCreated()
|
||||
{
|
||||
var utpTransport = Client.gameObject.AddComponent<UnityTransport>();
|
||||
Client.NetworkConfig.NetworkTransport = utpTransport;
|
||||
Client.NetworkConfig.EnableSceneManagement = false;
|
||||
Client.NetworkConfig.AutoSpawnPlayerPrefabClientSide = true;
|
||||
utpTransport.ConnectionData.Address = Dns.GetHostAddresses(m_TransportHost).First().ToString();
|
||||
utpTransport.ConnectionData.Port = k_TransportPort;
|
||||
Client.LogLevel = LogLevel.Developer;
|
||||
|
||||
// Validate we are in distributed authority mode with client side spawning and using CMB Service
|
||||
Assert.True(Client.DistributedAuthorityMode, "Distributed authority is not set!");
|
||||
Assert.True(Client.AutoSpawnPlayerPrefabClientSide, "Client side spawning is not set!");
|
||||
Assert.True(Client.CMBServiceConnection, "CMBServiceConnection is not set!");
|
||||
|
||||
// Create a prefab for creating and destroying tests (auto-registers with NetworkManagers)
|
||||
m_SpawnObject = CreateNetworkObjectPrefab("TestObject");
|
||||
m_SpawnObject.AddComponent<TestNetworkComponent>();
|
||||
|
||||
// Ignore the client connection timeout after starting the client
|
||||
m_BypassConnectionTimeout = true;
|
||||
}
|
||||
|
||||
protected override IEnumerator OnStartedServerAndClients()
|
||||
{
|
||||
// Register hooks after starting clients and server (in this case just the one client)
|
||||
// We do this at this point in time because the MessageManager exists (happens within the same call stack when starting NetworkManagers)
|
||||
m_ClientCodecHook = new CodecTestHooks();
|
||||
Client.MessageManager.Hook(m_ClientCodecHook);
|
||||
yield return base.OnStartedServerAndClients();
|
||||
|
||||
// wait for client to connect since m_BypassConnectionTimeout
|
||||
yield return WaitForConditionOrTimeOut(() => Client.LocalClient.PlayerObject != null);
|
||||
AssertOnTimeout($"Timed out waiting for the client's player to be spanwed!");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator AuthorityRpc()
|
||||
{
|
||||
var player = Client.LocalClient.PlayerObject;
|
||||
player.OwnerClientId = Client.LocalClientId + 1;
|
||||
|
||||
var networkComponent = player.GetComponent<TestNetworkComponent>();
|
||||
networkComponent.UpdateNetworkProperties();
|
||||
networkComponent.TestAuthorityRpc(new byte[] { 1, 2, 3, 4 });
|
||||
|
||||
// Universal Rpcs are sent as a ProxyMessage (which contains an RpcMessage)
|
||||
yield return m_ClientCodecHook.WaitForMessageReceived<ProxyMessage>();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ChangeOwnership()
|
||||
{
|
||||
var message = new ChangeOwnershipMessage
|
||||
{
|
||||
DistributedAuthorityMode = true,
|
||||
NetworkObjectId = 100,
|
||||
OwnerClientId = 2,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ClientConnected()
|
||||
{
|
||||
var message = new ClientConnectedMessage()
|
||||
{
|
||||
ClientId = 2,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ClientDisconnected()
|
||||
{
|
||||
var message = new ClientDisconnectedMessage()
|
||||
{
|
||||
ClientId = 2,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CreateObject()
|
||||
{
|
||||
SpawnObject(m_SpawnObject, Client);
|
||||
yield return m_ClientCodecHook.WaitForMessageReceived<CreateObjectMessage>();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DestroyObject()
|
||||
{
|
||||
var spawnedObject = SpawnObject(m_SpawnObject, Client);
|
||||
yield return m_ClientCodecHook.WaitForMessageReceived<CreateObjectMessage>();
|
||||
spawnedObject.GetComponent<NetworkObject>().Despawn();
|
||||
yield return m_ClientCodecHook.WaitForMessageReceived<DestroyObjectMessage>();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Disconnect()
|
||||
{
|
||||
var message = new DisconnectReasonMessage
|
||||
{
|
||||
Reason = "test"
|
||||
};
|
||||
|
||||
return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NamedMessage()
|
||||
{
|
||||
var writeBuffer = new FastBufferWriter(sizeof(int), Allocator.Temp);
|
||||
writeBuffer.WriteValueSafe(5);
|
||||
|
||||
var message = new NamedMessage
|
||||
{
|
||||
Hash = 3,
|
||||
SendData = writeBuffer,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NetworkVariableDelta()
|
||||
{
|
||||
var message = new NetworkVariableDeltaMessage
|
||||
{
|
||||
NetworkObjectId = 0,
|
||||
NetworkBehaviourIndex = 1,
|
||||
DeliveryMappedNetworkVariableIndex = new HashSet<int> { 2, 3, 4 },
|
||||
TargetClientId = 5,
|
||||
NetworkBehaviour = Client.LocalClient.PlayerObject.GetComponent<TestNetworkComponent>(),
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NotAuthorityRpc()
|
||||
{
|
||||
Client.LocalClient.PlayerObject.GetComponent<TestNetworkComponent>().TestNotAuthorityRpc(new byte[] { 1, 2, 3, 4 });
|
||||
|
||||
// Universal Rpcs are sent as a ProxyMessage (which contains an RpcMessage)
|
||||
yield return m_ClientCodecHook.WaitForMessageReceived<ProxyMessage>();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ParentSync()
|
||||
{
|
||||
var message = new ParentSyncMessage
|
||||
{
|
||||
NetworkObjectId = 0,
|
||||
WorldPositionStays = true,
|
||||
IsLatestParentSet = false,
|
||||
Position = new Vector3(1, 2, 3),
|
||||
Rotation = new Quaternion(4, 5, 6, 7),
|
||||
Scale = new Vector3(8, 9, 10),
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SessionOwner()
|
||||
{
|
||||
var message = new SessionOwnerMessage()
|
||||
{
|
||||
SessionOwner = 2,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ServerLog()
|
||||
{
|
||||
var message = new ServerLogMessage()
|
||||
{
|
||||
LogType = NetworkLog.LogType.Info,
|
||||
Message = "test",
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator UnnamedMessage()
|
||||
{
|
||||
var writeBuffer = new FastBufferWriter(sizeof(int), Allocator.Temp);
|
||||
writeBuffer.WriteValueSafe(5);
|
||||
|
||||
var message = new UnnamedMessage
|
||||
{
|
||||
SendData = writeBuffer,
|
||||
};
|
||||
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageLoad()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.Load,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageLoadWithObjects()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var prefabNetworkObject = m_SpawnObject.GetComponent<NetworkObject>();
|
||||
|
||||
Client.SceneManager.ScenePlacedObjects.Add(0, new Dictionary<int, NetworkObject>()
|
||||
{
|
||||
{ 1, prefabNetworkObject }
|
||||
});
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.Load,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageUnload()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.Unload,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageLoadComplete()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.LoadComplete,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageUnloadComplete()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.UnloadComplete,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageLoadCompleted()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.LoadEventCompleted,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
ClientsCompleted = new List<ulong>() { k_ClientId },
|
||||
ClientsTimedOut = new List<ulong>() { 123456789 },
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageUnloadLoadCompleted()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.UnloadEventCompleted,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
SceneEventProgressId = Guid.NewGuid(),
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
ClientsCompleted = new List<ulong>() { k_ClientId },
|
||||
ClientsTimedOut = new List<ulong>() { 123456789 },
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageSynchronize()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.Synchronize,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
ClientSynchronizationMode = LoadSceneMode.Single,
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
ScenesToSynchronize = new Queue<uint>()
|
||||
};
|
||||
eventData.ScenesToSynchronize.Enqueue(101);
|
||||
eventData.SceneHandlesToSynchronize = new Queue<uint>();
|
||||
eventData.SceneHandlesToSynchronize.Enqueue(202);
|
||||
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageReSynchronize()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.ReSynchronize,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
ClientSynchronizationMode = LoadSceneMode.Single,
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageSynchronizeComplete()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.ReSynchronize,
|
||||
LoadSceneMode = LoadSceneMode.Single,
|
||||
ClientSynchronizationMode = LoadSceneMode.Single,
|
||||
SceneHash = XXHash.Hash32("SomeRandomSceneName"),
|
||||
SceneHandle = 23456,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SceneEventMessageActiveSceneChanged()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.ActiveSceneChanged,
|
||||
ActiveSceneHash = XXHash.Hash32("ActiveScene")
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
[UnityTest, Ignore("Serializing twice causes data to disappear in the SceneManager for this event")]
|
||||
public IEnumerator SceneEventMessageObjectSceneChanged()
|
||||
{
|
||||
Client.SceneManager.SkipSceneHandling = true;
|
||||
var prefabNetworkObject = m_SpawnObject.GetComponent<NetworkObject>();
|
||||
Client.SceneManager.ObjectsMigratedIntoNewScene = new Dictionary<int, Dictionary<ulong, List<NetworkObject>>>
|
||||
{
|
||||
{ 0, new Dictionary<ulong, List<NetworkObject>>()}
|
||||
};
|
||||
|
||||
Client.SceneManager.ObjectsMigratedIntoNewScene[0].Add(Client.LocalClientId, new List<NetworkObject>() { prefabNetworkObject });
|
||||
var eventData = new SceneEventData(Client)
|
||||
{
|
||||
SceneEventType = SceneEventType.ObjectSceneChanged,
|
||||
};
|
||||
|
||||
var message = new SceneEventMessage()
|
||||
{
|
||||
EventData = eventData
|
||||
};
|
||||
yield return SendMessage(ref message);
|
||||
}
|
||||
|
||||
|
||||
private IEnumerator SendMessage<T>(ref T message) where T : INetworkMessage
|
||||
{
|
||||
Client.MessageManager.SetVersion(k_ClientId, XXHash.Hash32(typeof(T).FullName), 0);
|
||||
|
||||
var clientIds = new NativeArray<ulong>(1, Allocator.Temp);
|
||||
clientIds[0] = k_ClientId;
|
||||
Client.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientIds);
|
||||
Client.MessageManager.ProcessSendQueues();
|
||||
return m_ClientCodecHook.WaitForMessageReceived(message);
|
||||
}
|
||||
|
||||
#if UTP_TRANSPORT_2_0_ABOVE
|
||||
private static bool CanConnectToServer(string host, ushort port, double timeoutMs = 100)
|
||||
{
|
||||
var address = Dns.GetHostAddresses(host).First();
|
||||
var endpoint = NetworkEndpoint.Parse(address.ToString(), port);
|
||||
|
||||
var driver = NetworkDriver.Create();
|
||||
var connection = driver.Connect(endpoint);
|
||||
|
||||
var start = DateTime.Now;
|
||||
var ev = Networking.Transport.NetworkEvent.Type.Empty;
|
||||
while (ev != Networking.Transport.NetworkEvent.Type.Connect)
|
||||
{
|
||||
driver.ScheduleUpdate().Complete();
|
||||
ev = driver.PopEventForConnection(connection, out _, out _);
|
||||
|
||||
if (DateTime.Now - start > TimeSpan.FromMilliseconds(timeoutMs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
driver.Disconnect(connection);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal class CodecTestHooks : INetworkHooks
|
||||
{
|
||||
private Dictionary<string, Queue<TestMessage>> m_ExpectedMessages = new Dictionary<string, Queue<TestMessage>>();
|
||||
private Dictionary<string, HashSet<string>> m_ReceivedMessages = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
private struct TestMessage
|
||||
{
|
||||
public string Name;
|
||||
public byte[] Data;
|
||||
}
|
||||
|
||||
public void OnBeforeSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage
|
||||
{
|
||||
if (message is ConnectionRequestMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var writer = new FastBufferWriter(1024, Allocator.Temp);
|
||||
message.Serialize(writer, 0);
|
||||
|
||||
var testName = TestContext.CurrentContext.Test.Name;
|
||||
if (!m_ExpectedMessages.ContainsKey(testName))
|
||||
{
|
||||
m_ExpectedMessages[testName] = new Queue<TestMessage>();
|
||||
}
|
||||
|
||||
m_ExpectedMessages[testName].Enqueue(new TestMessage
|
||||
{
|
||||
Name = typeof(T).ToString(),
|
||||
Data = writer.ToArray(),
|
||||
});
|
||||
|
||||
writer.Dispose();
|
||||
}
|
||||
|
||||
public void OnAfterSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage
|
||||
{
|
||||
}
|
||||
|
||||
public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context)
|
||||
{
|
||||
if (messageType == typeof(ConnectionApprovedMessage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var testName = TestContext.CurrentContext.Test.Name;
|
||||
Assert.True(m_ExpectedMessages.ContainsKey(testName));
|
||||
Assert.IsNotEmpty(m_ExpectedMessages[testName]);
|
||||
|
||||
var nextMessage = m_ExpectedMessages[testName].Dequeue();
|
||||
Assert.AreEqual(messageType.ToString(), nextMessage.Name, $"received unexpected message type: {messageType}");
|
||||
|
||||
if (!m_ReceivedMessages.ContainsKey(testName))
|
||||
{
|
||||
m_ReceivedMessages[testName] = new HashSet<string>();
|
||||
}
|
||||
|
||||
m_ReceivedMessages[testName].Add(messageType.ToString());
|
||||
|
||||
// ServerLogMessage is an exception - it gets decoded correctly, but the bytes from the runtime do not directly match those sent by the SDK.
|
||||
if (messageType == typeof(ServerLogMessage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var expectedBytes = nextMessage.Data;
|
||||
var receivedBytes = messageContent.ToArray();
|
||||
Assert.AreEqual(expectedBytes, receivedBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnBeforeHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
|
||||
{
|
||||
}
|
||||
|
||||
public void OnAfterHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerator WaitForMessageReceived<T>(float timeout = 5) where T : INetworkMessage
|
||||
{
|
||||
var testName = TestContext.CurrentContext.Test.Name;
|
||||
var messageType = typeof(T).FullName;
|
||||
var startTime = Time.realtimeSinceStartup;
|
||||
|
||||
while ((!m_ReceivedMessages.ContainsKey(testName) || !m_ReceivedMessages[testName].Contains(messageType)) && Time.realtimeSinceStartup - startTime < timeout)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Assert.True(m_ReceivedMessages.ContainsKey(testName), "failed to receive any messages");
|
||||
Assert.True(m_ReceivedMessages[testName].Contains(messageType), $"failed to receive {messageType} message, received: {string.Join(", ", m_ReceivedMessages[testName])}");
|
||||
|
||||
// Reset received messages
|
||||
m_ReceivedMessages[testName] = new HashSet<string>();
|
||||
}
|
||||
|
||||
public IEnumerator WaitForMessageReceived<T>(T _, float timeout = 5) where T : INetworkMessage
|
||||
{
|
||||
return WaitForMessageReceived<T>(timeout: timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cec6e6e1b9bdb746806290355d8cb50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,298 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using Unity.Netcode.TestHelpers.Runtime;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
|
||||
namespace Unity.Netcode.RuntimeTests
|
||||
{
|
||||
[TestFixture(HostOrServer.Host)]
|
||||
[TestFixture(HostOrServer.Server)]
|
||||
[TestFixture(HostOrServer.DAHost)]
|
||||
public class NetworkClientAndPlayerObjectTests : NetcodeIntegrationTest
|
||||
{
|
||||
private const int k_PlayerPrefabCount = 6;
|
||||
protected override int NumberOfClients => 2;
|
||||
|
||||
private List<GameObject> m_PlayerPrefabs = new List<GameObject>();
|
||||
private Dictionary<ulong, uint> m_ChangedPlayerPrefabs = new Dictionary<ulong, uint>();
|
||||
|
||||
|
||||
public NetworkClientAndPlayerObjectTests(HostOrServer hostOrServer) : base(hostOrServer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerator OnTearDown()
|
||||
{
|
||||
m_PlayerPrefabs.Clear();
|
||||
return base.OnTearDown();
|
||||
}
|
||||
|
||||
protected override void OnServerAndClientsCreated()
|
||||
{
|
||||
m_PlayerPrefabs.Clear();
|
||||
for (int i = 0; i < k_PlayerPrefabCount; i++)
|
||||
{
|
||||
m_PlayerPrefabs.Add(CreateNetworkObjectPrefab($"PlayerPrefab{i}"));
|
||||
}
|
||||
base.OnServerAndClientsCreated();
|
||||
}
|
||||
|
||||
protected override void OnNewClientCreated(NetworkManager networkManager)
|
||||
{
|
||||
networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs;
|
||||
if (m_DistributedAuthority)
|
||||
{
|
||||
networkManager.OnFetchLocalPlayerPrefabToSpawn = FetchPlayerPrefabToSpawn;
|
||||
}
|
||||
base.OnNewClientCreated(networkManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only for distributed authority mode
|
||||
/// </summary>
|
||||
/// <returns>a unique player prefab for the player</returns>
|
||||
private GameObject FetchPlayerPrefabToSpawn()
|
||||
{
|
||||
var prefabObject = GetRandomPlayerPrefab();
|
||||
var clientId = m_ClientNetworkManagers[m_ClientNetworkManagers.Length - 1].LocalClientId;
|
||||
m_ChangedPlayerPrefabs.Add(clientId, prefabObject.GlobalObjectIdHash);
|
||||
return prefabObject.gameObject;
|
||||
}
|
||||
|
||||
|
||||
private StringBuilder m_ErrorLogLevel3 = new StringBuilder();
|
||||
private StringBuilder m_ErrorLogLevel2 = new StringBuilder();
|
||||
private StringBuilder m_ErrorLogLevel1 = new StringBuilder();
|
||||
|
||||
private bool ValidateNetworkClient(NetworkClient networkClient)
|
||||
{
|
||||
m_ErrorLogLevel3.Clear();
|
||||
var success = true;
|
||||
if (networkClient == null)
|
||||
{
|
||||
m_ErrorLogLevel3.Append($"[NetworkClient is NULL]");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
if (!networkClient.IsConnected)
|
||||
{
|
||||
m_ErrorLogLevel3.Append($"[NetworkClient {nameof(NetworkClient.IsConnected)}] is false]");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
if (networkClient.PlayerObject == null)
|
||||
{
|
||||
m_ErrorLogLevel3.Append($"[NetworkClient {nameof(NetworkClient.PlayerObject)}] is NULL]");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool ValidateNetworkManagerNetworkClients(NetworkManager networkManager)
|
||||
{
|
||||
var success = true;
|
||||
m_ErrorLogLevel2.Clear();
|
||||
|
||||
// Number of connected clients plus the DAHost
|
||||
var expectedCount = m_ClientNetworkManagers.Length + (m_UseHost ? 1 : 0);
|
||||
|
||||
if (networkManager.ConnectedClients.Count != expectedCount)
|
||||
{
|
||||
m_ErrorLogLevel2.Append($"[{nameof(NetworkManager.ConnectedClients)} count: {networkManager.ConnectedClients.Count} vs expected count: {expectedCount}]");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (m_UseHost && !ValidateNetworkClient(networkManager.LocalClient))
|
||||
{
|
||||
m_ErrorLogLevel2.Append($"[Local NetworkClient: --({m_ErrorLogLevel3})--]");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
|
||||
foreach (var networkClient in networkManager.ConnectedClients)
|
||||
{
|
||||
// When just running a server, ignore the server's local NetworkClient
|
||||
if (!m_UseHost && networkManager.IsServer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ValidateNetworkClient(networkManager.LocalClient))
|
||||
{
|
||||
// Log error
|
||||
success = false;
|
||||
m_ErrorLogLevel2.Append($"[NetworkClient-{networkManager.LocalClientId}: --({m_ErrorLogLevel3})--]");
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool AllNetworkClientsValidated()
|
||||
{
|
||||
m_ErrorLogLevel1.Clear();
|
||||
var success = true;
|
||||
|
||||
if (!UseCMBService())
|
||||
{
|
||||
if (!ValidateNetworkManagerNetworkClients(m_ServerNetworkManager))
|
||||
{
|
||||
m_ErrorLogLevel1.AppendLine($"[Client-{m_ServerNetworkManager.LocalClientId}]{m_ErrorLogLevel2}");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var clientNetworkManager in m_ClientNetworkManagers)
|
||||
{
|
||||
if (!ValidateNetworkManagerNetworkClients(clientNetworkManager))
|
||||
{
|
||||
m_ErrorLogLevel1.AppendLine($"[Client-{clientNetworkManager.LocalClientId}]{m_ErrorLogLevel2}");
|
||||
// Log error
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all NetworkManager instances have valid NetworkClients for all connected clients
|
||||
/// Validates the same thing when a client late joins and when a client disconnects.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator ValidateNetworkClients()
|
||||
{
|
||||
// Validate the initial clients created
|
||||
yield return WaitForConditionOrTimeOut(AllNetworkClientsValidated);
|
||||
AssertOnTimeout($"[Start] Not all NetworkClients were valid!\n{m_ErrorLogLevel1}");
|
||||
|
||||
// Late join a player and revalidate all instances
|
||||
yield return CreateAndStartNewClient();
|
||||
yield return WaitForConditionOrTimeOut(AllNetworkClientsValidated);
|
||||
AssertOnTimeout($"[Late Join] Not all NetworkClients were valid!\n{m_ErrorLogLevel1}");
|
||||
|
||||
// Disconnect a player and revalidate all instances
|
||||
var initialCount = m_ClientNetworkManagers.Length;
|
||||
yield return StopOneClient(m_ClientNetworkManagers[m_ClientNetworkManagers.Length - 1], true);
|
||||
// Sanity check to assure we removed the NetworkManager from m_ClientNetworkManagers
|
||||
Assert.False(initialCount == m_ClientNetworkManagers.Length, $"Disconnected player and expected total number of client {nameof(NetworkManager)}s " +
|
||||
$"to be {initialCount - 1} but it was still {initialCount}!");
|
||||
|
||||
yield return WaitForConditionOrTimeOut(AllNetworkClientsValidated);
|
||||
AssertOnTimeout($"[Client Disconnect] Not all NetworkClients were valid!\n{m_ErrorLogLevel1}");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that all NetworkClients are pointing to the correct player object, even if
|
||||
/// the player object is changed.
|
||||
/// </summary>
|
||||
private bool ValidatePlayerObjectOnClients(NetworkManager clientToValidate)
|
||||
{
|
||||
m_ErrorLogLevel2.Clear();
|
||||
var success = true;
|
||||
var expectedGlobalObjectIdHash = m_ChangedPlayerPrefabs[clientToValidate.LocalClientId];
|
||||
if (expectedGlobalObjectIdHash != clientToValidate.LocalClient.PlayerObject.GlobalObjectIdHash)
|
||||
{
|
||||
m_ErrorLogLevel2.Append($"[Local Prefab Mismatch][Expected GlobalObjectIdHash: {expectedGlobalObjectIdHash} but was {clientToValidate.LocalClient.PlayerObject.GlobalObjectIdHash}]");
|
||||
success = false;
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
if (client == clientToValidate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var remoteNetworkClient = client.ConnectedClients[clientToValidate.LocalClientId];
|
||||
if (expectedGlobalObjectIdHash != remoteNetworkClient.PlayerObject.GlobalObjectIdHash)
|
||||
{
|
||||
m_ErrorLogLevel2.Append($"[Client-{client.LocalClientId} Prefab Mismatch][Expected GlobalObjectIdHash: {expectedGlobalObjectIdHash} but was {remoteNetworkClient.PlayerObject.GlobalObjectIdHash}]");
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool ValidateAllPlayerObjects()
|
||||
{
|
||||
m_ErrorLogLevel1.Clear();
|
||||
var success = true;
|
||||
if (m_UseHost && !UseCMBService())
|
||||
{
|
||||
if (!ValidatePlayerObjectOnClients(m_ServerNetworkManager))
|
||||
{
|
||||
m_ErrorLogLevel1.AppendLine($"[Client-{m_ServerNetworkManager.LocalClientId}]{m_ErrorLogLevel2}");
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
if (!ValidatePlayerObjectOnClients(client))
|
||||
{
|
||||
m_ErrorLogLevel1.AppendLine($"[Client-{client.LocalClientId}]{m_ErrorLogLevel2}");
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private NetworkObject GetRandomPlayerPrefab()
|
||||
{
|
||||
return m_PlayerPrefabs[Random.Range(0, m_PlayerPrefabs.Count() - 1)].GetComponent<NetworkObject>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that when a client changes their player object that all connected client instances mirror the
|
||||
/// client's new player object.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator ValidatePlayerObjects()
|
||||
{
|
||||
// Just do a quick validation for all connected client's NetworkClients
|
||||
yield return WaitForConditionOrTimeOut(AllNetworkClientsValidated);
|
||||
AssertOnTimeout($"Not all NetworkClients were valid!\n{m_ErrorLogLevel1}");
|
||||
|
||||
// Now, have each client spawn a new player object
|
||||
m_ChangedPlayerPrefabs.Clear();
|
||||
var playerInstance = (GameObject)null;
|
||||
var playerPrefabToSpawn = (NetworkObject)null;
|
||||
if (m_UseHost)
|
||||
{
|
||||
playerPrefabToSpawn = GetRandomPlayerPrefab();
|
||||
playerInstance = SpawnPlayerObject(playerPrefabToSpawn.gameObject, m_ServerNetworkManager);
|
||||
m_ChangedPlayerPrefabs.Add(m_ServerNetworkManager.LocalClientId, playerPrefabToSpawn.GlobalObjectIdHash);
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
playerPrefabToSpawn = GetRandomPlayerPrefab();
|
||||
playerInstance = SpawnPlayerObject(playerPrefabToSpawn.gameObject, client);
|
||||
m_ChangedPlayerPrefabs.Add(client.LocalClientId, playerPrefabToSpawn.GlobalObjectIdHash);
|
||||
}
|
||||
|
||||
// Validate that all connected clients' NetworkClient instances have the correct player object for each connected client
|
||||
yield return WaitForConditionOrTimeOut(ValidateAllPlayerObjects);
|
||||
AssertOnTimeout($"[Existing Clients] Not all NetworkClient player objects were valid!\n{m_ErrorLogLevel1}");
|
||||
|
||||
// Distributed authority only feature validation (NetworkManager.OnFetchLocalPlayerPrefabToSpawn)
|
||||
if (m_DistributedAuthority)
|
||||
{
|
||||
// Now test the fetch prefab callback to assure that this is working correctly.
|
||||
// Start a new client and wait for it to connect
|
||||
yield return CreateAndStartNewClient();
|
||||
// Do another validation pass.
|
||||
yield return WaitForConditionOrTimeOut(ValidateAllPlayerObjects);
|
||||
AssertOnTimeout($"[Late Joined Client] Not all NetworkClient player objects were valid!\n{m_ErrorLogLevel1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db4c955c05fdc194eb47e7774b9c5101
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
406
Tests/Runtime/DistributedAuthority/OwnershipPermissionsTests.cs
Normal file
406
Tests/Runtime/DistributedAuthority/OwnershipPermissionsTests.cs
Normal file
@@ -0,0 +1,406 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using Unity.Netcode.TestHelpers.Runtime;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Netcode.RuntimeTests
|
||||
{
|
||||
public class OwnershipPermissionsTests : IntegrationTestWithApproximation
|
||||
{
|
||||
private GameObject m_PermissionsObject;
|
||||
|
||||
private StringBuilder m_ErrorLog = new StringBuilder();
|
||||
|
||||
protected override int NumberOfClients => 4;
|
||||
|
||||
public OwnershipPermissionsTests() : base(HostOrServer.DAHost)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerator OnSetup()
|
||||
{
|
||||
m_ObjectToValidate = null;
|
||||
OwnershipPermissionsTestHelper.CurrentOwnedInstance = null;
|
||||
return base.OnSetup();
|
||||
}
|
||||
|
||||
protected override void OnServerAndClientsCreated()
|
||||
{
|
||||
m_PermissionsObject = CreateNetworkObjectPrefab("PermObject");
|
||||
m_PermissionsObject.AddComponent<OwnershipPermissionsTestHelper>();
|
||||
|
||||
base.OnServerAndClientsCreated();
|
||||
}
|
||||
|
||||
private NetworkObject m_ObjectToValidate;
|
||||
|
||||
private bool ValidateObjectSpawnedOnAllClients()
|
||||
{
|
||||
m_ErrorLog.Clear();
|
||||
|
||||
var networkObjectId = m_ObjectToValidate.NetworkObjectId;
|
||||
var name = m_ObjectToValidate.name;
|
||||
if (!UseCMBService() && !m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{m_ServerNetworkManager.LocalClientId} has not spawned {name}!");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
if (!client.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{client.LocalClientId} has not spawned {name}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidatePermissionsOnAllClients()
|
||||
{
|
||||
var currentPermissions = (ushort)m_ObjectToValidate.Ownership;
|
||||
var otherPermissions = (ushort)0;
|
||||
var networkObjectId = m_ObjectToValidate.NetworkObjectId;
|
||||
var objectName = m_ObjectToValidate.name;
|
||||
m_ErrorLog.Clear();
|
||||
if (!UseCMBService())
|
||||
{
|
||||
otherPermissions = (ushort)m_ServerNetworkManager.SpawnManager.SpawnedObjects[networkObjectId].Ownership;
|
||||
if (currentPermissions != otherPermissions)
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{m_ServerNetworkManager.LocalClientId} permissions for {objectName} is {otherPermissions} when it should be {currentPermissions}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
otherPermissions = (ushort)client.SpawnManager.SpawnedObjects[networkObjectId].Ownership;
|
||||
if (currentPermissions != otherPermissions)
|
||||
{
|
||||
m_ErrorLog.Append($"Client-{client.LocalClientId} permissions for {objectName} is {otherPermissions} when it should be {currentPermissions}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateAllInstancesAreOwnedByClient(ulong clientId)
|
||||
{
|
||||
var networkObjectId = m_ObjectToValidate.NetworkObjectId;
|
||||
var otherNetworkObject = (NetworkObject)null;
|
||||
m_ErrorLog.Clear();
|
||||
if (!UseCMBService())
|
||||
{
|
||||
otherNetworkObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[networkObjectId];
|
||||
if (otherNetworkObject.OwnerClientId != clientId)
|
||||
{
|
||||
m_ErrorLog.Append($"[Client-{m_ServerNetworkManager.LocalClientId}][{otherNetworkObject.name}] Expected owner to be {clientId} but it was {otherNetworkObject.OwnerClientId}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var client in m_ClientNetworkManagers)
|
||||
{
|
||||
otherNetworkObject = client.SpawnManager.SpawnedObjects[networkObjectId];
|
||||
if (otherNetworkObject.OwnerClientId != clientId)
|
||||
{
|
||||
m_ErrorLog.Append($"[Client-{client.LocalClientId}][{otherNetworkObject.name}] Expected owner to be {clientId} but it was {otherNetworkObject.OwnerClientId}!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ValidateOwnershipPermissionsTest()
|
||||
{
|
||||
var firstInstance = SpawnObject(m_PermissionsObject, m_ClientNetworkManagers[0]).GetComponent<NetworkObject>();
|
||||
OwnershipPermissionsTestHelper.CurrentOwnedInstance = firstInstance;
|
||||
var firstInstanceHelper = firstInstance.GetComponent<OwnershipPermissionsTestHelper>();
|
||||
var networkObjectId = firstInstance.NetworkObjectId;
|
||||
m_ObjectToValidate = OwnershipPermissionsTestHelper.CurrentOwnedInstance;
|
||||
yield return WaitForConditionOrTimeOut(ValidateObjectSpawnedOnAllClients);
|
||||
AssertOnTimeout($"[Failed To Spawn] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Validate the base non-assigned persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
//////////////////////////////////////
|
||||
// Setting & Removing Ownership Flags:
|
||||
//////////////////////////////////////
|
||||
|
||||
// Now, cycle through all permissions and validate that when the owner changes them the change
|
||||
// is synchronized on all non-owner clients.
|
||||
foreach (var permissionObject in Enum.GetValues(typeof(NetworkObject.OwnershipStatus)))
|
||||
{
|
||||
var permission = (NetworkObject.OwnershipStatus)permissionObject;
|
||||
// Add the status
|
||||
firstInstance.SetOwnershipStatus(permission);
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Add][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Remove the status unless it is None (ignore None).
|
||||
if (permission == NetworkObject.OwnershipStatus.None)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
firstInstance.RemoveOwnershipStatus(permission);
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Remove][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
}
|
||||
|
||||
//Add multiple flags at the same time
|
||||
var multipleFlags = NetworkObject.OwnershipStatus.Transferable | NetworkObject.OwnershipStatus.Distributable | NetworkObject.OwnershipStatus.RequestRequired;
|
||||
firstInstance.SetOwnershipStatus(multipleFlags, true);
|
||||
Assert.IsTrue(firstInstance.HasOwnershipStatus(multipleFlags), $"[Set][Multi-flag Failure] Expected: {(ushort)multipleFlags} but was {(ushort)firstInstance.Ownership}!");
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Set Multiple][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Remove multiple flags at the same time
|
||||
multipleFlags = NetworkObject.OwnershipStatus.Transferable | NetworkObject.OwnershipStatus.RequestRequired;
|
||||
firstInstance.RemoveOwnershipStatus(multipleFlags);
|
||||
// Validate the two flags no longer are set
|
||||
Assert.IsFalse(firstInstance.HasOwnershipStatus(multipleFlags), $"[Remove][Multi-flag Failure] Expected: {(ushort)NetworkObject.OwnershipStatus.Distributable} but was {(ushort)firstInstance.Ownership}!");
|
||||
// Validate that the Distributable flag is still set
|
||||
Assert.IsTrue(firstInstance.HasOwnershipStatus(NetworkObject.OwnershipStatus.Distributable), $"[Remove][Multi-flag Failure] Expected: {(ushort)NetworkObject.OwnershipStatus.Distributable} but was {(ushort)firstInstance.Ownership}!");
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Set Multiple][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
//////////////////////
|
||||
// Changing Ownership:
|
||||
//////////////////////
|
||||
|
||||
// Clear the flags, set the permissions to transferrable, and lock ownership in one pass.
|
||||
firstInstance.SetOwnershipStatus(NetworkObject.OwnershipStatus.Transferable, true, NetworkObject.OwnershipLockActions.SetAndLock);
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Reset][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
var secondInstance = m_ClientNetworkManagers[1].SpawnManager.SpawnedObjects[networkObjectId];
|
||||
var secondInstanceHelper = secondInstance.GetComponent<OwnershipPermissionsTestHelper>();
|
||||
|
||||
secondInstance.ChangeOwnership(m_ClientNetworkManagers[1].LocalClientId);
|
||||
Assert.IsTrue(secondInstanceHelper.OwnershipPermissionsFailureStatus == NetworkObject.OwnershipPermissionsFailureStatus.Locked,
|
||||
$"Expected {secondInstance.name} to return {NetworkObject.OwnershipPermissionsFailureStatus.Locked} but its permission failure" +
|
||||
$" status is {secondInstanceHelper.OwnershipPermissionsFailureStatus}!");
|
||||
|
||||
firstInstance.SetOwnershipLock(false);
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Unlock][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Sanity check to assure this client's instance isn't already the owner.
|
||||
Assert.True(!secondInstance.IsOwner, $"[Ownership Check] Client-{m_ClientNetworkManagers[1].LocalClientId} already is the owner!");
|
||||
// Now try to acquire ownership
|
||||
secondInstance.ChangeOwnership(m_ClientNetworkManagers[1].LocalClientId);
|
||||
|
||||
// Validate the persmissions value for all instances are the same
|
||||
yield return WaitForConditionOrTimeOut(() => secondInstance.IsOwner);
|
||||
AssertOnTimeout($"[Acquire Ownership Failed] Client-{m_ClientNetworkManagers[1].LocalClientId} failed to get ownership!");
|
||||
|
||||
m_ObjectToValidate = OwnershipPermissionsTestHelper.CurrentOwnedInstance;
|
||||
// Validate all other client instances are showing the same owner
|
||||
yield return WaitForConditionOrTimeOut(() => ValidateAllInstancesAreOwnedByClient(m_ClientNetworkManagers[1].LocalClientId));
|
||||
AssertOnTimeout($"[Ownership Mismatch] {secondInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Clear the flags, set the permissions to RequestRequired, and lock ownership in one pass.
|
||||
secondInstance.SetOwnershipStatus(NetworkObject.OwnershipStatus.RequestRequired, true);
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Unlock][Permissions Mismatch] {secondInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Attempt to acquire ownership by just changing it
|
||||
firstInstance.ChangeOwnership(firstInstance.NetworkManager.LocalClientId);
|
||||
|
||||
// Assure we are denied ownership due to it requiring ownership be requested
|
||||
Assert.IsTrue(firstInstanceHelper.OwnershipPermissionsFailureStatus == NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired,
|
||||
$"Expected {secondInstance.name} to return {NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired} but its permission failure" +
|
||||
$" status is {secondInstanceHelper.OwnershipPermissionsFailureStatus}!");
|
||||
|
||||
//////////////////////////////////
|
||||
// Test for single race condition:
|
||||
//////////////////////////////////
|
||||
|
||||
// Start with a request for the client we expect to be given ownership
|
||||
var requestStatus = firstInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{firstInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
|
||||
// Get the 3rd client to send a request at the "relatively" same time
|
||||
var thirdInstance = m_ClientNetworkManagers[2].SpawnManager.SpawnedObjects[networkObjectId];
|
||||
var thirdInstanceHelper = thirdInstance.GetComponent<OwnershipPermissionsTestHelper>();
|
||||
|
||||
// At the same time send a request by the third client.
|
||||
requestStatus = thirdInstance.RequestOwnership();
|
||||
|
||||
// We expect the 3rd client's request should be able to be sent at this time as well (i.e. creates the race condition between two clients)
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{m_ServerNetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
|
||||
// We expect the first requesting client to be given ownership
|
||||
yield return WaitForConditionOrTimeOut(() => firstInstance.IsOwner);
|
||||
AssertOnTimeout($"[Acquire Ownership Failed] Client-{firstInstance.NetworkManager.LocalClientId} failed to get ownership! ({firstInstanceHelper.OwnershipRequestResponseStatus})(Owner: {OwnershipPermissionsTestHelper.CurrentOwnedInstance.OwnerClientId}");
|
||||
m_ObjectToValidate = OwnershipPermissionsTestHelper.CurrentOwnedInstance;
|
||||
|
||||
// Just do a sanity check to assure ownership has changed on all clients.
|
||||
yield return WaitForConditionOrTimeOut(() => ValidateAllInstancesAreOwnedByClient(firstInstance.NetworkManager.LocalClientId));
|
||||
AssertOnTimeout($"[Ownership Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Now, the third client should get a RequestInProgress returned as their request response
|
||||
yield return WaitForConditionOrTimeOut(() => thirdInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.RequestInProgress);
|
||||
AssertOnTimeout($"[Request In Progress Failed] Client-{thirdInstanceHelper.NetworkManager.LocalClientId} did not get the right request denied reponse!");
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Unlock][Permissions Mismatch] {firstInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// Test for multiple ownership race conditions:
|
||||
///////////////////////////////////////////////
|
||||
|
||||
// Get the 4th client's instance
|
||||
var fourthInstance = m_ClientNetworkManagers[3].SpawnManager.SpawnedObjects[networkObjectId];
|
||||
var fourthInstanceHelper = fourthInstance.GetComponent<OwnershipPermissionsTestHelper>();
|
||||
|
||||
// Send out a request from three clients at the same time
|
||||
// The first one sent (and received for this test) gets ownership
|
||||
requestStatus = secondInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{secondInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
requestStatus = thirdInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{thirdInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
requestStatus = fourthInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{fourthInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
|
||||
// The 2nd and 3rd client should be denied and the 4th client should be approved
|
||||
yield return WaitForConditionOrTimeOut(() =>
|
||||
(fourthInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.RequestInProgress) &&
|
||||
(thirdInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.RequestInProgress) &&
|
||||
(secondInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.Approved)
|
||||
);
|
||||
AssertOnTimeout($"[Targeted Owner] Client-{secondInstanceHelper.NetworkManager.LocalClientId} did not get the right request denied reponse: {secondInstanceHelper.OwnershipRequestResponseStatus}!");
|
||||
m_ObjectToValidate = OwnershipPermissionsTestHelper.CurrentOwnedInstance;
|
||||
// Just do a sanity check to assure ownership has changed on all clients.
|
||||
yield return WaitForConditionOrTimeOut(() => ValidateAllInstancesAreOwnedByClient(secondInstance.NetworkManager.LocalClientId));
|
||||
AssertOnTimeout($"[Ownership Mismatch] {secondInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
// Validate the persmissions value for all instances are the same.
|
||||
yield return WaitForConditionOrTimeOut(ValidatePermissionsOnAllClients);
|
||||
AssertOnTimeout($"[Unlock][Permissions Mismatch] {secondInstance.name}: \n {m_ErrorLog}");
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// Test for targeted ownership request:
|
||||
///////////////////////////////////////////////
|
||||
|
||||
// Now get the DAHost's client's instance
|
||||
var daHostInstance = m_ServerNetworkManager.SpawnManager.SpawnedObjects[networkObjectId];
|
||||
var daHostInstanceHelper = daHostInstance.GetComponent<OwnershipPermissionsTestHelper>();
|
||||
|
||||
secondInstanceHelper.AllowOwnershipRequest = true;
|
||||
secondInstanceHelper.OnlyAllowTargetClientId = true;
|
||||
secondInstanceHelper.ClientToAllowOwnership = daHostInstance.NetworkManager.LocalClientId;
|
||||
|
||||
// Send out a request from all three clients
|
||||
requestStatus = firstInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{firstInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
requestStatus = thirdInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{thirdInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
requestStatus = fourthInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{fourthInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
requestStatus = daHostInstance.RequestOwnership();
|
||||
Assert.True(requestStatus == NetworkObject.OwnershipRequestStatus.RequestSent, $"Client-{daHostInstance.NetworkManager.LocalClientId} was unabled to send a request for ownership because: {requestStatus}!");
|
||||
|
||||
// The server and the 2nd client should be denied and the third client should be approved
|
||||
yield return WaitForConditionOrTimeOut(() =>
|
||||
(firstInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.Denied) &&
|
||||
(thirdInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.Denied) &&
|
||||
(fourthInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.Denied) &&
|
||||
(daHostInstanceHelper.OwnershipRequestResponseStatus == NetworkObject.OwnershipRequestResponseStatus.Approved)
|
||||
);
|
||||
AssertOnTimeout($"[Targeted Owner] Client-{daHostInstance.NetworkManager.LocalClientId} did not get the right request reponse: {daHostInstanceHelper.OwnershipRequestResponseStatus} Expecting: {NetworkObject.OwnershipRequestResponseStatus.Approved}!");
|
||||
}
|
||||
|
||||
public class OwnershipPermissionsTestHelper : NetworkBehaviour
|
||||
{
|
||||
public static NetworkObject CurrentOwnedInstance;
|
||||
|
||||
public static Dictionary<ulong, Dictionary<ulong, List<NetworkObject>>> DistributedObjects = new Dictionary<ulong, Dictionary<ulong, List<NetworkObject>>>();
|
||||
|
||||
public bool AllowOwnershipRequest = true;
|
||||
public bool OnlyAllowTargetClientId = false;
|
||||
public ulong ClientToAllowOwnership;
|
||||
|
||||
public NetworkObject.OwnershipRequestResponseStatus OwnershipRequestResponseStatus { get; private set; }
|
||||
|
||||
public NetworkObject.OwnershipPermissionsFailureStatus OwnershipPermissionsFailureStatus { get; private set; }
|
||||
|
||||
public NetworkObject.OwnershipRequestResponseStatus ExpectOwnershipRequestResponseStatus { get; set; }
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
NetworkObject.OnOwnershipRequested = OnOwnershipRequested;
|
||||
NetworkObject.OnOwnershipRequestResponse = OnOwnershipRequestResponse;
|
||||
NetworkObject.OnOwnershipPermissionsFailure = OnOwnershipPermissionsFailure;
|
||||
|
||||
base.OnNetworkSpawn();
|
||||
}
|
||||
|
||||
private bool OnOwnershipRequested(ulong clientId)
|
||||
{
|
||||
// If we are not allowing any client to request (without locking), then deny all requests
|
||||
if (!AllowOwnershipRequest)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we are only allowing a specific client and the requesting client is not the target,
|
||||
// then deny the request
|
||||
if (OnlyAllowTargetClientId && clientId != ClientToAllowOwnership)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, approve the request
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnOwnershipRequestResponse(NetworkObject.OwnershipRequestResponseStatus ownershipRequestResponseStatus)
|
||||
{
|
||||
OwnershipRequestResponseStatus = ownershipRequestResponseStatus;
|
||||
}
|
||||
|
||||
private void OnOwnershipPermissionsFailure(NetworkObject.OwnershipPermissionsFailureStatus ownershipPermissionsFailureStatus)
|
||||
{
|
||||
OwnershipPermissionsFailureStatus = ownershipPermissionsFailureStatus;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
NetworkObject.OnOwnershipRequested = null;
|
||||
NetworkObject.OnOwnershipRequestResponse = null;
|
||||
base.OnNetworkSpawn();
|
||||
}
|
||||
|
||||
protected override void OnOwnershipChanged(ulong previous, ulong current)
|
||||
{
|
||||
if (current == NetworkManager.LocalClientId)
|
||||
{
|
||||
CurrentOwnedInstance = NetworkObject;
|
||||
}
|
||||
base.OnOwnershipChanged(previous, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f35119aec96feb348a49b8e0fcd779de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user