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:
Unity Technologies
2024-04-02 00:00:00 +00:00
parent f8ebf679ec
commit 143a6cbd34
140 changed files with 18009 additions and 2672 deletions

View 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}");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7b700919b058f446a75a398d5be9af4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a759aeb53d12d842899381b411f3d2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4cec6e6e1b9bdb746806290355d8cb50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db4c955c05fdc194eb47e7774b9c5101
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f35119aec96feb348a49b8e0fcd779de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: