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