using System; using System.Collections; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; using System.Runtime.CompilerServices; using Unity.Netcode.RuntimeTests; using Object = UnityEngine.Object; namespace Unity.Netcode.TestHelpers.Runtime { /// /// The default Netcode for GameObjects integration test helper class /// public abstract class NetcodeIntegrationTest { /// /// Used to determine if a NetcodeIntegrationTest is currently running to /// determine how clients will load scenes /// internal static bool IsRunning { get; private set; } protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f); protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); public NetcodeLogAssert NetcodeLogAssert; /// /// Registered list of all NetworkObjects spawned. /// Format is as follows: /// [ClientId-side where this NetworkObject instance resides][NetworkObjectId][NetworkObject] /// Where finding the NetworkObject with a NetworkObjectId of 10 on ClientId of 2 would be: /// s_GlobalNetworkObjects[2][10] /// To find the client or server player objects please see: /// /// protected static Dictionary> s_GlobalNetworkObjects = new Dictionary>(); public static void RegisterNetworkObject(NetworkObject networkObject) { if (!s_GlobalNetworkObjects.ContainsKey(networkObject.NetworkManager.LocalClientId)) { s_GlobalNetworkObjects.Add(networkObject.NetworkManager.LocalClientId, new Dictionary()); } if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].ContainsKey(networkObject.NetworkObjectId)) { if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId] == null) { Assert.False(s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId][networkObject.NetworkObjectId] != null, $"Duplicate NetworkObjectId {networkObject.NetworkObjectId} found in {nameof(s_GlobalNetworkObjects)} for client id {networkObject.NetworkManager.LocalClientId}!"); } else { s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId][networkObject.NetworkObjectId] = networkObject; } } else { s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].Add(networkObject.NetworkObjectId, networkObject); } } public static void DeregisterNetworkObject(NetworkObject networkObject) { if (networkObject.IsSpawned && networkObject.NetworkManager != null) { DeregisterNetworkObject(networkObject.NetworkManager.LocalClientId, networkObject.NetworkObjectId); } } public static void DeregisterNetworkObject(ulong localClientId, ulong networkObjectId) { if (s_GlobalNetworkObjects.ContainsKey(localClientId) && s_GlobalNetworkObjects[localClientId].ContainsKey(networkObjectId)) { s_GlobalNetworkObjects[localClientId].Remove(networkObjectId); if (s_GlobalNetworkObjects[localClientId].Count == 0) { s_GlobalNetworkObjects.Remove(localClientId); } } } protected int TotalClients => m_UseHost ? m_NumberOfClients + 1 : m_NumberOfClients; protected const uint k_DefaultTickRate = 30; private int m_NumberOfClients; protected abstract int NumberOfClients { get; } /// /// Set this to false to create the clients first. /// Note: If you are using scene placed NetworkObjects or doing any form of scene testing and /// get prefab hash id "soft synchronization" errors, then set this to false and run your test /// again. This is a work-around until we can resolve some issues with NetworkManagerOwner and /// NetworkManager.Singleton. /// protected bool m_CreateServerFirst = true; public enum NetworkManagerInstatiationMode { PerTest, // This will create and destroy new NetworkManagers for each test within a child derived class AllTests, // This will create one set of NetworkManagers used for all tests within a child derived class (destroyed once all tests are finished) DoNotCreate // This will not create any NetworkManagers, it is up to the derived class to manage. } public enum HostOrServer { Host, Server } protected GameObject m_PlayerPrefab; protected NetworkManager m_ServerNetworkManager; protected NetworkManager[] m_ClientNetworkManagers; /// /// Contains each client relative set of player NetworkObject instances /// [Client Relative set of player instances][The player instance ClientId][The player instance's NetworkObject] /// Example: /// To get the player instance with a ClientId of 3 that was instantiated (relative) on the player instance with a ClientId of 2 /// m_PlayerNetworkObjects[2][3] /// protected Dictionary> m_PlayerNetworkObjects = new Dictionary>(); protected bool m_UseHost = true; protected int m_TargetFrameRate = 60; private NetworkManagerInstatiationMode m_NetworkManagerInstatiationMode; protected bool m_EnableVerboseDebug { get; set; } /// /// When set to true, this will bypass the entire /// wait for clients to connect process. /// /// /// CAUTION: /// Setting this to true will bypass other helper /// identification related code, so this should only /// be used for connection failure oriented testing /// protected bool m_BypassConnectionTimeout { get; set; } /// /// Used to display the various integration test /// stages and can be used to log verbose information /// for troubleshooting an integration test. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void VerboseDebug(string msg) { if (m_EnableVerboseDebug) { Debug.Log(msg); } } /// /// Override this and return true if you need /// to troubleshoot a hard to track bug within an /// integration test. /// protected virtual bool OnSetVerboseDebug() { return false; } /// /// The very first thing invoked during the that /// determines how this integration test handles NetworkManager instantiation /// and destruction. /// Override this method to change the default mode: /// /// protected virtual NetworkManagerInstatiationMode OnSetIntegrationTestMode() { return NetworkManagerInstatiationMode.PerTest; } protected virtual void OnOneTimeSetup() { } [OneTimeSetUp] public void OneTimeSetup() { Application.runInBackground = true; m_NumberOfClients = NumberOfClients; IsRunning = true; m_EnableVerboseDebug = OnSetVerboseDebug(); IntegrationTestSceneHandler.VerboseDebugMode = m_EnableVerboseDebug; VerboseDebug($"Entering {nameof(OneTimeSetup)}"); m_NetworkManagerInstatiationMode = OnSetIntegrationTestMode(); // Enable NetcodeIntegrationTest auto-label feature NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(true); OnOneTimeSetup(); VerboseDebug($"Exiting {nameof(OneTimeSetup)}"); } /// /// Called before creating and starting the server and clients /// Note: For and /// mode integration tests. /// For those two modes, if you want to have access to the server or client /// s then override . /// and /// protected virtual IEnumerator OnSetup() { yield return null; } [UnitySetUp] public IEnumerator SetUp() { VerboseDebug($"Entering {nameof(SetUp)}"); NetcodeLogAssert = new NetcodeLogAssert(); yield return OnSetup(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null || m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) { CreateServerAndClients(); yield return StartServerAndClients(); } VerboseDebug($"Exiting {nameof(SetUp)}"); } /// /// Override this to add components or adjustments to the default player prefab /// /// protected virtual void OnCreatePlayerPrefab() { } private void CreatePlayerPrefab() { VerboseDebug($"Entering {nameof(CreatePlayerPrefab)}"); // Create playerPrefab m_PlayerPrefab = new GameObject("Player"); NetworkObject networkObject = m_PlayerPrefab.AddComponent(); // Make it a prefab NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); OnCreatePlayerPrefab(); VerboseDebug($"Exiting {nameof(CreatePlayerPrefab)}"); } /// /// This is invoked before the server and client(s) are started. /// Override this method if you want to make any adjustments to their /// NetworkManager instances. /// protected virtual void OnServerAndClientsCreated() { } /// /// Will create number of clients. /// To create a specific number of clients /// protected void CreateServerAndClients() { CreateServerAndClients(NumberOfClients); } private void AddRemoveNetworkManager(NetworkManager networkManager, bool addNetworkManager) { var clientNetworkManagersList = new List(m_ClientNetworkManagers); if (addNetworkManager) { clientNetworkManagersList.Add(networkManager); } else { clientNetworkManagersList.Remove(networkManager); } m_ClientNetworkManagers = clientNetworkManagersList.ToArray(); m_NumberOfClients = clientNetworkManagersList.Count; } /// /// CreateAndStartNewClient Only /// Invoked when the newly created client has been created /// protected virtual void OnNewClientCreated(NetworkManager networkManager) { } /// /// CreateAndStartNewClient Only /// Invoked when the newly created client has been created and started /// protected virtual void OnNewClientStarted(NetworkManager networkManager) { } /// /// CreateAndStartNewClient Only /// Invoked when the newly created client has been created, started, and connected /// to the server-host. /// protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager) { } /// /// This will create, start, and connect a new client while in the middle of an /// integration test. /// protected IEnumerator CreateAndStartNewClient() { var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length); networkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; // Notification that the new client (NetworkManager) has been created // in the event any modifications need to be made before starting the client OnNewClientCreated(networkManager); NetcodeIntegrationTestHelpers.StartOneClient(networkManager); if (LogAllMessages) { networkManager.MessagingSystem.Hook(new DebugNetworkHooks()); } AddRemoveNetworkManager(networkManager, true); OnNewClientStarted(networkManager); // Wait for the new client to connect yield return WaitForClientsConnectedOrTimeOut(); OnNewClientStartedAndConnected(networkManager); if (s_GlobalTimeoutHelper.TimedOut) { AddRemoveNetworkManager(networkManager, false); Object.Destroy(networkManager.gameObject); } AssertOnTimeout($"{nameof(CreateAndStartNewClient)} timed out waiting for the new client to be connected!"); ClientNetworkManagerPostStart(networkManager); VerboseDebug($"[{networkManager.name}] Created and connected!"); } /// /// This will stop a client while in the middle of an integration test /// protected IEnumerator StopOneClient(NetworkManager networkManager, bool destroy = false) { NetcodeIntegrationTestHelpers.StopOneClient(networkManager, destroy); AddRemoveNetworkManager(networkManager, false); yield return WaitForConditionOrTimeOut(() => !networkManager.IsConnectedClient); } /// /// Creates the server and clients /// /// protected void CreateServerAndClients(int numberOfClients) { VerboseDebug($"Entering {nameof(CreateServerAndClients)}"); CreatePlayerPrefab(); // Create multiple NetworkManager instances if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_CreateServerFirst)) { Debug.LogError("Failed to create instances"); Assert.Fail("Failed to create instances"); } m_ClientNetworkManagers = clients; m_ServerNetworkManager = server; if (m_ServerNetworkManager != null) { s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); } // Set the player prefab for the server and clients m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; foreach (var client in m_ClientNetworkManagers) { client.NetworkConfig.PlayerPrefab = m_PlayerPrefab; } // Provides opportunity to allow child derived classes to // modify the NetworkManager's configuration before starting. OnServerAndClientsCreated(); VerboseDebug($"Exiting {nameof(CreateServerAndClients)}"); } /// /// Override this method and return false in order to be able /// to manually control when the server and clients are started. /// protected virtual bool CanStartServerAndClients() { return true; } /// /// Invoked after the server and clients have started. /// Note: No connection verification has been done at this point /// protected virtual IEnumerator OnStartedServerAndClients() { yield return null; } /// /// Invoked after the server and clients have started and verified /// their connections with each other. /// protected virtual IEnumerator OnServerAndClientsConnected() { yield return null; } private void ClientNetworkManagerPostStart(NetworkManager networkManager) { networkManager.name = $"NetworkManager - Client - {networkManager.LocalClientId}"; Assert.NotNull(networkManager.LocalClient.PlayerObject, $"{nameof(StartServerAndClients)} detected that client {networkManager.LocalClientId} does not have an assigned player NetworkObject!"); // Go ahead and create an entry for this new client if (!m_PlayerNetworkObjects.ContainsKey(networkManager.LocalClientId)) { m_PlayerNetworkObjects.Add(networkManager.LocalClientId, new Dictionary()); } #if UNITY_2023_1_OR_NEWER // Get all player instances for the current client NetworkManager instance var clientPlayerClones = Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsPlayerObject && c.OwnerClientId == networkManager.LocalClientId).ToList(); #else // Get all player instances for the current client NetworkManager instance var clientPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == networkManager.LocalClientId).ToList(); #endif // Add this player instance to each client player entry foreach (var playerNetworkObject in clientPlayerClones) { // When the server is not the host this needs to be done if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId)) { m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary()); } if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(networkManager.LocalClientId)) { m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(networkManager.LocalClientId, playerNetworkObject); } } #if UNITY_2023_1_OR_NEWER // For late joining clients, add the remaining (if any) cloned versions of each client's player clientPlayerClones = Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsPlayerObject && c.NetworkManager == networkManager).ToList(); #else // For late joining clients, add the remaining (if any) cloned versions of each client's player clientPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.NetworkManager == networkManager).ToList(); #endif foreach (var playerNetworkObject in clientPlayerClones) { if (!m_PlayerNetworkObjects[networkManager.LocalClientId].ContainsKey(playerNetworkObject.OwnerClientId)) { m_PlayerNetworkObjects[networkManager.LocalClientId].Add(playerNetworkObject.OwnerClientId, playerNetworkObject); } } } protected void ClientNetworkManagerPostStartInit() { // Creates a dictionary for all player instances client and server relative // This provides a simpler way to get a specific player instance relative to a client instance foreach (var networkManager in m_ClientNetworkManagers) { ClientNetworkManagerPostStart(networkManager); } if (m_UseHost) { #if UNITY_2023_1_OR_NEWER var clientSideServerPlayerClones = Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); #else var clientSideServerPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); #endif foreach (var playerNetworkObject in clientSideServerPlayerClones) { // When the server is not the host this needs to be done if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId)) { m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary()); } if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(m_ServerNetworkManager.LocalClientId)) { m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); } } } } protected virtual bool LogAllMessages => false; /// /// This starts the server and clients as long as /// returns true. /// protected IEnumerator StartServerAndClients() { if (CanStartServerAndClients()) { VerboseDebug($"Entering {nameof(StartServerAndClients)}"); // Start the instances and pass in our SceneManagerInitialization action that is invoked immediately after host-server // is started and after each client is started. if (!NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, m_ClientNetworkManagers)) { Debug.LogError("Failed to start instances"); Assert.Fail("Failed to start instances"); } if (LogAllMessages) { EnableMessageLogging(); } RegisterSceneManagerHandler(); // Notification that the server and clients have been started yield return OnStartedServerAndClients(); // When true, we skip everything else (most likely a connection oriented test) if (!m_BypassConnectionTimeout) { // Wait for all clients to connect yield return WaitForClientsConnectedOrTimeOut(); AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!"); if (m_UseHost || m_ServerNetworkManager.IsHost) { #if UNITY_2023_1_OR_NEWER // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries var serverPlayerClones = Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); #else // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries var serverPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); #endif foreach (var playerNetworkObject in serverPlayerClones) { if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId)) { m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary()); } m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); } } ClientNetworkManagerPostStartInit(); // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. yield return OnServerAndClientsConnected(); VerboseDebug($"Exiting {nameof(StartServerAndClients)}"); } } } /// /// Override this method to control when clients /// can fake-load a scene. /// protected virtual bool CanClientsLoad() { return true; } /// /// Override this method to control when clients /// can fake-unload a scene. /// protected virtual bool CanClientsUnload() { return true; } /// /// De-Registers from the CanClientsLoad and CanClientsUnload events of the /// ClientSceneHandler (default is IntegrationTestSceneHandler). /// protected void DeRegisterSceneManagerHandler() { IntegrationTestSceneHandler.CanClientsLoad -= ClientSceneHandler_CanClientsLoad; IntegrationTestSceneHandler.CanClientsUnload -= ClientSceneHandler_CanClientsUnload; IntegrationTestSceneHandler.NetworkManagers.Clear(); } /// /// Registers the CanClientsLoad and CanClientsUnload events of the /// ClientSceneHandler. /// The default is: . /// protected void RegisterSceneManagerHandler() { IntegrationTestSceneHandler.CanClientsLoad += ClientSceneHandler_CanClientsLoad; IntegrationTestSceneHandler.CanClientsUnload += ClientSceneHandler_CanClientsUnload; NetcodeIntegrationTestHelpers.RegisterSceneManagerHandler(m_ServerNetworkManager, true); } private bool ClientSceneHandler_CanClientsUnload() { return CanClientsUnload(); } private bool ClientSceneHandler_CanClientsLoad() { return CanClientsLoad(); } protected bool OnCanSceneCleanUpUnload(Scene scene) { return true; } /// /// This shuts down all NetworkManager instances registered via the /// class and cleans up /// the test runner scene of any left over NetworkObjects. /// /// protected void ShutdownAndCleanUp() { VerboseDebug($"Entering {nameof(ShutdownAndCleanUp)}"); // Shutdown and clean up both of our NetworkManager instances try { DeRegisterSceneManagerHandler(); NetcodeIntegrationTestHelpers.Destroy(); m_PlayerNetworkObjects.Clear(); s_GlobalNetworkObjects.Clear(); } catch (Exception e) { throw e; } finally { if (m_PlayerPrefab != null) { Object.Destroy(m_PlayerPrefab); m_PlayerPrefab = null; } } // Cleanup any remaining NetworkObjects DestroySceneNetworkObjects(); UnloadRemainingScenes(); // reset the m_ServerWaitForTick for the next test to initialize s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); VerboseDebug($"Exiting {nameof(ShutdownAndCleanUp)}"); } /// /// Note: For mode /// this is called before ShutdownAndCleanUp. /// protected virtual IEnumerator OnTearDown() { yield return null; } [UnityTearDown] public IEnumerator TearDown() { VerboseDebug($"Entering {nameof(TearDown)}"); yield return OnTearDown(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) { ShutdownAndCleanUp(); } VerboseDebug($"Exiting {nameof(TearDown)}"); NetcodeLogAssert.Dispose(); } /// /// Override this method to do handle cleaning up once the test(s) /// within the child derived class have completed /// Note: For mode /// this is called before ShutdownAndCleanUp. /// protected virtual void OnOneTimeTearDown() { } [OneTimeTearDown] public void OneTimeTearDown() { IntegrationTestSceneHandler.VerboseDebugMode = false; VerboseDebug($"Entering {nameof(OneTimeTearDown)}"); OnOneTimeTearDown(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests) { ShutdownAndCleanUp(); } // Disable NetcodeIntegrationTest auto-label feature NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(false); UnloadRemainingScenes(); VerboseDebug($"Exiting {nameof(OneTimeTearDown)}"); IsRunning = false; } /// /// Override this to filter out the s that you /// want to allow to persist between integration tests. /// /// /// /// the network object in question to be destroyed protected virtual bool CanDestroyNetworkObject(NetworkObject networkObject) { return true; } /// /// Destroys all NetworkObjects at the end of a test cycle. /// protected void DestroySceneNetworkObjects() { #if UNITY_2023_1_OR_NEWER var networkObjects = Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = Object.FindObjectsOfType(); #endif foreach (var networkObject in networkObjects) { // This can sometimes be null depending upon order of operations // when dealing with parented NetworkObjects. If NetworkObjectB // is a child of NetworkObjectA and NetworkObjectA comes before // NetworkObjectB in the list of NeworkObjects found, then when // NetworkObjectA's GameObject is destroyed it will also destroy // NetworkObjectB's GameObject which will destroy NetworkObjectB. // If there is a null entry in the list, this is the most likely // scenario and so we just skip over it. if (networkObject == null) { continue; } if (CanDestroyNetworkObject(networkObject)) { networkObject.NetworkManagerOwner = m_ServerNetworkManager; // Destroy the GameObject that holds the NetworkObject component Object.DestroyImmediate(networkObject.gameObject); } } } /// /// For debugging purposes, this will turn on verbose logging of all messages and batches sent and received /// protected void EnableMessageLogging() { m_ServerNetworkManager.MessagingSystem.Hook(new DebugNetworkHooks()); foreach (var client in m_ClientNetworkManagers) { client.MessagingSystem.Hook(new DebugNetworkHooks()); } } /// /// Waits for the function condition to return true or it will time out. /// This will operate at the current m_ServerNetworkManager.NetworkConfig.TickRate /// and allow for a unique TimeoutHelper handler (if none then it uses the default) /// Notes: This provides more stability when running integration tests that could be /// impacted by: /// -how the integration test is being executed (i.e. in editor or in a stand alone build) /// -potential platform performance issues (i.e. VM is throttled or maxed) /// Note: For more complex tests, and the overloaded /// version of this method /// public static IEnumerator WaitForConditionOrTimeOut(Func checkForCondition, TimeoutHelper timeOutHelper = null) { if (checkForCondition == null) { throw new ArgumentNullException($"checkForCondition cannot be null!"); } // If none is provided we use the default global time out helper if (timeOutHelper == null) { timeOutHelper = s_GlobalTimeoutHelper; } // Start checking for a timeout timeOutHelper.Start(); while (!timeOutHelper.HasTimedOut()) { // Update and check to see if the condition has been met if (checkForCondition.Invoke()) { break; } // Otherwise wait for 1 tick interval yield return s_DefaultWaitForTick; } // Stop checking for a timeout timeOutHelper.Stop(); } /// /// This version accepts an IConditionalPredicate implementation to provide /// more flexibility for checking complex conditional cases. /// public static IEnumerator WaitForConditionOrTimeOut(IConditionalPredicate conditionalPredicate, TimeoutHelper timeOutHelper = null) { if (conditionalPredicate == null) { throw new ArgumentNullException($"checkForCondition cannot be null!"); } // If none is provided we use the default global time out helper if (timeOutHelper == null) { timeOutHelper = s_GlobalTimeoutHelper; } conditionalPredicate.Started(); yield return WaitForConditionOrTimeOut(conditionalPredicate.HasConditionBeenReached, timeOutHelper); conditionalPredicate.Finished(timeOutHelper.TimedOut); } /// /// Validates that all remote clients (i.e. non-server) detect they are connected /// to the server and that the server reflects the appropriate number of clients /// have connected or it will time out. /// /// An array of clients to be checked protected IEnumerator WaitForClientsConnectedOrTimeOut(NetworkManager[] clientsToCheck) { var remoteClientCount = clientsToCheck.Length; var serverClientCount = m_ServerNetworkManager.IsHost ? remoteClientCount + 1 : remoteClientCount; yield return WaitForConditionOrTimeOut(() => clientsToCheck.Where((c) => c.IsConnectedClient).Count() == remoteClientCount && m_ServerNetworkManager.ConnectedClients.Count == serverClientCount); } /// /// Overloaded method that just passes in all clients to /// /// protected IEnumerator WaitForClientsConnectedOrTimeOut() { yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers); } internal IEnumerator WaitForMessageReceived(List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) where T : INetworkMessage { // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages var messageHookEntriesForSpawn = new List(); foreach (var clientNetworkManager in wiatForReceivedBy) { var messageHook = new MessageHookEntry(clientNetworkManager, type); messageHook.AssignMessageType(); messageHookEntriesForSpawn.Add(messageHook); } // Used to determine if all clients received the CreateObjectMessage var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); yield return WaitForConditionOrTimeOut(hooks); Assert.False(s_GlobalTimeoutHelper.TimedOut); } internal IEnumerator WaitForMessagesReceived(List messagesInOrder, List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) { // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages var messageHookEntriesForSpawn = new List(); foreach (var clientNetworkManager in wiatForReceivedBy) { foreach (var message in messagesInOrder) { var messageHook = new MessageHookEntry(clientNetworkManager, type); messageHook.AssignMessageType(message); messageHookEntriesForSpawn.Add(messageHook); } } // Used to determine if all clients received the CreateObjectMessage var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); yield return WaitForConditionOrTimeOut(hooks); Assert.False(s_GlobalTimeoutHelper.TimedOut); } /// /// Creates a basic NetworkObject test prefab, assigns it to a new /// NetworkPrefab entry, and then adds it to the server and client(s) /// NetworkManagers' NetworkConfig.NetworkPrefab lists. /// /// the basic name to be used for each instance /// NetworkObject of the GameObject assigned to the new NetworkPrefab entry protected GameObject CreateNetworkObjectPrefab(string baseName) { var prefabCreateAssertError = $"You can only invoke this method during {nameof(OnServerAndClientsCreated)} " + $"but before {nameof(OnStartedServerAndClients)}!"; Assert.IsNotNull(m_ServerNetworkManager, prefabCreateAssertError); Assert.IsFalse(m_ServerNetworkManager.IsListening, prefabCreateAssertError); var gameObject = new GameObject(); gameObject.name = baseName; var networkObject = gameObject.AddComponent(); networkObject.NetworkManagerOwner = m_ServerNetworkManager; NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); var networkPrefab = new NetworkPrefab() { Prefab = gameObject }; m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); foreach (var clientNetworkManager in m_ClientNetworkManagers) { clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); } return gameObject; } /// /// Overloaded method /// protected GameObject SpawnObject(GameObject prefabGameObject, NetworkManager owner, bool destroyWithScene = false) { var prefabNetworkObject = prefabGameObject.GetComponent(); Assert.IsNotNull(prefabNetworkObject, $"{nameof(GameObject)} {prefabGameObject.name} does not have a {nameof(NetworkObject)} component!"); return SpawnObject(prefabNetworkObject, owner, destroyWithScene); } /// /// Spawn a NetworkObject prefab instance /// /// the prefab NetworkObject to spawn /// the owner of the instance /// default is false /// GameObject instance spawned private GameObject SpawnObject(NetworkObject prefabNetworkObject, NetworkManager owner, bool destroyWithScene = false) { Assert.IsTrue(prefabNetworkObject.GlobalObjectIdHash > 0, $"{nameof(GameObject)} {prefabNetworkObject.name} has a {nameof(NetworkObject.GlobalObjectIdHash)} value of 0! Make sure to make it a valid prefab before trying to spawn!"); var newInstance = Object.Instantiate(prefabNetworkObject.gameObject); var networkObjectToSpawn = newInstance.GetComponent(); networkObjectToSpawn.NetworkManagerOwner = m_ServerNetworkManager; // Required to assure the server does the spawning if (owner == m_ServerNetworkManager) { if (m_UseHost) { networkObjectToSpawn.SpawnWithOwnership(owner.LocalClientId, destroyWithScene); } else { networkObjectToSpawn.Spawn(destroyWithScene); } } else { networkObjectToSpawn.SpawnWithOwnership(owner.LocalClientId, destroyWithScene); } return newInstance; } /// /// Overloaded method /// protected List SpawnObjects(GameObject prefabGameObject, NetworkManager owner, int count, bool destroyWithScene = false) { var prefabNetworkObject = prefabGameObject.GetComponent(); Assert.IsNotNull(prefabNetworkObject, $"{nameof(GameObject)} {prefabGameObject.name} does not have a {nameof(NetworkObject)} component!"); return SpawnObjects(prefabNetworkObject, owner, count, destroyWithScene); } /// /// Will spawn (x) number of prefab NetworkObjects /// /// /// the prefab NetworkObject to spawn /// the owner of the instance /// number of instances to create and spawn /// default is false private List SpawnObjects(NetworkObject prefabNetworkObject, NetworkManager owner, int count, bool destroyWithScene = false) { var gameObjectsSpawned = new List(); for (int i = 0; i < count; i++) { gameObjectsSpawned.Add(SpawnObject(prefabNetworkObject, owner, destroyWithScene)); } return gameObjectsSpawned; } /// /// Default constructor /// public NetcodeIntegrationTest() { } /// /// Optional Host or Server integration tests /// Constructor that allows you To break tests up as a host /// and a server. /// Example: Decorate your child derived class with TestFixture /// and then create a constructor at the child level. /// Don't forget to set your constructor public, else Unity will /// give you a hard to decipher error /// [TestFixture(HostOrServer.Host)] /// [TestFixture(HostOrServer.Server)] /// public class MyChildClass : NetcodeIntegrationTest /// { /// public MyChildClass(HostOrServer hostOrServer) : base(hostOrServer) { } /// } /// /// public NetcodeIntegrationTest(HostOrServer hostOrServer) { m_UseHost = hostOrServer == HostOrServer.Host ? true : false; } /// /// Just a helper function to avoid having to write the entire assert just to check if you /// timed out. /// protected void AssertOnTimeout(string timeOutErrorMessage, TimeoutHelper assignedTimeoutHelper = null) { var timeoutHelper = assignedTimeoutHelper != null ? assignedTimeoutHelper : s_GlobalTimeoutHelper; Assert.False(timeoutHelper.TimedOut, timeOutErrorMessage); } private void UnloadRemainingScenes() { // Unload any remaining scenes loaded but the test runner scene // Note: Some tests only unload the server-side instance, and this // just assures no currently loaded scenes will impact the next test for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); if (!scene.IsValid() || !scene.isLoaded || scene.name.Contains(NetcodeIntegrationTestHelpers.FirstPartOfTestRunnerSceneName) || !OnCanSceneCleanUpUnload(scene)) { continue; } VerboseDebug($"Unloading scene {scene.name}-{scene.handle}"); var asyncOperation = SceneManager.UnloadSceneAsync(scene); } } } }