using System; using System.Collections; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Unity.Netcode.Transports.UTP; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace Unity.Netcode.TestHelpers.Runtime { /// /// Provides helpers for running multi instance tests. /// public static class NetcodeIntegrationTestHelpers { public const int DefaultMinFrames = 1; public const float DefaultTimeout = 4f; private static List s_NetworkManagerInstances = new List(); private static Dictionary s_Hooks = new Dictionary(); private static bool s_IsStarted; private static int s_ClientCount; private static int s_OriginalTargetFrameRate = -1; public delegate bool MessageHandleCheck(object receivedMessage); internal class MessageHandleCheckWithResult { public MessageHandleCheck Check; public bool Result; } internal class MessageReceiveCheckWithResult { public Type CheckType; public bool Result; } private class MultiInstanceHooks : INetworkHooks { public Dictionary> HandleChecks = new Dictionary>(); public List ReceiveChecks = new List(); public static bool CheckForMessageOfType(object receivedMessage) where T : INetworkMessage { return receivedMessage.GetType() == typeof(T); } public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage { } public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage { } public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { foreach (var check in ReceiveChecks) { if (check.CheckType == messageType) { check.Result = true; ReceiveChecks.Remove(check); break; } } } 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) { return true; } public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { } public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { if (HandleChecks.ContainsKey(typeof(T))) { foreach (var check in HandleChecks[typeof(T)]) { if (check.Check(message)) { check.Result = true; HandleChecks[typeof(T)].Remove(check); break; } } } } } internal const string FirstPartOfTestRunnerSceneName = "InitTestScene"; public static List NetworkManagerInstances => s_NetworkManagerInstances; internal static List ClientSceneHandlers = new List(); /// /// Registers the IntegrationTestSceneHandler for integration tests. /// The default client behavior is to not load scenes on the client side. /// internal static void RegisterSceneManagerHandler(NetworkManager networkManager, bool allowServer = false) { if (!networkManager.IsServer || networkManager.IsServer && allowServer) { var handler = new IntegrationTestSceneHandler(networkManager); ClientSceneHandlers.Add(handler); networkManager.SceneManager.SceneManagerHandler = handler; } } /// /// Call this to clean up the IntegrationTestSceneHandler and destroy the s_CoroutineRunner. /// Note: /// If deriving from or using then you /// typically won't need to do this. /// public static void CleanUpHandlers() { foreach (var handler in ClientSceneHandlers) { handler.Dispose(); } ClientSceneHandlers.Clear(); } /// /// Call this to register scene validation and the IntegrationTestSceneHandler /// Note: /// If deriving from or using then you /// typically won't need to call this. /// public static void RegisterHandlers(NetworkManager networkManager, bool serverSideSceneManager = false) { SceneManagerValidationAndTestRunnerInitialization(networkManager); if (!networkManager.IsServer || networkManager.IsServer && serverSideSceneManager) { RegisterSceneManagerHandler(networkManager); } } private static void AddUnityTransport(NetworkManager networkManager) { // Create transport var unityTransport = networkManager.gameObject.AddComponent(); // We need to increase this buffer size for tests that spawn a bunch of things unityTransport.MaxPayloadSize = 256000; unityTransport.MaxSendQueueSize = 1024 * 1024; // Allow 4 connection attempts that each will time out after 500ms unityTransport.MaxConnectAttempts = 4; unityTransport.ConnectTimeoutMS = 500; // Set the NetworkConfig networkManager.NetworkConfig = new NetworkConfig() { // Set transport NetworkTransport = unityTransport }; } public static NetworkManager CreateServer() { // Create gameObject var go = new GameObject("NetworkManager - Server"); // Create networkManager component var server = go.AddComponent(); NetworkManagerInstances.Insert(0, server); AddUnityTransport(server); return server; } /// /// Creates NetworkingManagers and configures them for use in a multi instance setting. /// /// The amount of clients /// The server NetworkManager /// The clients NetworkManagers /// The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown. /// This determines if the server or clients will be instantiated first (defaults to server first) public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, bool serverFirst = true) { s_NetworkManagerInstances = new List(); server = null; if (serverFirst) { server = CreateServer(); } CreateNewClients(clientCount, out clients); if (!serverFirst) { server = CreateServer(); } s_OriginalTargetFrameRate = Application.targetFrameRate; Application.targetFrameRate = targetFrameRate; return true; } internal static NetworkManager CreateNewClient(int identifier) { // Create gameObject var go = new GameObject("NetworkManager - Client - " + identifier); // Create networkManager component var networkManager = go.AddComponent(); AddUnityTransport(networkManager); return networkManager; } /// /// Used to add a client to the already existing list of clients /// /// The amount of clients /// public static bool CreateNewClients(int clientCount, out NetworkManager[] clients) { clients = new NetworkManager[clientCount]; for (int i = 0; i < clientCount; i++) { // Create networkManager component clients[i] = CreateNewClient(i); } NetworkManagerInstances.AddRange(clients); return true; } /// /// Stops one single client and makes sure to cleanup any static variables in this helper /// /// public static void StopOneClient(NetworkManager clientToStop, bool destroy = true) { clientToStop.Shutdown(); s_Hooks.Remove(clientToStop); if (destroy) { Object.Destroy(clientToStop.gameObject); NetworkManagerInstances.Remove(clientToStop); } } /// /// Starts one single client and makes sure to register the required hooks and handlers /// /// public static void StartOneClient(NetworkManager clientToStart) { clientToStart.StartClient(); s_Hooks[clientToStart] = new MultiInstanceHooks(); clientToStart.MessagingSystem.Hook(s_Hooks[clientToStart]); if (!NetworkManagerInstances.Contains(clientToStart)) { NetworkManagerInstances.Add(clientToStart); } // if set, then invoke this for the client RegisterHandlers(clientToStart); } /// /// Should always be invoked when finished with a single unit test /// (i.e. during TearDown) /// public static void Destroy() { if (s_IsStarted == false) { return; } s_IsStarted = false; // Shutdown the server which forces clients to disconnect foreach (var networkManager in NetworkManagerInstances) { networkManager.Shutdown(); s_Hooks.Remove(networkManager); } // Destroy the network manager instances foreach (var networkManager in NetworkManagerInstances) { if (networkManager.gameObject != null) { Object.Destroy(networkManager.gameObject); } } NetworkManagerInstances.Clear(); CleanUpHandlers(); Application.targetFrameRate = s_OriginalTargetFrameRate; } /// /// We want to exclude the TestRunner scene on the host-server side so it won't try to tell clients to /// synchronize to this scene when they connect /// private static bool VerifySceneIsValidForClientsToLoad(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode) { // exclude test runner scene if (sceneName.StartsWith(FirstPartOfTestRunnerSceneName)) { return false; } return true; } /// /// This registers scene validation callback for the server to prevent it from telling connecting /// clients to synchronize (i.e. load) the test runner scene. This will also register the test runner /// scene and its handle for both client(s) and server-host. /// private static void SceneManagerValidationAndTestRunnerInitialization(NetworkManager networkManager) { // If VerifySceneBeforeLoading is not already set, then go ahead and set it so the host/server // will not try to synchronize clients to the TestRunner scene. We only need to do this for the server. if (networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeLoading == null) { networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad; // If a unit/integration test does not handle this on their own, then Ignore the validation warning networkManager.SceneManager.DisableValidationWarnings(true); } // Register the test runner scene so it will be able to synchronize NetworkObjects without logging a // warning about using the currently active scene var scene = SceneManager.GetActiveScene(); // As long as this is a test runner scene (or most likely a test runner scene) if (scene.name.StartsWith(FirstPartOfTestRunnerSceneName)) { // Register the test runner scene just so we avoid another warning about not being able to find the // scene to synchronize NetworkObjects. Next, add the currently active test runner scene to the scenes // loaded and register the server to client scene handle since host-server shares the test runner scene // with the clients. networkManager.SceneManager.GetAndAddNewlyLoadedSceneByName(scene.name); networkManager.SceneManager.ServerSceneHandleToClientSceneHandle.Add(scene.handle, scene.handle); } } public delegate void BeforeClientStartCallback(); /// /// Starts NetworkManager instances created by the Create method. /// /// Whether or not to create a Host instead of Server /// The Server NetworkManager /// The Clients NetworkManager /// called immediately after server is started and before client(s) are started /// public static bool Start(bool host, NetworkManager server, NetworkManager[] clients, BeforeClientStartCallback callback = null) { if (s_IsStarted) { throw new InvalidOperationException($"{nameof(NetcodeIntegrationTestHelpers)} already thinks it is started. Did you forget to Destroy?"); } s_IsStarted = true; s_ClientCount = clients.Length; if (host) { server.StartHost(); } else { server.StartServer(); } var hooks = new MultiInstanceHooks(); server.MessagingSystem.Hook(hooks); s_Hooks[server] = hooks; // if set, then invoke this for the server RegisterHandlers(server); callback?.Invoke(); for (int i = 0; i < clients.Length; i++) { clients[i].StartClient(); hooks = new MultiInstanceHooks(); clients[i].MessagingSystem.Hook(hooks); s_Hooks[clients[i]] = hooks; // if set, then invoke this for the client RegisterHandlers(clients[i]); } return true; } /// /// Used to return a value of type T from a wait condition /// public class ResultWrapper { public T Result; } private static uint s_AutoIncrementGlobalObjectIdHashCounter = 111111; public static uint GetNextGlobalIdHashValue() { return ++s_AutoIncrementGlobalObjectIdHashCounter; } public static bool IsNetcodeIntegrationTestRunning { get; internal set; } public static void RegisterNetcodeIntegrationTest(bool registered) { IsNetcodeIntegrationTestRunning = registered; } /// /// Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects. /// In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain, /// MultiInstanceHelper has a helper function that lets you mark a runtime created object to be /// treated as a prefab by the Netcode. That's how we can get away with creating the player prefab /// at runtime without it being treated as a SceneObject or causing other conflicts with the Netcode. /// /// The networkObject to be treated as Prefab /// The GlobalObjectId to force public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint globalObjectIdHash = default) { // Override `GlobalObjectIdHash` if `globalObjectIdHash` param is set if (globalObjectIdHash != default) { networkObject.GlobalObjectIdHash = globalObjectIdHash; } // Fallback to auto-increment if `GlobalObjectIdHash` was never set if (networkObject.GlobalObjectIdHash == default) { networkObject.GlobalObjectIdHash = ++s_AutoIncrementGlobalObjectIdHashCounter; } // Prevent object from being snapped up as a scene object networkObject.IsSceneObject = false; // To avoid issues with integration tests that forget to clean up, // this feature only works with NetcodeIntegrationTest derived classes if (IsNetcodeIntegrationTestRunning) { if (networkObject.GetComponent() == null && networkObject.GetComponentInChildren() == null) { // Add the object identifier component networkObject.gameObject.AddComponent(); } } } // We use GameObject instead of SceneObject to be able to keep hierarchy public static void MarkAsSceneObjectRoot(GameObject networkObjectRoot, NetworkManager server, NetworkManager[] clients) { networkObjectRoot.name += " - Server"; NetworkObject[] serverNetworkObjects = networkObjectRoot.GetComponentsInChildren(); for (int i = 0; i < serverNetworkObjects.Length; i++) { serverNetworkObjects[i].NetworkManagerOwner = server; } for (int i = 0; i < clients.Length; i++) { GameObject root = Object.Instantiate(networkObjectRoot); root.name += " - Client - " + i; NetworkObject[] clientNetworkObjects = root.GetComponentsInChildren(); for (int j = 0; j < clientNetworkObjects.Length; j++) { clientNetworkObjects[j].NetworkManagerOwner = clients[i]; } } } /// /// Waits (yields) until specified amount of network ticks has been passed. /// public static IEnumerator WaitForTicks(NetworkManager networkManager, int count) { var targetTick = networkManager.NetworkTickSystem.LocalTime.Tick + count; yield return new WaitUntil(() => networkManager.NetworkTickSystem.LocalTime.Tick >= targetTick); } /// /// Waits on the client side to be connected. /// /// The client /// The result. If null, it will automatically assert /// The max frames to wait for public static IEnumerator WaitForClientConnected(NetworkManager client, ResultWrapper result = null, float timeout = DefaultTimeout) { yield return WaitForClientsConnected(new NetworkManager[] { client }, result, timeout); } /// /// Similar to WaitForClientConnected, this waits for multiple clients to be connected. /// /// The clients to be connected /// The result. If null, it will automatically assert< /// The max frames to wait for /// public static IEnumerator WaitForClientsConnected(NetworkManager[] clients, ResultWrapper result = null, float timeout = DefaultTimeout) { // Make sure none are the host client foreach (var client in clients) { if (client.IsServer) { throw new InvalidOperationException("Cannot wait for connected as server"); } } var allConnected = true; var startTime = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startTime < timeout) { allConnected = true; foreach (var client in clients) { if (!client.IsConnectedClient) { allConnected = false; break; } } if (allConnected) { break; } var nextFrameNumber = Time.frameCount + 1; yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); } if (result != null) { result.Result = allConnected; } else { for (var i = 0; i < clients.Length; ++i) { var client = clients[i]; // Logging i+1 because that's the local client ID they'll get (0 is server) // Can't use client.LocalClientId because that doesn't get assigned until IsConnectedClient == true, Assert.True(client.IsConnectedClient, $"Client {i + 1} never connected"); } } } /// /// Waits on the server side for 1 client to be connected /// /// The server /// The result. If null, it will automatically assert /// The max frames to wait for public static IEnumerator WaitForClientConnectedToServer(NetworkManager server, ResultWrapper result = null, float timeout = DefaultTimeout) { yield return WaitForClientsConnectedToServer(server, server.IsHost ? s_ClientCount + 1 : s_ClientCount, result, timeout); } /// /// Waits on the server side for 1 client to be connected /// /// The server /// The result. If null, it will automatically assert /// The max frames to wait for public static IEnumerator WaitForClientsConnectedToServer(NetworkManager server, int clientCount = 1, ResultWrapper result = null, float timeout = DefaultTimeout) { if (!server.IsServer) { throw new InvalidOperationException("Cannot wait for connected as client"); } var startTime = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startTime < timeout && server.ConnectedClients.Count != clientCount) { var nextFrameNumber = Time.frameCount + 1; yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); } var res = server.ConnectedClients.Count == clientCount; if (result != null) { result.Result = res; } else { Assert.True(res, "A client never connected to server"); } } /// /// Gets a NetworkObject instance as it's represented by a certain peer. /// /// The networkObjectId to get /// The representation to get the object from /// The result /// Whether or not to fail if no object is found and result is null /// The max frames to wait for public static IEnumerator GetNetworkObjectByRepresentation(ulong networkObjectId, NetworkManager representation, ResultWrapper result, bool failIfNull = true, float timeout = DefaultTimeout) { if (result == null) { throw new ArgumentNullException("Result cannot be null"); } var startTime = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startTime < timeout && representation.SpawnManager.SpawnedObjects.All(x => x.Value.NetworkObjectId != networkObjectId)) { var nextFrameNumber = Time.frameCount + 1; yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); } result.Result = representation.SpawnManager.SpawnedObjects.First(x => x.Value.NetworkObjectId == networkObjectId).Value; if (failIfNull && result.Result == null) { Assert.Fail("NetworkObject could not be found"); } } /// /// Gets a NetworkObject instance as it's represented by a certain peer. /// /// The predicate used to filter for your target NetworkObject /// The representation to get the object from /// The result /// Whether or not to fail if no object is found and result is null /// The max frames to wait for public static IEnumerator GetNetworkObjectByRepresentation(Func predicate, NetworkManager representation, ResultWrapper result, bool failIfNull = true, float timeout = DefaultTimeout) { if (result == null) { throw new ArgumentNullException("Result cannot be null"); } if (predicate == null) { throw new ArgumentNullException("Predicate cannot be null"); } var startTime = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startTime < timeout && !representation.SpawnManager.SpawnedObjects.Any(x => predicate(x.Value))) { var nextFrameNumber = Time.frameCount + 1; yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); } result.Result = representation.SpawnManager.SpawnedObjects.FirstOrDefault(x => predicate(x.Value)).Value; if (failIfNull && result.Result == null) { Assert.Fail("NetworkObject could not be found"); } } /// /// Waits for a predicate condition to be met /// /// The predicate to wait for /// The result. If null, it will fail if the predicate is not met /// The min frames to wait for /// The max frames to wait for public static IEnumerator WaitForCondition(Func predicate, ResultWrapper result = null, float timeout = DefaultTimeout, int minFrames = DefaultMinFrames) { if (predicate == null) { throw new ArgumentNullException("Predicate cannot be null"); } var startTime = Time.realtimeSinceStartup; if (minFrames > 0) { yield return new WaitUntil(() => Time.frameCount >= minFrames); } while (Time.realtimeSinceStartup - startTime < timeout && !predicate()) { // Changed to 2 frames to avoid the scenario where it would take 1+ frames to // see a value change (i.e. discovered in the NetworkTransformTests) var nextFrameNumber = Time.frameCount + 2; yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); } var res = predicate(); if (result != null) { result.Result = res; } else { Assert.True(res, "PREDICATE CONDITION"); } } /// /// Waits for a message of the given type to be received /// /// The result. If null, it will fail if the predicate is not met /// The max time in seconds to wait for internal static IEnumerator WaitForMessageOfTypeReceived(NetworkManager toBeReceivedBy, ResultWrapper result = null, float timeout = DefaultTimeout) where T : INetworkMessage { var hooks = s_Hooks[toBeReceivedBy]; var check = new MessageReceiveCheckWithResult { CheckType = typeof(T) }; hooks.ReceiveChecks.Add(check); if (result == null) { result = new ResultWrapper(); } var startTime = Time.realtimeSinceStartup; while (!check.Result && Time.realtimeSinceStartup - startTime < timeout) { yield return null; } var res = check.Result; result.Result = res; Assert.True(result.Result, $"Expected message {typeof(T).Name} was not received within {timeout}s."); } /// /// Waits for a message of the given type to be received /// /// The result. If null, it will fail if the predicate is not met /// The max time in seconds to wait for internal static IEnumerator WaitForMessageOfTypeHandled(NetworkManager toBeReceivedBy, ResultWrapper result = null, float timeout = DefaultTimeout) where T : INetworkMessage { var hooks = s_Hooks[toBeReceivedBy]; if (!hooks.HandleChecks.ContainsKey(typeof(T))) { hooks.HandleChecks.Add(typeof(T), new List()); } var check = new MessageHandleCheckWithResult { Check = MultiInstanceHooks.CheckForMessageOfType }; hooks.HandleChecks[typeof(T)].Add(check); if (result == null) { result = new ResultWrapper(); } yield return ExecuteWaitForHook(check, result, timeout); Assert.True(result.Result, $"Expected message {typeof(T).Name} was not handled within {timeout}s."); } /// /// Waits for a specific message, defined by a user callback, to be received /// /// Called for each received message to check if it's the right one /// The result. If null, it will fail if the predicate is not met /// The max time in seconds to wait for internal static IEnumerator WaitForMessageMeetingRequirementHandled(NetworkManager toBeReceivedBy, MessageHandleCheck requirement, ResultWrapper result = null, float timeout = DefaultTimeout) { var hooks = s_Hooks[toBeReceivedBy]; if (!hooks.HandleChecks.ContainsKey(typeof(T))) { hooks.HandleChecks.Add(typeof(T), new List()); } var check = new MessageHandleCheckWithResult { Check = requirement }; hooks.HandleChecks[typeof(T)].Add(check); if (result == null) { result = new ResultWrapper(); } yield return ExecuteWaitForHook(check, result, timeout); Assert.True(result.Result, $"Expected message meeting user requirements was not handled within {timeout}s."); } private static IEnumerator ExecuteWaitForHook(MessageHandleCheckWithResult check, ResultWrapper result, float timeout) { var startTime = Time.realtimeSinceStartup; while (!check.Result && Time.realtimeSinceStartup - startTime < timeout) { yield return null; } var res = check.Result; result.Result = res; } } // Empty MonoBehaviour that is a holder of coroutine internal class CoroutineRunner : MonoBehaviour { } }