diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index a9f91da961..c34f7f1d90 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -18,6 +18,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed issue where non-authority `NetworkTransform` instances would not allow non-synchronized axis values to be updated locally. (#3471) - Fixed issue where invoking `NetworkObject.NetworkShow` and `NetworkObject.ChangeOwnership` consecutively within the same call stack location could result in an unnecessary change in ownership error message generated on the target client side. (#3468) - Fixed issue where `NetworkVariable`s on a `NetworkBehaviour` could fail to synchronize changes if one has `NetworkVariableUpdateTraits` set and is dirty but is not ready to send. (#3466) +- Fixed issue with the Distributed Authority connection sequence with scene management enabled where the `ClientConnected` event was fired before the client was synchronized. (#3459) - Fixed inconsistencies in the `OnSceneEvent` callback. (#3458) - Fixed issues with the `NetworkBehaviour` and `NetworkVariable` length safety checks. (#3405) - Fixed memory leaks when domain reload is disabled. (#3427) diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/SessionConfig.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/SessionConfig.cs index aabc18dfc1..feba82d994 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/SessionConfig.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/SessionConfig.cs @@ -10,9 +10,10 @@ internal class SessionConfig public const uint ServerDistributionCompatible = 2; public const uint SessionStateToken = 3; public const uint NetworkBehaviourSerializationSafety = 4; + public const uint FixConnectionFlow = 5; // The most current session version (!!!!set this when you increment!!!!!) - public static uint PackageSessionVersion => NetworkBehaviourSerializationSafety; + public static uint PackageSessionVersion => FixConnectionFlow; internal uint SessionVersion; diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 59a187a5fc..4d48074d3f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -133,48 +133,49 @@ internal void InvokeOnClientConnectedCallback(ulong clientId) Debug.LogException(exception); } - if (!NetworkManager.IsServer) + if (NetworkManager.IsServer || NetworkManager.LocalClient.IsSessionOwner) { - var peerClientIds = new NativeArray(Math.Max(NetworkManager.ConnectedClientsIds.Count - 1, 0), Allocator.Temp); - // `using var peerClientIds` or `using(peerClientIds)` renders it immutable... - using var sentinel = peerClientIds; - - var idx = 0; - foreach (var peerId in NetworkManager.ConnectedClientsIds) - { - if (peerId == NetworkManager.LocalClientId) - { - continue; - } - - // This assures if the server has not timed out prior to the client synchronizing that it doesn't exceed the allocated peer count. - if (peerClientIds.Length > idx) - { - peerClientIds[idx] = peerId; - ++idx; - } - } - try { - OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = NetworkManager.LocalClientId, EventType = ConnectionEvent.ClientConnected, PeerClientIds = peerClientIds }); + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.ClientConnected }); } catch (Exception exception) { Debug.LogException(exception); } + + return; } - else + + // Invoking connection event on non-authority local client. Need to calculate PeerIds. + var peerClientIds = new NativeArray(Math.Max(NetworkManager.ConnectedClientsIds.Count - 1, 0), Allocator.Temp); + // `using var peerClientIds` or `using(peerClientIds)` renders it immutable... + using var sentinel = peerClientIds; + + var idx = 0; + foreach (var peerId in NetworkManager.ConnectedClientsIds) { - try + if (peerId == NetworkManager.LocalClientId) { - OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.ClientConnected }); + continue; } - catch (Exception exception) + + // This assures if the server has not timed out prior to the client synchronizing that it doesn't exceed the allocated peer count. + if (peerClientIds.Length > idx) { - Debug.LogException(exception); + peerClientIds[idx] = peerId; + ++idx; } } + + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = NetworkManager.LocalClientId, EventType = ConnectionEvent.ClientConnected, PeerClientIds = peerClientIds }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } } internal void InvokeOnClientDisconnectCallback(ulong clientId) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs index 2d01389cd0..1fa9a93e53 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs @@ -31,7 +31,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; - if (ShouldSynchronize && networkManager.NetworkConfig.EnableSceneManagement && networkManager.DistributedAuthorityMode && networkManager.LocalClient.IsSessionOwner) + if (ShouldSynchronize && networkManager.NetworkConfig.EnableSceneManagement && networkManager.DistributedAuthorityMode && !networkManager.CMBServiceConnection && networkManager.LocalClient.IsSessionOwner) { networkManager.SceneManager.SynchronizeNetworkObjects(ClientId); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs index 6cf0e413f0..7d374b321f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs @@ -25,6 +25,10 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; + if (networkManager.DistributedAuthorityMode && networkManager.CMBServiceConnection && networkManager.LocalClient.IsSessionOwner && networkManager.NetworkConfig.EnableSceneManagement) + { + networkManager.SceneManager.ClientConnectionQueue.Remove(ClientId); + } // All modes support removing NetworkClients networkManager.ConnectionManager.RemoveClient(ClientId); networkManager.ConnectionManager.ConnectedClientIds.Remove(ClientId); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 807429fdb8..02c63bfd34 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -246,10 +246,30 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; + + if (networkManager.CMBServiceConnection && networkManager.LocalClient.IsSessionOwner && networkManager.NetworkConfig.EnableSceneManagement) + { + if (networkManager.LocalClientId != OwnerClientId) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo($"[Session Owner] Received connection approved for Client-{OwnerClientId}! Synchronizing..."); + } + + networkManager.SceneManager.SynchronizeNetworkObjects(OwnerClientId); + } + else + { + NetworkLog.LogWarning($"[Client-{OwnerClientId}] Receiving duplicate connection approved. Client is already connected!"); + } + return; + } + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo($"[Client-{OwnerClientId}] Connection approved! Synchronizing..."); } + networkManager.LocalClientId = OwnerClientId; networkManager.MessageManager.SetLocalClientId(networkManager.LocalClientId); networkManager.NetworkMetrics.SetConnectionId(networkManager.LocalClientId); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkManagerHooks.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkManagerHooks.cs index e8a60b9f28..05f0d2a2f0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkManagerHooks.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkManagerHooks.cs @@ -98,7 +98,8 @@ public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReade return false; } - if (m_NetworkManager.IsConnectedClient && messageType == typeof(ConnectionApprovedMessage)) + if (m_NetworkManager.IsConnectedClient && messageType == typeof(ConnectionApprovedMessage) && + !(m_NetworkManager.CMBServiceConnection && m_NetworkManager.LocalClient.IsSessionOwner && m_NetworkManager.NetworkConfig.EnableSceneManagement)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index d51360a852..275d7961bf 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1925,6 +1925,10 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) /// when many clients attempt to connect at the same time they will be /// handled sequentially so as to not saturate the session owner's maximum /// reliable messages. + /// DANGO-TODO: Get clients to track their synchronization status (if they haven't finished synchronizing) + /// - pending clients can listen to SessionOwnerChanged messages + /// - If the session owner changes, fire "some sort of event" to the service + /// - service can fire ConnectionApproved at new session owner /// internal List ClientConnectionQueue = new List(); @@ -2332,37 +2336,15 @@ private void HandleClientSceneEvent(uint sceneEventId) sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete; if (NetworkManager.DistributedAuthorityMode) { - if (NetworkManager.CMBServiceConnection) + sceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner; + sceneEventData.SenderClientId = NetworkManager.LocalClientId; + var message = new SceneEventMessage { - foreach (var clientId in NetworkManager.ConnectedClientsIds) - { - if (clientId == NetworkManager.LocalClientId) - { - continue; - } - sceneEventData.TargetClientId = clientId; - sceneEventData.SenderClientId = NetworkManager.LocalClientId; - var message = new SceneEventMessage - { - EventData = sceneEventData, - }; - var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); - NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); - } - } - else - { - sceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner; - sceneEventData.SenderClientId = NetworkManager.LocalClientId; - var message = new SceneEventMessage - { - EventData = sceneEventData, - }; - var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); - NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); - } + EventData = sceneEventData, + }; + var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; + var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); + NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); } else { @@ -2485,13 +2467,13 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) // Mark this client as being connected NetworkManager.ConnectedClients[clientId].IsConnected = true; - // Notify the local server that a client has finished synchronizing + // Notify that a client has finished synchronizing OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, - SceneName = string.Empty, ClientId = clientId }); + OnSynchronizeComplete?.Invoke(clientId); // For non-authority clients in a distributed authority session, we show hidden objects, // we distribute NetworkObjects, and then we end the scene event. @@ -2511,9 +2493,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) return; } - // All scenes are synchronized, let the server know we are done synchronizing - OnSynchronizeComplete?.Invoke(clientId); - // At this time the client is fully synchronized with all loaded scenes and // NetworkObjects and should be considered "fully connected". Send the // notification that the client is connected. @@ -2555,26 +2534,12 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) // Remove the client that just synchronized ClientConnectionQueue.Remove(clientId); - // If we have pending clients to synchronize, then make sure they are still connected - while (ClientConnectionQueue.Count > 0) - { - // If the next client is no longer connected then remove it from the list - if (!NetworkManager.ConnectedClientsIds.Contains(ClientConnectionQueue[0])) - { - ClientConnectionQueue.RemoveAt(0); - } - else - { - break; - } - } - // If we still have any pending clients waiting, then synchronize the next one if (ClientConnectionQueue.Count > 0) { if (NetworkManager.LogLevel <= LogLevel.Developer) { - Debug.Log($"Synchronizing Client-{ClientConnectionQueue[0]}..."); + Debug.Log($"Synchronizing deferred Client-{ClientConnectionQueue[0]}..."); } SynchronizeNetworkObjects(ClientConnectionQueue[0]); } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs new file mode 100644 index 0000000000..1096bdfe71 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.TestTools; + +namespace TestProject.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.DAHost)] + public class SceneManagementSynchronizationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + + public SceneManagementSynchronizationTests(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + private struct ExpectedEvent + { + public SceneEvent SceneEvent; + public ConnectionEventData ConnectionEvent; + } + + private readonly Queue m_ExpectedEventQueue = new(); + + private static int s_NumEventsProcessed; + + private void OnSceneEvent(SceneEvent sceneEvent) + { + VerboseDebug($"OnSceneEvent! Type: {sceneEvent.SceneEventType}."); + AssertEventMatchesExpectedEvent(expectedEvent => + ValidateSceneEventsAreEqual(expectedEvent, sceneEvent), sceneEvent.SceneEventType); + } + + private void OnConnectionEvent(NetworkManager manager, ConnectionEventData eventData) + { + VerboseDebug($"OnConnectionEvent! Type: {eventData.EventType} - Client-{eventData.ClientId}"); + AssertEventMatchesExpectedEvent(expectedEvent => + ValidateConnectionEventsAreEqual(expectedEvent, eventData), eventData.EventType); + } + + private void AssertEventMatchesExpectedEvent(Action predicate, T eventType) + { + if (m_ExpectedEventQueue.Count > 0) + { + var expectedEvent = m_ExpectedEventQueue.Dequeue(); + predicate(expectedEvent); + } + else + { + Assert.Fail($"Received unexpected event at index {s_NumEventsProcessed}: {eventType}"); + } + + s_NumEventsProcessed++; + } + + private NetworkManager m_ManagerToTest; + + private void SetManagerToTest(NetworkManager manager) + { + m_ManagerToTest = manager; + m_ManagerToTest.OnConnectionEvent += OnConnectionEvent; + m_ManagerToTest.SceneManager.OnSceneEvent += OnSceneEvent; + } + + protected override void OnNewClientStarted(NetworkManager networkManager) + { + // If m_ManagerToTest isn't set at this point, it means we are testing the newly created NetworkManager + if (m_ManagerToTest == null) + { + SetManagerToTest(networkManager); + } + base.OnNewClientCreated(networkManager); + } + + protected override IEnumerator OnTearDown() + { + m_ManagerToTest.OnConnectionEvent -= OnConnectionEvent; + m_ManagerToTest.SceneManager.OnSceneEvent -= OnSceneEvent; + m_ManagerToTest = null; + m_ExpectedEventQueue.Clear(); + s_NumEventsProcessed = 0; + + yield return base.OnTearDown(); + } + + [UnityTest] + public IEnumerator SynchronizationCallbacks_Authority() + { + SetManagerToTest(GetAuthorityNetworkManager()); + + // Calculate the expected ID of the newly connecting networkManager + var expectedClientId = GetNonAuthorityNetworkManager().LocalClientId + 1; + + // Setup expected events + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = SceneEventType.Synchronize, + ClientId = expectedClientId + }, + }); + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = SceneEventType.SynchronizeComplete, + ClientId = expectedClientId, + }, + }); + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + ConnectionEvent = new ConnectionEventData() + { + EventType = ConnectionEvent.ClientConnected, + ClientId = expectedClientId, + } + }); + + if (m_UseHost) + { + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + ConnectionEvent = new ConnectionEventData() + { + EventType = ConnectionEvent.PeerConnected, + ClientId = expectedClientId, + } + }); + } + + m_EnableVerboseDebug = true; + ////////////////////////////////////////// + // Testing event notifications + yield return CreateAndStartNewClient(); + yield return null; + + Assert.IsEmpty(m_ExpectedEventQueue, "Not all expected callbacks were received"); + } + + [UnityTest] + public IEnumerator SynchronizationCallbacks_NonAuthority() + { + var authorityId = GetAuthorityNetworkManager().LocalClientId; + var peerClientId = GetNonAuthorityNetworkManager().LocalClientId; + var expectedClientId = peerClientId + 1; + + var expectedPeerClientIds = m_UseHost ? new[] { authorityId, peerClientId } : new[] { peerClientId }; + + // Setup expected events + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + ConnectionEvent = new ConnectionEventData() + { + EventType = ConnectionEvent.ClientConnected, + ClientId = expectedClientId, + PeerClientIds = new NativeArray(expectedPeerClientIds.ToArray(), Allocator.Persistent), + } + }); + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = SceneEventType.SynchronizeComplete, + ClientId = expectedClientId, + }, + }); + + Assert.Null(m_ManagerToTest, "m_ManagerToTest should be null as we should be testing newly created client"); + + ////////////////////////////////////////// + // Testing event notifications + + // CreateAndStartNewClient will configure m_ManagerToTest inside OnNewClientStarted + yield return CreateAndStartNewClient(); + yield return null; + + Assert.IsEmpty(m_ExpectedEventQueue, "Not all expected callbacks were received"); + } + + [UnityTest] + public IEnumerator LateJoiningClient_PeerCallbacks() + { + var nonAuthority = GetNonAuthorityNetworkManager(); + var expectedClientId = nonAuthority.LocalClientId + 1; + SetManagerToTest(nonAuthority); + // Setup expected events + if (m_UseCmbService) + { + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = SceneEventType.SynchronizeComplete, + ClientId = expectedClientId, + }, + }); + } + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + ConnectionEvent = new ConnectionEventData() + { + EventType = ConnectionEvent.PeerConnected, + ClientId = expectedClientId, + } + }); + + ////////////////////////////////////////// + // Testing event notifications + yield return CreateAndStartNewClient(); + yield return null; + + Assert.IsEmpty(m_ExpectedEventQueue, "Not all expected callbacks were received"); + } + + private static void ValidateSceneEventsAreEqual(ExpectedEvent expectedEvent, SceneEvent sceneEvent) + { + Assert.NotNull(expectedEvent.SceneEvent, $"Received unexpected scene event {sceneEvent.SceneEventType} at index {s_NumEventsProcessed}"); + AssertField(expectedEvent.SceneEvent.SceneEventType, sceneEvent.SceneEventType, nameof(sceneEvent.SceneEventType), sceneEvent.SceneEventType); + AssertField(expectedEvent.SceneEvent.ClientId, sceneEvent.ClientId, nameof(sceneEvent.ClientId), sceneEvent.SceneEventType); + } + + private static void ValidateConnectionEventsAreEqual(ExpectedEvent expectedEvent, ConnectionEventData eventData) + { + Assert.NotNull(expectedEvent.ConnectionEvent, $"Received unexpected connection event {eventData.EventType} at index {s_NumEventsProcessed}"); + AssertField(expectedEvent.ConnectionEvent.EventType, eventData.EventType, nameof(eventData.EventType), eventData.EventType); + AssertField(expectedEvent.ConnectionEvent.ClientId, eventData.ClientId, nameof(eventData.ClientId), eventData.EventType); + + AssertField(expectedEvent.ConnectionEvent.PeerClientIds.Length, eventData.PeerClientIds.Length, "length of PeerClientIds", eventData.EventType); + if (eventData.PeerClientIds.Length > 0) + { + var peerIds = eventData.PeerClientIds.ToArray(); + foreach (var expectedClientId in expectedEvent.ConnectionEvent.PeerClientIds) + { + Assert.Contains(expectedClientId, peerIds, "PeerClientIds does not contain all expected client IDs."); + } + } + } + + private static void AssertField(T expected, T actual, string fieldName, TK type) + { + Assert.AreEqual(expected, actual, $"Failed on event {s_NumEventsProcessed} - {type}. Incorrect {fieldName}"); + } + + } +} diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs.meta new file mode 100644 index 0000000000..d55a1846fe --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d9899540f0bc4d4d9ea04614092d3696 +timeCreated: 1747946029 \ No newline at end of file