diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0fc62b2fa8..f05ecff6a7 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -12,6 +12,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where a spawned `NetworkObject` that was registered with a prefab handler and owned by a client would invoke destroy more than once on the host-server side if the client disconnected while the `NetworkObject` was still spawned. (#3202) - Fixed issue where `NetworkRigidBody2D` was still using the deprecated `isKinematic` property in Unity versions 2022.3 and newer. (#3199) - Fixed issue where an exception was thrown when calling `NetworkManager.Shutdown` after calling `UnityTransport.Shutdown`. (#3118) diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 8504ad065c..fb56d762e5 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -967,6 +967,11 @@ internal void OnClientDisconnectFromServer(ulong clientId) { if (NetworkManager.PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash)) { + // If the player is spawned, then despawn before invoking HandleNetworkPrefabDestroy + if (playerObject.IsSpawned) + { + NetworkManager.SpawnManager.DespawnObject(ConnectedClients[clientId].PlayerObject, false); + } NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject); } else if (playerObject.IsSpawned) @@ -984,40 +989,34 @@ internal void OnClientDisconnectFromServer(ulong clientId) // Get the NetworkObjects owned by the disconnected client var clientOwnedObjects = NetworkManager.SpawnManager.GetClientOwnedObjects(clientId); - if (clientOwnedObjects == null) + + // Handle despawn & destroy or change ownership + for (int i = clientOwnedObjects.Count - 1; i >= 0; i--) { - // This could happen if a client is never assigned a player object and is disconnected - // Only log this in verbose/developer mode - if (NetworkManager.LogLevel == LogLevel.Developer) + var ownedObject = clientOwnedObjects[i]; + if (!ownedObject) { - NetworkLog.LogWarning($"ClientID {clientId} disconnected with (0) zero owned objects! Was a player prefab not assigned?"); + continue; } - } - else - { - // Handle changing ownership and prefab handlers - for (int i = clientOwnedObjects.Count - 1; i >= 0; i--) + if (!ownedObject.DontDestroyWithOwner) { - var ownedObject = clientOwnedObjects[i]; - if (ownedObject != null) + if (NetworkManager.PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash)) { - if (!ownedObject.DontDestroyWithOwner) + if (ownedObject.IsSpawned) { - if (NetworkManager.PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash)) - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]); - } - else - { - Object.Destroy(ownedObject.gameObject); - } - } - else if (!NetworkManager.ShutdownInProgress) - { - ownedObject.RemoveOwnership(); + NetworkManager.SpawnManager.DespawnObject(ownedObject, false); } + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]); + } + else + { + Object.Destroy(ownedObject.gameObject); } } + else if (!NetworkManager.ShutdownInProgress) + { + ownedObject.RemoveOwnership(); + } } // TODO: Could(should?) be replaced with more memory per client, by storing the visibility diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index ca7c6d2478..b1c31976f8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -435,10 +435,15 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ var networkObject = networkPrefab; // Host spawns the ovveride and server spawns the original prefab unless forceOverride is set to true where both server or host will spawn the override. - if (forceOverride || NetworkManager.IsHost) + if (forceOverride || NetworkManager.IsHost || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) { networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation); } + else // Under this case, server instantiate the prefab passed in. + { + networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation); + } + if (networkObject == null) { Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); @@ -447,7 +452,15 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ networkObject.IsPlayerObject = isPlayerObject; networkObject.transform.position = position; networkObject.transform.rotation = rotation; - networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); + // If spawning as a player, then invoke SpawnAsPlayerObject + if (isPlayerObject) + { + networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene); + } + else // Otherwise just spawn with ownership + { + networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); + } return networkObject; } @@ -512,17 +525,38 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow } else { - // Create prefab instance - networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); - networkObject.transform.position = position ?? networkObject.transform.position; - networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; - networkObject.NetworkManagerOwner = NetworkManager; - networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash; + // Create prefab instance while applying any pre-assigned position and rotation values + networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation); } } return networkObject; } + /// + /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients + /// the instance. + /// !!! Should only be invoked by unless used by an integration test !!! + /// + /// + /// should be the base prefab value and not the + /// overrided value. + /// (Can be used for integration testing) + /// + /// prefab to instantiate + /// of the base prefab instance + /// conditional position in place of the network prefab's default position + /// conditional rotation in place of the network prefab's default rotation + /// the instance of the + internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) + { + var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); + networkObject.transform.position = position ?? networkObject.transform.position; + networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; + networkObject.NetworkManagerOwner = NetworkManager; + networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; + return networkObject; + } + /// /// Creates a local NetowrkObject to be spawned. /// diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index bf26f9f05a..5dcf5dc814 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -527,6 +527,29 @@ public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint } } + /// + /// Creates a to be used with integration testing + /// + /// namr of the object + /// owner of the object + /// when true, the instance is automatically migrated into the DDOL + /// + internal static GameObject CreateNetworkObject(string baseName, NetworkManager owner, bool moveToDDOL = false) + { + var gameObject = new GameObject + { + name = baseName + }; + var networkObject = gameObject.AddComponent(); + networkObject.NetworkManagerOwner = owner; + MakeNetworkObjectTestPrefab(networkObject); + if (moveToDDOL) + { + Object.DontDestroyOnLoad(gameObject); + } + return gameObject; + } + public static GameObject CreateNetworkObjectPrefab(string baseName, NetworkManager server, params NetworkManager[] clients) { void AddNetworkPrefab(NetworkConfig config, NetworkPrefab prefab) @@ -538,13 +561,7 @@ void AddNetworkPrefab(NetworkConfig config, NetworkPrefab prefab) Assert.IsNotNull(server, prefabCreateAssertError); Assert.IsFalse(server.IsListening, prefabCreateAssertError); - var gameObject = new GameObject - { - name = baseName - }; - var networkObject = gameObject.AddComponent(); - networkObject.NetworkManagerOwner = server; - MakeNetworkObjectTestPrefab(networkObject); + var gameObject = CreateNetworkObject(baseName, server); var networkPrefab = new NetworkPrefab() { Prefab = gameObject }; // We could refactor this test framework to share a NetworkPrefabList instance, but at this point it's diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs new file mode 100644 index 0000000000..a03632791e --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs @@ -0,0 +1,341 @@ +using System.Collections; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Integration test that validates spawning instances of s with overrides and + /// registered overrides. + /// + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.Host)] + internal class NetworkPrefabOverrideTests : NetcodeIntegrationTest + { + private const string k_PrefabRootName = "PrefabObj"; + protected override int NumberOfClients => 2; + + private NetworkPrefab m_ClientSidePlayerPrefab; + private NetworkPrefab m_PrefabOverride; + + public NetworkPrefabOverrideTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + /// + /// Prefab override handler that will instantiate the ServerSideInstance (m_PlayerPrefab) only on server instances + /// and will spawn the ClientSideInstance (m_ClientSidePlayerPrefab.Prefab) only on clients and/or a host. + /// + public class TestPrefabOverrideHandler : MonoBehaviour, INetworkPrefabInstanceHandler + { + public GameObject ServerSideInstance; + public GameObject ClientSideInstance; + private NetworkManager m_NetworkManager; + + private void Start() + { + m_NetworkManager = GetComponent(); + m_NetworkManager.PrefabHandler.AddHandler(ServerSideInstance, this); + } + + private void OnDestroy() + { + if (m_NetworkManager != null && m_NetworkManager.PrefabHandler != null) + { + m_NetworkManager.PrefabHandler.RemoveHandler(ServerSideInstance); + } + } + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) + { + var instance = m_NetworkManager.IsClient ? Instantiate(ClientSideInstance) : Instantiate(ServerSideInstance); + return instance.GetComponent(); + } + + public void Destroy(NetworkObject networkObject) + { + Object.Destroy(networkObject); + } + } + + + internal class SpawnDespawnDestroyNotifications : NetworkBehaviour + { + + public int Despawned { get; private set; } + public int Destroyed { get; private set; } + + private bool m_WasSpawned; + + private ulong m_LocalClientId; + + public override void OnNetworkSpawn() + { + m_WasSpawned = true; + m_LocalClientId = NetworkManager.LocalClientId; + base.OnNetworkSpawn(); + } + + public override void OnNetworkDespawn() + { + Assert.True(Destroyed == 0, $"{name} on client-{m_LocalClientId} should have a destroy invocation count of 0 but it is {Destroyed}!"); + Assert.True(Despawned == 0, $"{name} on client-{m_LocalClientId} should have a despawn invocation count of 0 but it is {Despawned}!"); + Despawned++; + base.OnNetworkDespawn(); + } + + public override void OnDestroy() + { + // When the original prefabs are destroyed, we want to ignore this check (those instances are never spawned) + if (m_WasSpawned) + { + Assert.True(Despawned == 1, $"{name} on client-{m_LocalClientId} should have a despawn invocation count of 1 but it is {Despawned}!"); + Assert.True(Destroyed == 0, $"{name} on client-{m_LocalClientId} should have a destroy invocation count of 0 but it is {Destroyed}!"); + } + Destroyed++; + + base.OnDestroy(); + } + } + + /// + /// Mock component for testing that the client-side player is using the right + /// network prefab. + /// + public class ClientSideOnlyComponent : MonoBehaviour + { + + } + + /// + /// When we create the player prefab, make a modified instance that will be used + /// with the . + /// + protected override void OnCreatePlayerPrefab() + { + var clientPlayer = Object.Instantiate(m_PlayerPrefab); + clientPlayer.AddComponent(); + Object.DontDestroyOnLoad(clientPlayer); + m_ClientSidePlayerPrefab = new NetworkPrefab() + { + Prefab = clientPlayer, + }; + + base.OnCreatePlayerPrefab(); + } + + /// + /// Add the additional s and s to + /// all instances. + /// + protected override void OnServerAndClientsCreated() + { + // Create a NetworkPrefab with an override + + var basePrefab = NetcodeIntegrationTestHelpers.CreateNetworkObject($"{k_PrefabRootName}-base", m_ServerNetworkManager, true); + basePrefab.AddComponent(); + var targetPrefab = NetcodeIntegrationTestHelpers.CreateNetworkObject($"{k_PrefabRootName}-over", m_ServerNetworkManager, true); + targetPrefab.AddComponent(); + m_PrefabOverride = new NetworkPrefab() + { + Prefab = basePrefab, + Override = NetworkPrefabOverride.Prefab, + SourcePrefabToOverride = basePrefab, + OverridingTargetPrefab = targetPrefab, + }; + + // Add the prefab override handler for instance specific player prefabs to the server side + var playerPrefabOverrideHandler = m_ServerNetworkManager.gameObject.AddComponent(); + playerPrefabOverrideHandler.ServerSideInstance = m_PlayerPrefab; + playerPrefabOverrideHandler.ClientSideInstance = m_ClientSidePlayerPrefab.Prefab; + + // Add the NetworkPrefab with override + m_ServerNetworkManager.NetworkConfig.Prefabs.Add(m_PrefabOverride); + // Add the client player prefab that will be used on clients (and the host) + m_ServerNetworkManager.NetworkConfig.Prefabs.Add(m_ClientSidePlayerPrefab); + + foreach (var networkManager in m_ClientNetworkManagers) + { + // Add the prefab override handler for instance specific player prefabs to the client side + playerPrefabOverrideHandler = networkManager.gameObject.AddComponent(); + playerPrefabOverrideHandler.ServerSideInstance = m_PlayerPrefab; + playerPrefabOverrideHandler.ClientSideInstance = m_ClientSidePlayerPrefab.Prefab; + + // Add the NetworkPrefab with override + networkManager.NetworkConfig.Prefabs.Add(m_PrefabOverride); + // Add the client player prefab that will be used on clients (and the host) + networkManager.NetworkConfig.Prefabs.Add(m_ClientSidePlayerPrefab); + } + + m_PrefabOverride.Prefab.GetComponent().IsSceneObject = false; + m_PrefabOverride.SourcePrefabToOverride.GetComponent().IsSceneObject = false; + m_PrefabOverride.OverridingTargetPrefab.GetComponent().IsSceneObject = false; + m_ClientSidePlayerPrefab.Prefab.GetComponent().IsSceneObject = false; + + base.OnServerAndClientsCreated(); + } + + protected override IEnumerator OnTearDown() + { + if (m_PrefabOverride != null) + { + if (m_PrefabOverride.SourcePrefabToOverride) + { + Object.Destroy(m_PrefabOverride.SourcePrefabToOverride); + } + + if (m_PrefabOverride.OverridingTargetPrefab) + { + Object.Destroy(m_PrefabOverride.OverridingTargetPrefab); + } + } + + if (m_ClientSidePlayerPrefab != null) + { + if (m_ClientSidePlayerPrefab.Prefab) + { + Object.Destroy(m_ClientSidePlayerPrefab.Prefab); + } + } + m_ClientSidePlayerPrefab = null; + m_PrefabOverride = null; + + yield return base.OnTearDown(); + } + + + private GameObject GetPlayerNetworkPrefabObject(NetworkManager networkManager) + { + return networkManager.IsClient ? m_ClientSidePlayerPrefab.Prefab : m_PlayerPrefab; + } + + [UnityTest] + public IEnumerator PrefabOverrideTests() + { + var prefabNetworkObject = (NetworkObject)null; + var spawnedGlobalObjectId = (uint)0; + + var networkManagers = m_ClientNetworkManagers.ToList(); + if (m_UseHost) + { + networkManagers.Insert(0, m_ServerNetworkManager); + } + else + { + // If running as just a server, validate that all player prefab clone instances are the server side version + prefabNetworkObject = GetPlayerNetworkPrefabObject(m_ServerNetworkManager).GetComponent(); + foreach (var playerEntry in m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId]) + { + spawnedGlobalObjectId = playerEntry.Value.GlobalObjectIdHash; + Assert.IsTrue(prefabNetworkObject.GlobalObjectIdHash == spawnedGlobalObjectId, $"Server-Side {playerEntry.Value.name} was spawned as prefab ({spawnedGlobalObjectId}) but we expected ({prefabNetworkObject.GlobalObjectIdHash})!"); + } + } + + // Validates prefab overrides via the NetworkPrefabHandler. + // Validate the player prefab instance clones relative to all NetworkManagers. + foreach (var networkManager in networkManagers) + { + // Get the expected player prefab to be spawned based on the NetworkManager + prefabNetworkObject = GetPlayerNetworkPrefabObject(networkManager).GetComponent(); + if (networkManager.IsClient) + { + spawnedGlobalObjectId = networkManager.LocalClient.PlayerObject.GlobalObjectIdHash; + Assert.IsTrue(prefabNetworkObject.GlobalObjectIdHash == spawnedGlobalObjectId, $"{networkManager.name} spawned player prefab ({spawnedGlobalObjectId}) did not match the expected one ({prefabNetworkObject.GlobalObjectIdHash})!"); + } + + foreach (var playerEntry in m_PlayerNetworkObjects[networkManager.LocalClientId]) + { + // We already checked our locally spawned player prefab above + if (playerEntry.Key == networkManager.LocalClientId) + { + continue; + } + spawnedGlobalObjectId = playerEntry.Value.GlobalObjectIdHash; + Assert.IsTrue(prefabNetworkObject.GlobalObjectIdHash == spawnedGlobalObjectId, $"Client-{networkManager.LocalClientId} clone of {playerEntry.Value.name} was spawned as prefab ({spawnedGlobalObjectId}) but we expected ({prefabNetworkObject.GlobalObjectIdHash})!"); + } + } + + // Validates prefab overrides via NetworkPrefab configuration. + var spawnedInstance = (NetworkObject)null; + var networkManagerOwner = m_ServerNetworkManager; + + // Clients and Host will spawn the OverridingTargetPrefab while a dedicated server will spawn the SourcePrefabToOverride + var expectedServerGlobalObjectIdHash = networkManagerOwner.IsClient ? m_PrefabOverride.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash : m_PrefabOverride.SourcePrefabToOverride.GetComponent().GlobalObjectIdHash; + var expectedClientGlobalObjectIdHash = m_PrefabOverride.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; + + spawnedInstance = NetworkObject.InstantiateAndSpawn(m_PrefabOverride.SourcePrefabToOverride, networkManagerOwner, networkManagerOwner.LocalClientId); + var builder = new StringBuilder(); + bool ObjectSpawnedOnAllNetworkMangers() + { + builder.Clear(); + if (!m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(spawnedInstance.NetworkObjectId)) + { + builder.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} failed to spawn {spawnedInstance.name}-{spawnedInstance.NetworkObjectId}!"); + return false; + } + var instanceGID = m_ServerNetworkManager.SpawnManager.SpawnedObjects[spawnedInstance.NetworkObjectId].GlobalObjectIdHash; + if (instanceGID != expectedServerGlobalObjectIdHash) + { + builder.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} instance {spawnedInstance.name}-{spawnedInstance.NetworkObjectId} GID is {instanceGID} but was expected to be {expectedServerGlobalObjectIdHash}!"); + return false; + } + + foreach (var networkManger in m_ClientNetworkManagers) + { + if (!networkManger.SpawnManager.SpawnedObjects.ContainsKey(spawnedInstance.NetworkObjectId)) + { + builder.AppendLine($"Client-{networkManger.LocalClientId} failed to spawn {spawnedInstance.name}-{spawnedInstance.NetworkObjectId}!"); + return false; + } + instanceGID = networkManger.SpawnManager.SpawnedObjects[spawnedInstance.NetworkObjectId].GlobalObjectIdHash; + if (instanceGID != expectedClientGlobalObjectIdHash) + { + builder.AppendLine($"Client-{networkManger.LocalClientId} instance {spawnedInstance.name}-{spawnedInstance.NetworkObjectId} GID is {instanceGID} but was expected to be {expectedClientGlobalObjectIdHash}!"); + return false; + } + } + return true; + } + + yield return WaitForConditionOrTimeOut(ObjectSpawnedOnAllNetworkMangers); + AssertOnTimeout($"The spawned prefab override validation failed!\n {builder}"); + + // Verify that the despawn and destroy order of operations is correct for client owned NetworkObjects and the nunmber of times each is invoked is correct + expectedServerGlobalObjectIdHash = networkManagerOwner.IsClient ? m_PrefabOverride.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash : m_PrefabOverride.SourcePrefabToOverride.GetComponent().GlobalObjectIdHash; + expectedClientGlobalObjectIdHash = m_PrefabOverride.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; + + spawnedInstance = NetworkObject.InstantiateAndSpawn(m_PrefabOverride.SourcePrefabToOverride, networkManagerOwner, m_ClientNetworkManagers[0].LocalClientId); + + + yield return WaitForConditionOrTimeOut(ObjectSpawnedOnAllNetworkMangers); + AssertOnTimeout($"The spawned prefab override validation failed!\n {builder}"); + var clientId = m_ClientNetworkManagers[0].LocalClientId; + m_ClientNetworkManagers[0].Shutdown(); + + // Wait until all of the client's owned objects are destroyed + // If no asserts occur, then the despawn & destroy order of operations and invocation count is correct + /// For more information look at: + bool ClientDisconnected(ulong clientId) + { + var clientOwnedObjects = m_ServerNetworkManager.SpawnManager.SpawnedObjects.Where((c) => c.Value.OwnerClientId == clientId).ToList(); + if (clientOwnedObjects.Count > 0) + { + return false; + } + + clientOwnedObjects = m_ClientNetworkManagers[1].SpawnManager.SpawnedObjects.Where((c) => c.Value.OwnerClientId == clientId).ToList(); + if (clientOwnedObjects.Count > 0) + { + return false; + } + return true; + } + + yield return WaitForConditionOrTimeOut(() => ClientDisconnected(clientId)); + AssertOnTimeout($"Timed out waiting for client to disconnect!"); + } + } +} + diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs.meta new file mode 100644 index 0000000000..390b8ba04e --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkPrefabOverrideTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9e880f2982b3ad4794454a95cea6582 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: