diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 3dd4001177..d9f395baa2 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- 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) - Fixed an exception being thrown when unregistering a custom message handler from within the registered callback. (#3417) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index b904056c8a..b27c1ddb71 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: @@ -1056,43 +1071,18 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds) var sceneEvent = SceneEventDataStore[sceneEventId]; sceneEvent.SenderClientId = NetworkManager.LocalClientId; - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) - { - if (NetworkManager.DistributedAuthorityMode && HasSceneAuthority()) - { - sceneEvent.TargetClientId = NetworkManager.ServerClientId; - var message = new SceneEventMessage - { - EventData = sceneEvent, - }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); - NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); - } - foreach (var clientId in targetClientIds) - { - sceneEvent.TargetClientId = clientId; - var message = new SceneEventMessage - { - EventData = sceneEvent, - }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); - NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); - } - } - else + // Send to each individual client to assure only the in-scene placed NetworkObjects being observed by the client + // is serialized + foreach (var clientId in targetClientIds) { - // Send to each individual client to assure only the in-scene placed NetworkObjects being observed by the client - // is serialized - foreach (var clientId in targetClientIds) + sceneEvent.TargetClientId = clientId; + var message = new SceneEventMessage { - sceneEvent.TargetClientId = clientId; - var message = new SceneEventMessage - { - EventData = sceneEvent, - }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, clientId); - NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); - } + EventData = sceneEvent, + }; + var sendTarget = NetworkManager.CMBServiceConnection ? NetworkManager.ServerClientId : clientId; + var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, sendTarget); + NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); } } @@ -1233,24 +1223,7 @@ private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress } // Send a local notification to the session owner that all clients are done loading or unloading - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = sceneEventProgress.SceneEventType, - SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), - ClientId = NetworkManager.CurrentSessionOwner, - 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.CurrentSessionOwner, sceneEventData); EndSceneEvent(sceneEventData.SceneEventId); return true; @@ -1268,7 +1241,6 @@ public SceneEventProgressStatus UnloadScene(Scene scene) var sceneName = scene.name; var sceneHandle = scene.handle; - if (!scene.isLoaded) { Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); @@ -1295,6 +1267,8 @@ public SceneEventProgressStatus UnloadScene(Scene scene) } } + 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 // currently active scene. @@ -1322,16 +1296,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene) 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.LocalClientId // Session owner can only invoke this - }); - - OnUnload?.Invoke(NetworkManager.LocalClientId, sceneName, sceneUnload); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneUnload); //Return the status return sceneEventProgress.Status; @@ -1393,16 +1358,12 @@ 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); + // The only scenes unloaded are scenes that were additively loaded + sceneEventData.LoadSceneMode = LoadSceneMode.Additive; + + // Server sent this message to the client, but client is executing it + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneUnload); } /// @@ -1447,15 +1408,7 @@ 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.LocalClientId, - }); - - OnUnloadComplete?.Invoke(NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash)); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData); if (!HasSceneAuthority()) { @@ -1547,6 +1500,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) @@ -1583,15 +1538,7 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc 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.LocalClientId - }); - OnLoad?.Invoke(NetworkManager.LocalClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + InvokeSceneEvents(NetworkManager.LocalClientId, sceneEventData, sceneLoad); //Return our scene progress instance return sceneEventProgress.Status; @@ -1673,6 +1620,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 }); @@ -1697,6 +1645,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 }); @@ -1758,16 +1707,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); } /// @@ -1905,17 +1845,8 @@ private void OnSessionOwnerLoadedScene(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 = sceneEventData.SceneEventType, - LoadSceneMode = sceneEventData.LoadSceneMode, - SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = NetworkManager.LocalClientId, - Scene = scene, - }); - - OnLoadComplete?.Invoke(NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + // First, notify local server that the scene was loaded + InvokeSceneEvents(NetworkManager.LocalClientId, 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.IsClient) @@ -1965,16 +1896,7 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) } // 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); } @@ -2204,6 +2126,7 @@ private void OnClientBeginSync(uint sceneEventId) SceneEventType = SceneEventType.Load, LoadSceneMode = loadSceneMode, SceneName = sceneName, + ScenePath = ScenePathFromHash(sceneHash), ClientId = NetworkManager.LocalClientId, }); @@ -2278,16 +2201,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, scene: nextScene); // Check to see if we still have scenes to load and synchronize with HandleClientSceneEvent(sceneEventId); @@ -2512,27 +2426,10 @@ 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); - } + var clientId = NetworkManager.CMBServiceConnection ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; + InvokeSceneEvents(clientId, sceneEventData); EndSceneEvent(sceneEventId); - break; } default: @@ -2553,42 +2450,15 @@ private void HandleSessionOwnerEvent(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; } @@ -3324,5 +3194,45 @@ public List GetSceneMapping(MapTypes mapType) return mapping; } + [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(NetworkManager.LocalClientId, 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/Runtime/SceneManagement/SceneEventProgress.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs index e9b52a218e..ebbb7b1bb9 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs @@ -124,7 +124,7 @@ internal List GetClientsWithStatus(bool completedSceneEvent) { // If we are the host, then add the host-client to the list // of clients that completed if the AsyncOperation is done. - if (m_NetworkManager.IsHost && m_AsyncOperation.isDone) + if ((m_NetworkManager.IsHost || m_NetworkManager.LocalClient.IsSessionOwner) && m_AsyncOperation.isDone) { clients.Add(m_NetworkManager.LocalClientId); } diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index ea3d919228..df262dea0c 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -1194,7 +1194,7 @@ protected void StartServerAndClientsWithTimeTravel() // Wait for all clients to connect WaitForClientsConnectedOrTimeOutWithTimeTravel(); - AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!"); + AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!\n {m_InternalErrorLog}"); if (m_UseHost || authorityManager.IsHost) { diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs new file mode 100644 index 0000000000..f6cfea5aa1 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs @@ -0,0 +1,264 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +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)] + [TestFixture(HostOrServer.DAHost)] + 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}."); + 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) + { + if (m_UseCmbService) + { + yield return s_DefaultWaitForTick; + } + + var authority = GetAuthorityNetworkManager(); + var nonAuthority = GetNonAuthorityNetworkManager(); + var managerToTest = clientType == ClientType.Authority ? authority : nonAuthority; + + + var expectedCompletedClients = new List { nonAuthority.LocalClientId }; + // the authority ID is not inside ClientsThatCompleted when running as a server + if (m_UseHost) + { + expectedCompletedClients.Insert(0, authority.LocalClientId); + } + + Scene loadedScene = default; + if (action == Action.Unload) + { + // Load the scene initially + authority.SceneManager.LoadScene(k_SceneToLoad, LoadSceneMode.Additive); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsLoaded); + AssertOnTimeout($"Timed out waiting for client to load the scene {k_SceneToLoad}!"); + + // Wait for any pending messages to be processed + yield return null; + + // 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 = nonAuthority.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 = authority.LocalClientId, + ClientsThatCompleted = expectedCompletedClients, + ClientsThatTimedOut = new List() + } + }); + + ////////////////////////////////////////// + // Testing event notifications + managerToTest.SceneManager.OnSceneEvent += OnSceneEvent; + + if (action == Action.Load) + { + Assert.That(authority.SceneManager.LoadScene(loadCall, LoadSceneMode.Additive) == SceneEventProgressStatus.Started); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsLoaded); + AssertOnTimeout($"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(authority.SceneManager.UnloadScene(loadedScene) == SceneEventProgressStatus.Started); + + yield return WaitForConditionOrTimeOut(ValidateSceneIsUnloaded); + AssertOnTimeout($"Timed out waiting for client to unload the scene {k_SceneToLoad}!"); + } + + // Wait for all messages to process + yield return null; + + 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() + { + foreach (var manager in m_NetworkManagers) + { + // default will have isLoaded as false so we can get the scene or default and test on isLoaded + var loadedScene = manager.SceneManager.ScenesLoaded.Values.FirstOrDefault(scene => scene.name == k_SceneToLoad); + if (!loadedScene.isLoaded) + { + return false; + } + + if (manager.SceneManager.SceneEventProgressTracking.Count > 0) + { + return false; + } + } + + return true; + } + + private bool ValidateSceneIsUnloaded() + { + foreach (var manager in m_NetworkManagers) + { + 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..c4c2a7de06 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/OnSceneEventCallbackTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 85ef6a4707d04010a84eeea61fdeb9a8 +timeCreated: 1747776388 \ No newline at end of file