From 59f420beadd5c1a406658510bd95c2cd06f75eb4 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 5 Jun 2025 15:04:30 -0400 Subject: [PATCH 1/6] fix: [Backport] MTTB-1273 Inconsistent scene name in scene event --- .../SceneManagement/NetworkSceneManager.cs | 232 +++++--------- .../Runtime/NetcodeIntegrationTest.cs | 28 +- .../OnSceneEventCallbackTests.cs | 282 ++++++++++++++++++ .../OnSceneEventCallbackTests.cs.meta | 3 + 4 files changed, 388 insertions(+), 157 deletions(-) create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 1843c65e05..b9b8dc7f83 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Unity.Collections; using UnityEngine; using UnityEngine.SceneManagement; @@ -62,6 +63,20 @@ public class SceneEvent /// public string SceneName; + /// + /// This will be set to the path to the scene that the event pertains to.
+ /// This is set for the following s: + /// + /// + /// + /// + /// + /// + /// + /// + ///
+ public string ScenePath; + /// /// When a scene is loaded, the Scene structure is returned.
/// This is set for the following s: @@ -1125,24 +1140,7 @@ private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress size); // Send a local notification to the server that all clients are done loading or unloading - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventProgress.SceneEventType, - SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), - ClientId = NetworkManager.ServerClientId, - LoadSceneMode = sceneEventProgress.LoadSceneMode, - ClientsThatCompleted = clientsThatCompleted, - ClientsThatTimedOut = clientsThatTimedOut, - }); - - if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) - { - OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); - } - else - { - OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); - } + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData); EndSceneEvent(sceneEventData.SceneEventId); return true; @@ -1177,6 +1175,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene) Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!"); return SceneEventProgressStatus.InternalNetcodeError; } + sceneEventProgress.LoadSceneMode = LoadSceneMode.Additive; // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the @@ -1199,16 +1198,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene) sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress); // Notify local server that a scene is going to be unloaded - OnSceneEvent?.Invoke(new SceneEvent() - { - AsyncOperation = sceneUnload, - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = sceneName, - ClientId = NetworkManager.ServerClientId // Server can only invoke this - }); - - OnUnload?.Invoke(NetworkManager.ServerClientId, sceneName, sceneUnload); + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData); //Return the status return sceneEventProgress.Status; @@ -1265,16 +1255,7 @@ private void OnClientUnloadScene(uint sceneEventId) } // Notify the local client that a scene is going to be unloaded - OnSceneEvent?.Invoke(new SceneEvent() - { - AsyncOperation = sceneUnload, - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded - SceneName = sceneName, - ClientId = NetworkManager.LocalClientId // Server sent this message to the client, but client is executing it - }); - - OnUnload?.Invoke(NetworkManager.LocalClientId, sceneName, sceneUnload); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData); } /// @@ -1312,15 +1293,8 @@ private void OnSceneUnloaded(uint sceneEventId) sceneEventData.SceneEventType = SceneEventType.UnloadComplete; //Notify the client or server that a scene was unloaded - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = NetworkManager.IsServer ? NetworkManager.ServerClientId : NetworkManager.LocalClientId - }); - - OnUnloadComplete?.Invoke(NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash)); + var client = NetworkManager.IsServer ? NetworkManager.ServerClientId : NetworkManager.LocalClientId; + InvokeSceneEvents(client, sceneEventData); // Clients send a notification back to the server they have completed the unload scene event if (!NetworkManager.IsServer) @@ -1395,6 +1369,8 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName); sceneEventData.LoadSceneMode = loadSceneMode; var sceneEventId = sceneEventData.SceneEventId; + // LoadScene can be called with either a sceneName or a scenePath. Ensure that sceneName is correct at this point + sceneName = SceneNameFromHash(sceneEventData.SceneHash); // This both checks to make sure the scene is valid and if not resets the active scene event m_IsSceneEventActive = ValidateSceneBeforeLoading(sceneEventData.SceneHash, loadSceneMode); if (!m_IsSceneEventActive) @@ -1430,16 +1406,7 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify the local server that a scene loading event has begun - OnSceneEvent?.Invoke(new SceneEvent() - { - AsyncOperation = sceneLoad, - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = sceneName, - ClientId = NetworkManager.ServerClientId - }); - - OnLoad?.Invoke(NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData, sceneLoad); //Return our scene progress instance return sceneEventProgress.Status; @@ -1521,6 +1488,7 @@ private void SceneUnloaded(Scene scene) AsyncOperation = m_AsyncOperation, SceneEventType = SceneEventType.UnloadComplete, SceneName = m_Scene.name, + ScenePath = m_Scene.path, LoadSceneMode = m_LoadSceneMode, ClientId = m_ClientId }); @@ -1545,6 +1513,7 @@ private SceneUnloadEventHandler(NetworkSceneManager networkSceneManager, Scene s AsyncOperation = m_AsyncOperation, SceneEventType = SceneEventType.Unload, SceneName = m_Scene.name, + ScenePath = m_Scene.path, LoadSceneMode = m_LoadSceneMode, ClientId = clientId }); @@ -1599,16 +1568,7 @@ private void OnClientSceneLoadingEvent(uint sceneEventId) }; var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress); - OnSceneEvent?.Invoke(new SceneEvent() - { - AsyncOperation = sceneLoad, - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = sceneName, - ClientId = NetworkManager.LocalClientId - }); - - OnLoad?.Invoke(NetworkManager.LocalClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneLoad); } /// @@ -1723,17 +1683,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) } m_IsSceneEventActive = false; + sceneEventData.SceneEventType = SceneEventType.LoadComplete; //First, notify local server that the scene was loaded - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = SceneEventType.LoadComplete, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = NetworkManager.ServerClientId, - Scene = scene, - }); - - OnLoadComplete?.Invoke(NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData, scene: scene); //Second, only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && NetworkManager.IsHost) @@ -1760,16 +1712,7 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) ProcessDeferredCreateObjectMessages(); // Notify local client that the scene was loaded - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = SceneEventType.LoadComplete, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = NetworkManager.LocalClientId, - Scene = scene, - }); - - OnLoadComplete?.Invoke(NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, scene: scene); EndSceneEvent(sceneEventId); } @@ -1925,16 +1868,7 @@ private void OnClientBeginSync(uint sceneEventId) sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify local client that a scene load has begun - OnSceneEvent?.Invoke(new SceneEvent() - { - AsyncOperation = sceneLoad, - SceneEventType = SceneEventType.Load, - LoadSceneMode = loadSceneMode, - SceneName = sceneName, - ClientId = NetworkManager.LocalClientId, - }); - - OnLoad?.Invoke(NetworkManager.LocalClientId, sceneName, loadSceneMode, sceneLoad); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneLoad); } else { @@ -1999,16 +1933,7 @@ private void ClientLoadedSynchronization(uint sceneEventId) EndSceneEvent(responseSceneEventData.SceneEventId); // Send notification to local client that the scene has finished loading - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = SceneEventType.LoadComplete, - LoadSceneMode = loadSceneMode, - SceneName = sceneName, - Scene = nextScene, - ClientId = NetworkManager.LocalClientId, - }); - - OnLoadComplete?.Invoke(NetworkManager.LocalClientId, sceneName, loadSceneMode); + InvokeSceneEvents(NetworkManager.LocalClientId, responseSceneEventData); // Check to see if we still have scenes to load and synchronize with HandleClientSceneEvent(sceneEventId); @@ -2180,27 +2105,8 @@ private void HandleClientSceneEvent(uint sceneEventId) case SceneEventType.UnloadEventCompleted: { // Notify the local client that all clients have finished loading or unloading - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = NetworkManager.ServerClientId, - ClientsThatCompleted = sceneEventData.ClientsCompleted, - ClientsThatTimedOut = sceneEventData.ClientsTimedOut, - }); - - if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) - { - OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); - } - else - { - OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); - } - + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData); EndSceneEvent(sceneEventId); - break; } default: @@ -2221,41 +2127,15 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) switch (sceneEventData.SceneEventType) { case SceneEventType.LoadComplete: - { - // Notify the local server that the client has finished loading a scene - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = clientId - }); - - OnLoadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); - - if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) - { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); - } - EndSceneEvent(sceneEventId); - break; - } case SceneEventType.UnloadComplete: { + // Notify the local server that the client has finished unloading a scene + InvokeSceneEvents(clientId, sceneEventData); + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); } - // Notify the local server that the client has finished unloading a scene - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = clientId - }); - - OnUnloadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash)); EndSceneEvent(sceneEventId); break; @@ -2670,5 +2550,45 @@ private void ProcessDeferredCreateObjectMessages() DeferredObjectCreationCount = DeferredObjectCreationList.Count; DeferredObjectCreationList.Clear(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvokeSceneEvents(ulong clientId, SceneEventData eventData, AsyncOperation asyncOperation = null, Scene scene = default) + { + var sceneName = SceneNameFromHash(eventData.SceneHash); + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = asyncOperation, + SceneEventType = eventData.SceneEventType, + SceneName = sceneName, + ScenePath = ScenePathFromHash(eventData.SceneHash), + ClientId = clientId, + LoadSceneMode = eventData.LoadSceneMode, + ClientsThatCompleted = eventData.ClientsCompleted, + ClientsThatTimedOut = eventData.ClientsTimedOut, + Scene = scene, + }); + + switch (eventData.SceneEventType) + { + case SceneEventType.Load: + OnLoad?.Invoke(clientId, sceneName, eventData.LoadSceneMode, asyncOperation); + break; + case SceneEventType.Unload: + OnUnload?.Invoke(clientId, sceneName, asyncOperation); + break; + case SceneEventType.LoadComplete: + OnLoadComplete?.Invoke(clientId, sceneName, eventData.LoadSceneMode); + break; + case SceneEventType.UnloadComplete: + OnUnloadComplete?.Invoke(clientId, sceneName); + break; + case SceneEventType.LoadEventCompleted: + OnLoadEventCompleted?.Invoke(SceneNameFromHash(eventData.SceneHash), eventData.LoadSceneMode, eventData.ClientsCompleted, eventData.ClientsTimedOut); + break; + case SceneEventType.UnloadEventCompleted: + OnUnloadEventCompleted?.Invoke(SceneNameFromHash(eventData.SceneHash), eventData.LoadSceneMode, eventData.ClientsCompleted, eventData.ClientsTimedOut); + break; + } + } } } diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index f0056538ba..b9f0aa124b 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using NUnit.Framework; using Unity.Netcode.RuntimeTests; using Unity.Netcode.Transports.UTP; @@ -36,6 +37,8 @@ public abstract class NetcodeIntegrationTest /// protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); + private readonly StringBuilder m_InternalErrorLog = new StringBuilder(); + /// /// An instance of used to capture and assert log messages during integration tests. /// This helps in verifying that expected log messages are produced and unexpected log messages are not. @@ -1466,6 +1469,27 @@ public bool WaitForConditionOrTimeOutWithTimeTravel(IConditionalPredicate condit return success; } + /// + /// Waits until the specified condition returns true or a timeout occurs, then asserts if the timeout was reached. + /// This overload allows the condition to provide additional error details via a . + /// + /// A delegate that takes a for error details and returns true when the desired condition is met. + /// An optional to control the timeout period. If null, the default timeout is used. + /// An for use in Unity coroutines. + protected IEnumerator WaitForConditionOrTimeOut(Func checkForCondition, TimeoutHelper timeOutHelper = null) + { + if (checkForCondition == null) + { + throw new ArgumentNullException($"checkForCondition cannot be null!"); + } + + yield return WaitForConditionOrTimeOut(() => + { + m_InternalErrorLog.Clear(); + return checkForCondition(m_InternalErrorLog); + }, timeOutHelper); + } + /// /// Validation for clients connected. /// @@ -1736,7 +1760,9 @@ public NetcodeIntegrationTest(HostOrServer hostOrServer) protected void AssertOnTimeout(string timeOutErrorMessage, TimeoutHelper assignedTimeoutHelper = null) { var timeoutHelper = assignedTimeoutHelper ?? s_GlobalTimeoutHelper; - Assert.False(timeoutHelper.TimedOut, timeOutErrorMessage); + var internalError = m_InternalErrorLog.Length > 0 ? $"{timeOutErrorMessage}\n{m_InternalErrorLog}" : timeOutErrorMessage; + Assert.False(timeoutHelper.TimedOut, internalError); + m_InternalErrorLog.Clear(); } private void UnloadRemainingScenes() diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs new file mode 100644 index 0000000000..7853b40f16 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Unity.Netcode; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.SceneManagement; +using UnityEngine.TestTools; + +namespace TestProject.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class OnSceneEventCallbackTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + private const string k_SceneToLoad = "EmptyScene"; + private const string k_PathToLoad = "Assets/Scenes/EmptyScene.unity"; + + public OnSceneEventCallbackTests(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + private struct ExpectedEvent + { + public SceneEvent SceneEvent; + public string SceneName; + public string ScenePath; + } + + private readonly Queue m_ExpectedEventQueue = new(); + + private static int s_NumEventsProcessed; + private void OnSceneEvent(SceneEvent sceneEvent) + { + VerboseDebug($"OnSceneEvent! Type: {sceneEvent.SceneEventType}. for client: {sceneEvent.ClientId}"); + if (m_ExpectedEventQueue.Count > 0) + { + var expectedEvent = m_ExpectedEventQueue.Dequeue(); + + ValidateEventsAreEqual(expectedEvent.SceneEvent, sceneEvent); + + // Only LoadComplete events have an attached scene + if (sceneEvent.SceneEventType == SceneEventType.LoadComplete) + { + ValidateReceivedScene(expectedEvent, sceneEvent.Scene); + } + } + else + { + Assert.Fail($"Received unexpected event at index {s_NumEventsProcessed}: {sceneEvent.SceneEventType}"); + } + s_NumEventsProcessed++; + } + + public enum ClientType + { + Authority, + NonAuthority, + } + + public enum Action + { + Load, + Unload, + } + + [UnityTest] + public IEnumerator LoadAndUnloadCallbacks([Values] ClientType clientType, [Values] Action action) + { + yield return RunSceneEventCallbackTest(clientType, action, k_SceneToLoad); + } + + [UnityTest] + public IEnumerator LoadSceneFromPath([Values] ClientType clientType) + { + yield return RunSceneEventCallbackTest(clientType, Action.Load, k_PathToLoad); + } + + private IEnumerator RunSceneEventCallbackTest(ClientType clientType, Action action, string loadCall) + { + m_EnableVerboseDebug = true; + var client = m_ClientNetworkManagers[0]; + var managerToTest = clientType == ClientType.Authority ? m_ServerNetworkManager : client; + + + var expectedCompletedClients = new List { client.LocalClientId }; + // the authority ID is not inside ClientsThatCompleted when running as a server + if (m_UseHost) + { + expectedCompletedClients.Insert(0, m_ServerNetworkManager.LocalClientId); + } + + Scene loadedScene = default; + if (action == Action.Unload) + { + // Load the scene initially + m_ServerNetworkManager.SceneManager.LoadScene(k_SceneToLoad, LoadSceneMode.Additive); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsLoaded); + AssertOnTimeout($"[Setup] Timed out waiting for client to load the scene {k_SceneToLoad}!"); + + // Wait for any pending messages to be processed + yield return s_DefaultWaitForTick; + + // Get a reference to the scene to test + loadedScene = SceneManager.GetSceneByName(k_SceneToLoad); + } + + s_NumEventsProcessed = 0; + m_ExpectedEventQueue.Clear(); + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = action == Action.Load ? SceneEventType.Load : SceneEventType.Unload, + LoadSceneMode = LoadSceneMode.Additive, + SceneName = k_SceneToLoad, + ScenePath = k_PathToLoad, + ClientId = managerToTest.LocalClientId, + }, + }); + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = action == Action.Load ? SceneEventType.LoadComplete : SceneEventType.UnloadComplete, + LoadSceneMode = LoadSceneMode.Additive, + SceneName = k_SceneToLoad, + ScenePath = k_PathToLoad, + ClientId = managerToTest.LocalClientId, + }, + SceneName = action == Action.Load ? k_SceneToLoad : null, + ScenePath = action == Action.Load ? k_PathToLoad : null + }); + + if (clientType == ClientType.Authority) + { + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = action == Action.Load ? SceneEventType.LoadComplete : SceneEventType.UnloadComplete, + LoadSceneMode = LoadSceneMode.Additive, + SceneName = k_SceneToLoad, + ScenePath = k_PathToLoad, + ClientId = client.LocalClientId, + } + }); + } + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + SceneEvent = new SceneEvent() + { + SceneEventType = action == Action.Load ? SceneEventType.LoadEventCompleted : SceneEventType.UnloadEventCompleted, + LoadSceneMode = LoadSceneMode.Additive, + SceneName = k_SceneToLoad, + ScenePath = k_PathToLoad, + ClientId = m_ServerNetworkManager.LocalClientId, + ClientsThatCompleted = expectedCompletedClients, + ClientsThatTimedOut = new List() + } + }); + + ////////////////////////////////////////// + // Testing event notifications + managerToTest.SceneManager.OnSceneEvent += OnSceneEvent; + + if (action == Action.Load) + { + Assert.That(m_ServerNetworkManager.SceneManager.LoadScene(loadCall, LoadSceneMode.Additive) == SceneEventProgressStatus.Started); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsLoaded); + AssertOnTimeout($"[Test] Timed out waiting for client to load the scene {k_SceneToLoad}!"); + } + else + { + Assert.That(loadedScene.name, Is.EqualTo(k_SceneToLoad), "scene was not loaded!"); + Assert.That(m_ServerNetworkManager.SceneManager.UnloadScene(loadedScene) == SceneEventProgressStatus.Started); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsUnloaded); + AssertOnTimeout($"[Test] Timed out waiting for client to unload the scene {k_SceneToLoad}!"); + } + + // Wait for all messages to process + yield return s_DefaultWaitForTick; + + if (m_ExpectedEventQueue.Count > 0) + { + Assert.Fail($"Failed to invoke all expected OnSceneEvent callbacks. {m_ExpectedEventQueue.Count} callbacks missing. First missing event is {m_ExpectedEventQueue.Dequeue().SceneEvent.SceneEventType}"); + } + + managerToTest.SceneManager.OnSceneEvent -= OnSceneEvent; + } + + private bool ValidateSceneIsLoaded(StringBuilder errorBuilder) + { + var loadedScene = m_ServerNetworkManager.SceneManager.ScenesLoaded.Values.FirstOrDefault(scene => scene.name == k_SceneToLoad); + if (!loadedScene.isLoaded) + { + errorBuilder.AppendLine($"[ValidateIsLoaded] Scene {loadedScene.name} exists but is not loaded!"); + return false; + } + if (m_ServerNetworkManager.SceneManager.SceneEventProgressTracking.Count > 0) + { + errorBuilder.AppendLine($"[ValidateIsLoaded] Server NetworkManager still has progress tracking events."); + return false; + } + + foreach (var manager in m_ClientNetworkManagers) + { + // default will have isLoaded as false so we can get the scene or default and test on isLoaded + loadedScene = manager.SceneManager.ScenesLoaded.Values.FirstOrDefault(scene => scene.name == k_SceneToLoad); + if (!loadedScene.isLoaded) + { + errorBuilder.AppendLine($"[ValidateIsLoaded] Scene {loadedScene.name} exists but is not loaded!"); + return false; + } + + if (manager.SceneManager.SceneEventProgressTracking.Count > 0) + { + errorBuilder.AppendLine($"[ValidateIsLoaded] Client-{manager.name} still has progress tracking events."); + return false; + } + } + + return true; + } + + private bool ValidateSceneIsUnloaded() + { + if (m_ServerNetworkManager.SceneManager.ScenesLoaded.Values.Any(scene => scene.name == k_SceneToLoad)) + { + return false; + } + if (m_ServerNetworkManager.SceneManager.SceneEventProgressTracking.Count > 0) + { + return false; + } + + foreach (var manager in m_ClientNetworkManagers) + { + if (manager.SceneManager.ScenesLoaded.Values.Any(scene => scene.name == k_SceneToLoad)) + { + return false; + } + + if (manager.SceneManager.SceneEventProgressTracking.Count > 0) + { + return false; + } + } + return true; + } + + private static void ValidateEventsAreEqual(SceneEvent expectedEvent, SceneEvent sceneEvent) + { + AssertField(expectedEvent.SceneEventType, sceneEvent.SceneEventType, nameof(sceneEvent.SceneEventType), sceneEvent.SceneEventType); + AssertField(expectedEvent.LoadSceneMode, sceneEvent.LoadSceneMode, nameof(sceneEvent.LoadSceneMode), sceneEvent.SceneEventType); + AssertField(expectedEvent.SceneName, sceneEvent.SceneName, nameof(sceneEvent.SceneName), sceneEvent.SceneEventType); + AssertField(expectedEvent.ClientId, sceneEvent.ClientId, nameof(sceneEvent.ClientId), sceneEvent.SceneEventType); + AssertField(expectedEvent.ClientsThatCompleted, sceneEvent.ClientsThatCompleted, nameof(sceneEvent.SceneEventType), sceneEvent.SceneEventType); + AssertField(expectedEvent.ClientsThatTimedOut, sceneEvent.ClientsThatTimedOut, nameof(sceneEvent.ClientsThatTimedOut), sceneEvent.SceneEventType); + } + + // The LoadCompleted event includes the scene being loaded + private static void ValidateReceivedScene(ExpectedEvent expectedEvent, Scene scene) + { + AssertField(expectedEvent.SceneName, scene.name, "Scene.name", SceneEventType.LoadComplete); + AssertField(expectedEvent.ScenePath, scene.path, "Scene.path", SceneEventType.LoadComplete); + } + + private static void AssertField(T expected, T actual, string fieldName, SceneEventType type) + { + Assert.AreEqual(expected, actual, $"Failed on event {s_NumEventsProcessed} - {type}: Expected {fieldName} to be {expected}. Found {actual}"); + } + } +} diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs.meta new file mode 100644 index 0000000000..8ea129e864 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a74db0da82e045c3ba13f46688486efd +timeCreated: 1748970897 \ No newline at end of file From 35ae221c7cc2a5ae1a9bd51b8e4ff3b147a75c08 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 5 Jun 2025 15:06:36 -0400 Subject: [PATCH 2/6] Update CHANGELOG.md --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 3d8cc3cc01..6ed948b721 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -15,6 +15,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed inconsistencies in the `OnSceneEvent` callback. (#3487) - 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. (#3465) - Fixed issue where when a client changes ownership via RPC the `NetworkBehaviour.OnOwnershipChanged` can result in identical previous and current owner identifiers. (#3434) From 85930e09fc9c5acf6cd7726366355909c396b6a2 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 9 Jun 2025 13:55:19 -0500 Subject: [PATCH 3/6] style Removing the System.Text from StringBuilder since System.Text is included in a using directive. --- .../TestHelpers/Runtime/NetcodeIntegrationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index b9f0aa124b..6e5e50fa71 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -1783,7 +1783,7 @@ private void UnloadRemainingScenes() } } - private System.Text.StringBuilder m_WaitForLog = new System.Text.StringBuilder(); + private StringBuilder m_WaitForLog = new StringBuilder(); private void LogWaitForMessages() { From 0b311e7f8ff510996ae292caf929ab06b7518e3e Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 9 Jun 2025 14:06:00 -0500 Subject: [PATCH 4/6] update removing using directive that is not needed. --- .../Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs index 7853b40f16..0e8f010623 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; From 691750d1eca303011c54279b437c4dd4206c5e63 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 10 Jun 2025 14:54:56 -0400 Subject: [PATCH 5/6] Backport fixes --- .../SceneManagement/NetworkSceneManager.cs | 17 ++++++++++++++--- .../Runtime/NetcodeIntegrationTest.cs | 11 ++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index b9b8dc7f83..ce41e93e70 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -724,6 +724,14 @@ internal string ScenePathFromHash(uint sceneHash) } else { + // In the event there is no scene associated with the scene event then just return "No Scene" + // This can happen during unit tests when clients first connect and the only scene loaded is the + // unit test scene (which is ignored by default) that results in a scene event that has no associated + // scene. Under this specific special case, we just return "No Scene". + if (sceneHash == 0) + { + return "No Scene"; + } throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table! Verify that all scenes requiring" + $" server to client synchronization are in the scenes in build list."); } @@ -1198,7 +1206,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene) sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress); // Notify local server that a scene is going to be unloaded - InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData); + InvokeSceneEvents(NetworkManager.ServerClientId, sceneEventData, sceneUnload); //Return the status return sceneEventProgress.Status; @@ -1254,8 +1262,11 @@ private void OnClientUnloadScene(uint sceneEventId) throw new Exception($"Failed to remove server scene handle ({sceneEventData.SceneHandle}) or client scene handle({sceneHandle})! Happened during scene unload for {sceneName}."); } + // The only scenes unloaded are scenes that were additively loaded + sceneEventData.LoadSceneMode = LoadSceneMode.Additive; + // Notify the local client that a scene is going to be unloaded - InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneUnload); } /// @@ -1933,7 +1944,7 @@ private void ClientLoadedSynchronization(uint sceneEventId) EndSceneEvent(responseSceneEventData.SceneEventId); // Send notification to local client that the scene has finished loading - InvokeSceneEvents(NetworkManager.LocalClientId, responseSceneEventData); + InvokeSceneEvents(NetworkManager.LocalClientId, responseSceneEventData, scene: nextScene); // Check to see if we still have scenes to load and synchronize with HandleClientSceneEvent(sceneEventId); diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 6e5e50fa71..49e7142df7 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -1760,9 +1760,14 @@ public NetcodeIntegrationTest(HostOrServer hostOrServer) protected void AssertOnTimeout(string timeOutErrorMessage, TimeoutHelper assignedTimeoutHelper = null) { var timeoutHelper = assignedTimeoutHelper ?? s_GlobalTimeoutHelper; - var internalError = m_InternalErrorLog.Length > 0 ? $"{timeOutErrorMessage}\n{m_InternalErrorLog}" : timeOutErrorMessage; - Assert.False(timeoutHelper.TimedOut, internalError); - m_InternalErrorLog.Clear(); + if (m_InternalErrorLog.Length > 0) + { + Assert.False(timeoutHelper.TimedOut, $"{timeOutErrorMessage}\n{m_InternalErrorLog}"); + m_InternalErrorLog.Clear(); + return; + } + + Assert.False(timeoutHelper.TimedOut, timeOutErrorMessage); } private void UnloadRemainingScenes() From 64936898d9c56fef36f655448fd59ba143422ff4 Mon Sep 17 00:00:00 2001 From: Emma Date: Wed, 11 Jun 2025 13:52:18 -0400 Subject: [PATCH 6/6] Fix OnClientBeginSync --- .../SceneManagement/NetworkSceneManager.cs | 12 +- .../SceneManagementSynchronizationTests.cs | 246 ++++++++++++++++++ ...ceneManagementSynchronizationTests.cs.meta | 3 + 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index ce41e93e70..243502c3d6 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1879,7 +1879,17 @@ private void OnClientBeginSync(uint sceneEventId) sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify local client that a scene load has begun - InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneLoad); + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneLoad, + SceneEventType = SceneEventType.Load, + LoadSceneMode = loadSceneMode, + SceneName = sceneName, + ScenePath = ScenePathFromHash(sceneHash), + ClientId = NetworkManager.LocalClientId, + }); + + OnLoad?.Invoke(NetworkManager.LocalClientId, sceneName, loadSceneMode, sceneLoad); } else { diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs new file mode 100644 index 0000000000..3c08910299 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs @@ -0,0 +1,246 @@ +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)] + 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(m_ServerNetworkManager); + + // Calculate the expected ID of the newly connecting networkManager + var expectedClientId = m_ClientNetworkManagers[0].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 s_DefaultWaitForTick; + + if (m_ExpectedEventQueue.Count > 0) + { + Assert.Fail($"Failed to invoke all expected callbacks. {m_ExpectedEventQueue.Count} callbacks missing. First missing event is {m_ExpectedEventQueue.Dequeue().SceneEvent.SceneEventType}"); + } + } + + [UnityTest] + public IEnumerator SynchronizationCallbacks_NonAuthority() + { + var authorityId = m_ServerNetworkManager.LocalClientId; + var peerClientId = m_ClientNetworkManagers[0].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 s_DefaultWaitForTick; + + Assert.IsEmpty(m_ExpectedEventQueue, "Not all expected callbacks were received"); + } + + [UnityTest] + public IEnumerator LateJoiningClient_PeerCallbacks() + { + var expectedClientId = m_ClientNetworkManagers[0].LocalClientId + 1; + SetManagerToTest(m_ClientNetworkManagers[0]); + + m_ExpectedEventQueue.Enqueue(new ExpectedEvent() + { + ConnectionEvent = new ConnectionEventData() + { + EventType = ConnectionEvent.PeerConnected, + ClientId = expectedClientId, + } + }); + + ////////////////////////////////////////// + // Testing event notifications + yield return CreateAndStartNewClient(); + yield return s_DefaultWaitForTick; + + 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..f1eda32abc --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneManagementSynchronizationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ae2f461cb3da41d3ac68f77bf83f944a +timeCreated: 1749663194 \ No newline at end of file