using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Unity.Netcode
{
///
/// Used for local notifications of various scene events. The of
/// delegate type uses this class to provide
/// scene event status.
/// Note: This is only when is enabled.
/// *** Do not start new scene events within scene event notification callbacks.
/// See also:
///
///
public class SceneEvent
{
///
/// The returned by
/// This is set for the following s:
///
///
///
///
///
public AsyncOperation AsyncOperation;
///
/// Will always be set to the current
///
public SceneEventType SceneEventType;
///
/// If applicable, this reflects the type of scene loading or unloading that is occurring.
/// This is set for the following s:
///
///
///
///
///
///
///
///
///
public LoadSceneMode LoadSceneMode;
///
/// This will be set to the scene name that the event pertains to.
/// This is set for the following s:
///
///
///
///
///
///
///
///
///
public string SceneName;
///
/// When a scene is loaded, the Scene structure is returned.
/// This is set for the following s:
///
///
///
///
public Scene Scene;
///
/// The client identifier can vary depending upon the following conditions:
///
/// - s that always set the
/// to the local client identifier, are initiated (and processed locally) by the
/// server-host, and sent to all clients to be processed.
///
///
///
///
///
///
///
/// - Events that always set the to the local client identifier,
/// are initiated (and processed locally) by a client or server-host, and if initiated
/// by a client will always be sent to and processed on the server-host:
///
///
///
///
///
///
/// -
/// Events that always set the to the ServerId:
///
///
///
///
///
///
///
public ulong ClientId;
///
/// List of clients that completed a loading or unloading event.
/// This is set for the following s:
///
///
///
///
///
public List ClientsThatCompleted;
///
/// List of clients that timed out during a loading or unloading event.
/// This is set for the following s:
///
///
///
///
///
public List ClientsThatTimedOut;
}
///
/// Main class for managing network scenes when is enabled.
/// Uses the message to communicate between the server and client(s)
///
public class NetworkSceneManager : IDisposable
{
private const NetworkDelivery k_DeliveryType = NetworkDelivery.ReliableFragmentedSequenced;
internal const int InvalidSceneNameOrPath = -1;
// Used to be able to turn re-synchronization off
internal static bool DisableReSynchronization;
///
/// Used to detect if a scene event is underway
/// Only 1 scene event can occur on the server at a time for now.
///
private bool m_IsSceneEventActive = false;
///
/// The delegate callback definition for scene event notifications.
/// See also:
///
///
///
///
public delegate void SceneEventDelegate(SceneEvent sceneEvent);
///
/// Subscribe to this event to receive all notifications.
/// For more details review over and .
/// Alternate Single Event Type Notification Registration Options
/// To receive only a specific event type notification or a limited set of notifications you can alternately subscribe to
/// each notification type individually via the following events:
///
/// - Invoked only when a event is being processed
/// - Invoked only when an event is being processed
/// - Invoked only when a event is being processed
/// - Invoked only when a event is being processed
/// - Invoked only when an event is being processed
/// - Invoked only when a event is being processed
/// - Invoked only when an event is being processed
/// - Invoked only when a event is being processed
///
/// Note: Do not start new scene events within NetworkSceneManager scene event notification callbacks.
///
public event SceneEventDelegate OnSceneEvent;
///
/// Delegate declaration for the OnLoad event.
/// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
/// name of the scene being processed
/// the LoadSceneMode mode for the scene being loaded
/// the associated that can be used for scene loading progress
public delegate void OnLoadDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation);
///
/// Delegate declaration for the OnUnload event.
/// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
/// name of the scene being processed
/// the associated that can be used for scene unloading progress
public delegate void OnUnloadDelegateHandler(ulong clientId, string sceneName, AsyncOperation asyncOperation);
///
/// Delegate declaration for the OnSynchronize event.
/// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
public delegate void OnSynchronizeDelegateHandler(ulong clientId);
///
/// Delegate declaration for the OnLoadEventCompleted and OnUnloadEventCompleted events.
/// See also:
///
///
///
/// scene pertaining to this event
/// of the associated event completed
/// the clients that completed the loading event
/// the clients (if any) that timed out during the loading event
public delegate void OnEventCompletedDelegateHandler(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut);
///
/// Delegate declaration for the OnLoadComplete event.
/// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
/// the scene name pertaining to this event
/// the mode the scene was loaded in
public delegate void OnLoadCompleteDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode);
///
/// Delegate declaration for the OnUnloadComplete event.
/// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
/// the scene name pertaining to this event
public delegate void OnUnloadCompleteDelegateHandler(ulong clientId, string sceneName);
///
/// Delegate declaration for the OnSynchronizeComplete event.
/// See also:
/// for more information
///
/// the client that completed this event
public delegate void OnSynchronizeCompleteDelegateHandler(ulong clientId);
///
/// Invoked when a event is started by the server.
/// Note: The server and connected client(s) will always receive this notification.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadDelegateHandler OnLoad;
///
/// Invoked when a event is started by the server.
/// Note: The server and connected client(s) will always receive this notification.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadDelegateHandler OnUnload;
///
/// Invoked when a event is started by the server
/// after a client is approved for connection in order to synchronize the client with the currently loaded
/// scenes and NetworkObjects. This event signifies the beginning of the synchronization event.
/// Note: The server and connected client(s) will always receive this notification.
/// This event is generated on a per newly connected and approved client basis.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeDelegateHandler OnSynchronize;
///
/// Invoked when a event is generated by the server.
/// This event signifies the end of an existing event as it pertains
/// to all clients connected when the event was started. This event signifies that all clients (and server) have
/// finished the event.
/// Note: this is useful to know when all clients have loaded the same scene (single or additive mode)
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnLoadEventCompleted;
///
/// Invoked when a event is generated by the server.
/// This event signifies the end of an existing event as it pertains
/// to all clients connected when the event was started. This event signifies that all clients (and server) have
/// finished the event.
/// Note: this is useful to know when all clients have unloaded a specific scene. The will
/// always be for this event.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnUnloadEventCompleted;
///
/// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself).
/// Each client receives their own notification sent to the server.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadCompleteDelegateHandler OnLoadComplete;
///
/// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself).
/// Each client receives their own notification sent to the server.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadCompleteDelegateHandler OnUnloadComplete;
///
/// Invoked when a event is generated by a client.
/// Note: The server receives this message from the client, but will never generate this event for itself.
/// Each client receives their own notification sent to the server. This is useful to know that a client has
/// completed the entire connection sequence, loaded all scenes, and synchronized all NetworkObjects.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeCompleteDelegateHandler OnSynchronizeComplete;
///
/// Delegate declaration for the handler that provides
/// an additional level of scene loading security and/or validation to assure the scene being loaded
/// is valid scene to be loaded in the LoadSceneMode specified.
///
/// Build Settings Scenes in Build List index of the scene
/// Name of the scene
/// LoadSceneMode the scene is going to be loaded
/// true (valid) or false (not valid)
public delegate bool VerifySceneBeforeLoadingDelegateHandler(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode);
///
/// Delegate handler defined by that is invoked before the
/// server or client loads a scene during an active netcode game session.
///
///
/// Client Side: In order for clients to be notified of this condition you must assign the delegate handler.
/// Server Side: will return .
///
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading;
///
/// Delegate declaration for the handler that provides
/// an additional level of scene unloading validation to assure the scene being unloaded should
/// be unloaded.
///
/// The scene to be unloaded
/// true (valid) or false (not valid)
public delegate bool VerifySceneBeforeUnloadingDelegateHandler(Scene scene);
///
/// Client Side Only:
/// Delegate handler defined by that is only invoked when the client
/// is finished synchronizing and when is set to .
///
public VerifySceneBeforeUnloadingDelegateHandler VerifySceneBeforeUnloading;
///
/// When enabled and is , any scenes not synchronized with
/// the server will be unloaded unless returns true. This provides more granular control over
/// which already loaded client-side scenes not synchronized with the server should be unloaded.
///
///
/// If the delegate callback is not set then any scene loaded on the just synchronized client
/// will be unloaded.
/// One scenario is a synchronized client is disconnected for unexpected reasons and attempts to reconnect to the same network session
/// but still has all scenes that were loaded through server synchronization (initially or through scene events). However, during the
/// client disconnection period the server unloads one (or more) of the scenes loaded and as such the reconnecting client could still
/// have the now unloaded scenes still loaded. Enabling this flag coupled with assignment of the assignment of the
/// delegate callback provides you with the ability to keep scenes loaded by the client (i.e. UI etc) while discarding any artifact
/// scenes that no longer need to be loaded.
///
public bool PostSynchronizationSceneUnloading;
private bool m_ActiveSceneSynchronizationEnabled;
///
/// When enabled, the server or host will synchronize clients with changes to the currently active scene
///
public bool ActiveSceneSynchronizationEnabled
{
get
{
return m_ActiveSceneSynchronizationEnabled;
}
set
{
if (m_ActiveSceneSynchronizationEnabled != value)
{
m_ActiveSceneSynchronizationEnabled = value;
if (m_ActiveSceneSynchronizationEnabled)
{
SceneManager.activeSceneChanged += SceneManager_ActiveSceneChanged;
}
else
{
SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
}
}
}
}
///
/// The SceneManagerHandler implementation
///
internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
internal readonly Dictionary SceneEventProgressTracking = new Dictionary();
///
/// Used to track in-scene placed NetworkObjects
/// We store them by:
/// [GlobalObjectIdHash][Scene.Handle][NetworkObject]
/// The Scene.Handle aspect allows us to distinguish duplicated in-scene placed NetworkObjects created by the loading
/// of the same additive scene multiple times.
///
internal readonly Dictionary> ScenePlacedObjects = new Dictionary>();
///
/// This is used for the deserialization of in-scene placed NetworkObjects in order to distinguish duplicated in-scene
/// placed NetworkObjects created by the loading of the same additive scene multiple times.
///
internal Scene SceneBeingSynchronized;
///
/// Used to track which scenes are currently loaded
/// We store the scenes as [SceneHandle][Scene] in order to handle the loading and unloading of the same scene additively
/// Scene handle is only unique locally. So, clients depend upon the in order
/// to be able to know which specific scene instance the server is instructing the client to unload.
/// The client links the server scene handle to the client local scene handle upon a scene being loaded
///
///
internal Dictionary ScenesLoaded = new Dictionary();
///
/// Returns the currently loaded scenes that are synchronized with the session owner or server depending upon the selected
/// network topology.
///
///
/// The scenes loaded returns all scenes loaded where this returns only the scenes that have been
/// synchronized remotely. This can be useful when using scene validation and excluding certain scenes from being synchronized.
///
/// List of the known synchronized scenes
public List GetSynchronizedScenes()
{
return ScenesLoaded.Values.ToList();
}
///
/// Since Scene.handle is unique per client, we create a look-up table between the client and server to associate server unique scene
/// instances with client unique scene instances
///
internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary();
internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
internal bool IsRestoringSession;
///
/// Add the client to server (and vice versa) scene handle lookup.
/// Add the client-side handle to scene entry in the HandleToScene table.
/// If it fails (i.e. already added) it returns false.
///
internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle, Scene localScene)
{
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
{
ServerSceneHandleToClientSceneHandle.Add(serverHandle, clientHandle);
}
else if (!IsRestoringSession)
{
return false;
}
if (!ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
{
ClientSceneHandleToServerSceneHandle.Add(clientHandle, serverHandle);
}
else if (!IsRestoringSession)
{
return false;
}
// It is "Ok" if this already has an entry
if (!ScenesLoaded.ContainsKey(clientHandle))
{
ScenesLoaded.Add(clientHandle, localScene);
}
return true;
}
///
/// Removes the client to server (and vice versa) scene handles.
/// If it fails (i.e. already removed) it returns false.
///
internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
{
if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
{
ServerSceneHandleToClientSceneHandle.Remove(serverHandle);
}
else
{
return false;
}
if (ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
{
ClientSceneHandleToServerSceneHandle.Remove(clientHandle);
}
else
{
return false;
}
if (ScenesLoaded.ContainsKey(clientHandle))
{
ScenesLoaded.Remove(clientHandle);
}
else
{
return false;
}
return true;
}
///
/// Hash to build index lookup table
///
internal Dictionary HashToBuildIndex = new Dictionary();
///
/// Build index to hash lookup table
///
internal Dictionary BuildIndexToHash = new Dictionary();
///
/// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned
/// they need to be moved into the do not destroy temporary scene
/// When it is set: Just before starting the asynchronous loading call
/// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do
/// not destroy temporary scene are moved into the active scene
///
internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad;
///
/// Client and Server:
/// Used for all scene event processing
///
internal Dictionary SceneEventDataStore;
internal readonly NetworkManager NetworkManager;
// Keep track of this scene until the NetworkSceneManager is destroyed.
internal Scene DontDestroyOnLoadScene;
///
/// This setting changes how clients handle scene loading when initially synchronizing with the server.
/// See:
///
///
/// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and the
/// server's currently active scene will be loaded in single mode on the client unless it was already
/// loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded
/// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
/// and, if is
/// set, callback(s).
///
public LoadSceneMode ClientSynchronizationMode { get; internal set; }
///
/// When true, the messages will be turned off
///
private bool m_DisableValidationWarningMessages;
internal bool HasSceneAuthority()
{
if (!NetworkManager)
{
return false;
}
return (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer) || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClient.IsSessionOwner);
}
///
/// Handle NetworkSeneManager clean up
///
public void Dispose()
{
// Always assure we no longer listen to scene changes when disposed.
SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
SceneUnloadEventHandler.Shutdown();
foreach (var keypair in SceneEventDataStore)
{
if (NetworkLog.CurrentLogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'.");
}
keypair.Value.Dispose();
}
SceneEventDataStore.Clear();
SceneEventDataStore = null;
}
///
/// Creates a new SceneEventData object for a new scene event
///
/// SceneEventData instance
internal SceneEventData BeginSceneEvent()
{
var sceneEventData = new SceneEventData(NetworkManager);
SceneEventDataStore.Add(sceneEventData.SceneEventId, sceneEventData);
return sceneEventData;
}
///
/// Disposes and removes SceneEventData object for the scene event
///
/// SceneEventId to end
internal void EndSceneEvent(uint sceneEventId)
{
if (SceneEventDataStore.ContainsKey(sceneEventId))
{
SceneEventDataStore[sceneEventId].Dispose();
SceneEventDataStore.Remove(sceneEventId);
}
else
{
Debug.LogWarning($"Trying to dispose and remove SceneEventData Id '{sceneEventId}' that no longer exists!");
}
}
///
/// Used for integration tests, normal runtime mode this will always be LoadSceneMode.Single
///
internal LoadSceneMode DeferLoadingFilter = LoadSceneMode.Single;
///
/// Determines if a remote client should defer object creation initiated by CreateObjectMessage
/// until a scene event is completed.
///
///
/// Deferring object creation should only occur when there is a possibility the objects could be
/// instantiated in a currently active scene that will be unloaded during single mode scene loading
/// to prevent the newly created objects from being destroyed when the scene is unloaded.
///
internal bool ShouldDeferCreateObject()
{
// This applies only to remote clients and when scene management is enabled
if (!NetworkManager.NetworkConfig.EnableSceneManagement || HasSceneAuthority())
{
return false;
}
var synchronizeEventDetected = false;
var loadingEventDetected = false;
foreach (var entry in SceneEventDataStore)
{
if (entry.Value.SceneEventType == SceneEventType.Synchronize)
{
synchronizeEventDetected = true;
}
// When loading a scene and the load scene mode is single we should defer object creation
if (entry.Value.SceneEventType == SceneEventType.Load && entry.Value.LoadSceneMode == DeferLoadingFilter)
{
loadingEventDetected = true;
}
}
// Synchronizing while in client synchronization mode single --> Defer
// When not synchronizing but loading a scene in single mode --> Defer
return (synchronizeEventDetected && ClientSynchronizationMode == LoadSceneMode.Single) || (!synchronizeEventDetected && loadingEventDetected);
}
///
/// Gets the scene name from full path to the scene
///
internal string GetSceneNameFromPath(string scenePath)
{
var begin = scenePath.LastIndexOf("/", StringComparison.Ordinal) + 1;
var end = scenePath.LastIndexOf(".", StringComparison.Ordinal);
return scenePath.Substring(begin, end - begin);
}
///
/// Generates the hash values and associated tables
/// for the scenes in build list
///
internal void GenerateScenesInBuild()
{
// TODO 2023: We could support addressable or asset bundle scenes by
// adding a method that would allow users to add scenes to this.
// The method would be server-side only and require an additional SceneEventType
// that would be used to notify clients of the added scene. This might need
// to include information about the addressable or asset bundle (i.e. address to load assets)
HashToBuildIndex.Clear();
BuildIndexToHash.Clear();
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
var scenePath = SceneUtility.GetScenePathByBuildIndex(i);
var hash = XXHash.Hash32(scenePath);
var buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath);
// In the rare-case scenario where a programmatically generated build has duplicate
// scene entries, we will log an error and skip the entry
if (!HashToBuildIndex.ContainsKey(hash))
{
HashToBuildIndex.Add(hash, buildIndex);
BuildIndexToHash.Add(buildIndex, hash);
}
else
{
Debug.LogError($"{nameof(NetworkSceneManager)} is skipping duplicate scene path entry {scenePath}. Make sure your scenes in build list does not contain duplicates!");
}
}
}
///
/// Gets the scene name from a hash value generated from the full scene path
///
internal string SceneNameFromHash(uint sceneHash)
{
// 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";
}
return GetSceneNameFromPath(ScenePathFromHash(sceneHash));
}
///
/// Gets the full scene path from a hash value
///
internal string ScenePathFromHash(uint sceneHash)
{
if (HashToBuildIndex.ContainsKey(sceneHash))
{
return SceneUtility.GetScenePathByBuildIndex(HashToBuildIndex[sceneHash]);
}
else
{
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.");
}
}
///
/// Gets the associated hash value for the scene name or path
///
internal uint SceneHashFromNameOrPath(string sceneNameOrPath)
{
var buildIndex = SceneUtility.GetBuildIndexByScenePath(sceneNameOrPath);
if (buildIndex >= 0)
{
if (BuildIndexToHash.ContainsKey(buildIndex))
{
return BuildIndexToHash[buildIndex];
}
else
{
throw new Exception($"Scene '{sceneNameOrPath}' has a build index of {buildIndex} that does not exist in the {nameof(BuildIndexToHash)} table!");
}
}
else
{
throw new Exception($"Scene '{sceneNameOrPath}' couldn't be loaded because it has not been added to the build settings scenes in build list.");
}
}
///
/// When set to true, this will disable the console warnings about
/// a scene being invalidated.
///
/// true/false
public void DisableValidationWarnings(bool disabled)
{
m_DisableValidationWarningMessages = disabled;
}
///
/// This setting changes how clients handle scene loading when initially synchronizing with the server.
/// The server or host should set this value as clients will automatically be synchronized with the server (or host) side.
///
/// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and the
/// server's currently active scene will be loaded in single mode on the client unless it was already
/// loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded
/// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
/// and, if is
/// set, callback(s).
///
/// for initial client synchronization
public void SetClientSynchronizationMode(LoadSceneMode mode)
{
var networkManager = NetworkManager;
SceneManagerHandler.SetClientSynchronizationMode(ref networkManager, mode);
}
///
/// Constructor
///
/// one instance per instance
/// maximum pool size
internal NetworkSceneManager(NetworkManager networkManager)
{
NetworkManager = networkManager;
SceneEventDataStore = new Dictionary();
// Generates the scene name to hash value
GenerateScenesInBuild();
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
// Since the server tracks loaded scenes, we need to add any currently loaded scenes on the
// server side when the NetworkManager is started and NetworkSceneManager instantiated when
// scene management is enabled.
if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkManager.NetworkConfig.EnableSceneManagement)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var loadedScene = SceneManager.GetSceneAt(i);
ScenesLoaded.Add(loadedScene.handle, loadedScene);
}
SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager);
}
// Add to the server to client scene handle table
UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
internal void InitializeScenesLoaded()
{
if (!NetworkManager.DistributedAuthorityMode)
{
return;
}
if (HasSceneAuthority() && NetworkManager.NetworkConfig.EnableSceneManagement)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var loadedScene = SceneManager.GetSceneAt(i);
UpdateServerClientSceneHandle(loadedScene.handle, loadedScene.handle, loadedScene);
}
SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager);
}
}
///
/// Synchronizes clients when the currently active scene is changed
///
private void SceneManager_ActiveSceneChanged(Scene current, Scene next)
{
if ((!NetworkManager.DistributedAuthorityMode && !NetworkManager.IsServer) || (NetworkManager.DistributedAuthorityMode && !NetworkManager.LocalClient.IsSessionOwner))
{
return;
}
// If no clients are connected, then don't worry about notifications
if (!(NetworkManager.ConnectedClientsIds.Count > (NetworkManager.IsHost ? 1 : 0)))
{
return;
}
// Don't notify if a scene event is in progress
foreach (var sceneEventEntry in SceneEventProgressTracking)
{
if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
{
return;
}
}
// If the scene's build index is in the hash table
if (BuildIndexToHash.ContainsKey(next.buildIndex))
{
// Notify clients of the change in active scene
var sceneEvent = BeginSceneEvent();
sceneEvent.SceneEventType = SceneEventType.ActiveSceneChanged;
sceneEvent.ActiveSceneHash = BuildIndexToHash[next.buildIndex];
var sessionOwner = NetworkManager.ServerClientId;
if (NetworkManager.DistributedAuthorityMode)
{
sessionOwner = NetworkManager.CurrentSessionOwner;
}
SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != sessionOwner).ToArray());
EndSceneEvent(sceneEvent.SceneEventId);
}
}
///
/// If the VerifySceneBeforeLoading delegate handler has been set by the user, this will provide
/// an additional level of security and/or validation that the scene being loaded in the specified
/// loading mode is "a valid scene to be loaded in the LoadSceneMode specified".
///
/// index into ScenesInBuild
/// LoadSceneMode the scene is going to be loaded
/// true (Valid) or false (Invalid)
internal bool ValidateSceneBeforeLoading(uint sceneHash, LoadSceneMode loadSceneMode)
{
var sceneName = SceneNameFromHash(sceneHash);
var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName);
return ValidateSceneBeforeLoading(sceneIndex, sceneName, loadSceneMode);
}
///
/// Overloaded version that is invoked by and .
/// This specifically is to allow runtime generated scenes to be excluded by the server during synchronization.
///
internal bool ValidateSceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
{
var validated = true;
if (VerifySceneBeforeLoading != null)
{
validated = VerifySceneBeforeLoading.Invoke(sceneIndex, sceneName, loadSceneMode);
}
if (!validated && !m_DisableValidationWarningMessages)
{
var serverHostorClient = "Client";
if (HasSceneAuthority())
{
serverHostorClient = NetworkManager.DistributedAuthorityMode ? "Session Owner" : NetworkManager.IsHost ? "Host" : "Server";
}
Debug.LogWarning($"Scene {sceneName} of Scenes in Build Index {sceneIndex} being loaded in {loadSceneMode} mode failed validation on the {serverHostorClient}!");
}
return validated;
}
///
/// Used for NetcodeIntegrationTest testing in order to properly
/// assign the right loaded scene to the right client's ScenesLoaded list
///
internal Func OverrideGetAndAddNewlyLoadedSceneByName;
///
/// Since SceneManager.GetSceneByName only returns the first scene that matches the name
/// we must "find" a newly added scene by looking through all loaded scenes and determining
/// which scene with the same name has not yet been loaded.
/// In order to support loading the same additive scene within in-scene placed NetworkObjects,
/// we must do this to be able to soft synchronize the "right version" of the NetworkObject.
///
///
///
internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
{
if (OverrideGetAndAddNewlyLoadedSceneByName != null)
{
return OverrideGetAndAddNewlyLoadedSceneByName.Invoke(sceneName);
}
else
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var sceneLoaded = SceneManager.GetSceneAt(i);
if (sceneLoaded.name == sceneName)
{
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
SceneManagerHandler.StartTrackingScene(sceneLoaded, true, NetworkManager);
return sceneLoaded;
}
}
}
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
}
}
///
/// Client Side Only:
/// This takes a server scene handle that is written by the server before the scene relative
/// NetworkObject is serialized and converts the server scene handle to a local client handle
/// so it can set the appropriate SceneBeingSynchronized.
/// Note: This is now part of the soft synchronization process and is needed for the scenario
/// where a user loads the same scene additively that has an in-scene placed NetworkObject
/// which means each scene relative in-scene placed NetworkObject will have the identical GlobalObjectIdHash
/// value. Scene handles are used to distinguish between in-scene placed NetworkObjects under this situation.
///
///
internal void SetTheSceneBeingSynchronized(int serverSceneHandle)
{
var clientSceneHandle = serverSceneHandle;
if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverSceneHandle))
{
clientSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle];
// If we were already set, then ignore
if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == clientSceneHandle)
{
return;
}
// Get the scene currently being synchronized
SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene();
if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded)
{
// Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of
SceneBeingSynchronized = SceneManager.GetActiveScene();
// Keeping the warning here in the event we cannot find the scene being synchronized
Debug.LogWarning($"[{nameof(NetworkSceneManager)}- {nameof(ScenesLoaded)}] Could not find the appropriate scene to set as being synchronized! Using the currently active scene.");
}
}
else
{
// Most common scenario for DontDestroyOnLoad is when NetworkManager is set to not be destroyed
if (serverSceneHandle == DontDestroyOnLoadScene.handle)
{
SceneBeingSynchronized = NetworkManager.gameObject.scene;
return;
}
else
{
// Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of
// or the NetworkObject has yet to be moved to that specific scene (i.e. no DontDestroyOnLoad scene exists yet).
SceneBeingSynchronized = SceneManager.GetActiveScene();
// This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene
// Otherwise, this is some other specific scenario that we might not be handling currently.
Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} ({serverSceneHandle}) could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene.");
}
}
}
///
/// During soft synchronization of in-scene placed NetworkObjects, this is now used by NetworkSpawnManager.CreateLocalNetworkObject
///
///
///
internal NetworkObject GetSceneRelativeInSceneNetworkObject(uint globalObjectIdHash, int? networkSceneHandle)
{
if (ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
var sceneHandle = SceneBeingSynchronized.handle;
if (networkSceneHandle.HasValue && networkSceneHandle.Value != 0 && ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle.Value))
{
sceneHandle = ServerSceneHandleToClientSceneHandle[networkSceneHandle.Value];
}
if (ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle))
{
return ScenePlacedObjects[globalObjectIdHash][sceneHandle];
}
}
return null;
}
///
/// Generic sending of scene event data
///
/// array of client identifiers to receive the scene event message
private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
{
if (targetClientIds.Length == 0 && !NetworkManager.DistributedAuthorityMode)
{
// This would be the Host/Server with no clients connected
// Silently return as there is nothing to be done
return;
}
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
{
var message = new SceneEventMessage
{
EventData = sceneEvent,
};
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, targetClientIds);
NetworkManager.NetworkMetrics.TrackSceneEventSent(targetClientIds, (uint)SceneEventDataStore[sceneEventId].SceneEventType, SceneNameFromHash(SceneEventDataStore[sceneEventId].SceneHash), size);
}
}
///
/// Entry method for scene unloading validation
///
/// the scene to be unloaded
///
private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
{
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
{
Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!");
return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled);
}
if (!HasSceneAuthority())
{
if (NetworkManager.DistributedAuthorityMode)
{
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.SessionOwnerOnlyAction)}][Unload] Clients cannot invoke the {nameof(UnloadScene)} method!");
return new SceneEventProgress(null, SceneEventProgressStatus.SessionOwnerOnlyAction);
}
else
{
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Unload] Clients cannot invoke the {nameof(UnloadScene)} method!");
return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction);
}
}
if (!scene.isLoaded)
{
Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!");
return new SceneEventProgress(null, SceneEventProgressStatus.SceneNotLoaded);
}
return ValidateSceneEvent(scene.name, true);
}
///
/// Entry method for scene loading validation
///
/// scene name to load
///
private SceneEventProgress ValidateSceneEventLoading(string sceneName)
{
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
{
Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!");
return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled);
}
if (!HasSceneAuthority())
{
if (NetworkManager.DistributedAuthorityMode)
{
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.SessionOwnerOnlyAction)}][Load] Only the session owner can invoke the {nameof(LoadScene)} method!");
return new SceneEventProgress(null, SceneEventProgressStatus.SessionOwnerOnlyAction);
}
else
{
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Load] Clients cannot invoke the {nameof(LoadScene)} method!");
return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction);
}
}
return ValidateSceneEvent(sceneName);
}
///
/// Validates the new scene event request by the server-side code.
/// This also initializes some commonly shared values as well as SceneEventProgress
///
///
/// that should have a of otherwise it failed.
private SceneEventProgress ValidateSceneEvent(string sceneName, bool isUnloading = false)
{
// Return scene event already in progress if one is already in progress
if (m_IsSceneEventActive)
{
return new SceneEventProgress(null, SceneEventProgressStatus.SceneEventInProgress);
}
// Return invalid scene name status if the scene name is invalid
if (SceneUtility.GetBuildIndexByScenePath(sceneName) == InvalidSceneNameOrPath)
{
Debug.LogError($"Scene '{sceneName}' couldn't be loaded because it has not been added to the build settings scenes in build list.");
return new SceneEventProgress(null, SceneEventProgressStatus.InvalidSceneName);
}
var sceneEventProgress = new SceneEventProgress(NetworkManager)
{
SceneHash = SceneHashFromNameOrPath(sceneName)
};
SceneEventProgressTracking.Add(sceneEventProgress.Guid, sceneEventProgress);
m_IsSceneEventActive = true;
// Set our callback delegate handler for completion
sceneEventProgress.OnComplete = OnSceneEventProgressCompleted;
return sceneEventProgress;
}
///
/// Callback for the handler
///
///
private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress)
{
var sceneEventData = BeginSceneEvent();
var clientsThatCompleted = sceneEventProgress.GetClientsWithStatus(true);
var clientsThatTimedOut = sceneEventProgress.GetClientsWithStatus(false);
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneHash = sceneEventProgress.SceneHash;
sceneEventData.SceneEventType = sceneEventProgress.SceneEventType;
sceneEventData.ClientsCompleted = clientsThatCompleted;
sceneEventData.LoadSceneMode = sceneEventProgress.LoadSceneMode;
sceneEventData.ClientsTimedOut = clientsThatTimedOut;
if (NetworkManager.DistributedAuthorityMode)
{
SendSceneEventData(sceneEventData.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray());
}
else
{
var message = new SceneEventMessage
{
EventData = sceneEventData
};
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ConnectedClientsIds);
NetworkManager.NetworkMetrics.TrackSceneEventSent(
NetworkManager.ConnectedClientsIds,
(uint)sceneEventProgress.SceneEventType,
SceneNameFromHash(sceneEventProgress.SceneHash),
size);
}
// 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);
}
EndSceneEvent(sceneEventData.SceneEventId);
return true;
}
///
/// Server Side:
/// Unloads an additively loaded scene. If you want to unload a mode loaded scene load another scene.
/// When applicable, the is delivered within the via the
///
///
/// ( means it was successful)
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!");
return SceneEventProgressStatus.SceneNotLoaded;
}
var sceneEventProgress = ValidateSceneEventUnloading(scene);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{
return sceneEventProgress.Status;
}
if (!ScenesLoaded.ContainsKey(sceneHandle))
{
Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
return SceneEventProgressStatus.InternalNetcodeError;
}
if (NetworkManager.DistributedAuthorityMode)
{
if (ClientSceneHandleToServerSceneHandle.ContainsKey(sceneHandle))
{
sceneHandle = ClientSceneHandleToServerSceneHandle[sceneHandle];
}
}
// 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.
var networkManager = NetworkManager;
SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Unload;
sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName);
sceneEventData.LoadSceneMode = LoadSceneMode.Additive; // The only scenes unloaded are scenes that were additively loaded
sceneEventData.SceneHandle = sceneHandle;
// This will be the message we send to everyone when this scene event sceneEventProgress is complete
sceneEventProgress.SceneEventType = SceneEventType.UnloadEventCompleted;
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
if (!RemoveServerClientSceneHandle(sceneEventData.SceneHandle, scene.handle))
{
Debug.LogError($"Failed to remove {SceneNameFromHash(sceneEventData.SceneHash)} scene handles [Server ({sceneEventData.SceneHandle})][Local({scene.handle})]");
}
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);
//Return the status
return sceneEventProgress.Status;
}
///
/// Client Side:
/// Handles scene events.
///
private void OnClientUnloadScene(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.SceneHash);
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle))
{
Debug.Log($"Client failed to unload scene {sceneName} " +
$"because we are missing the client scene handle due to the server scene handle {sceneEventData.SceneHandle} not being found.");
EndSceneEvent(sceneEventId);
return;
}
var sceneHandle = ServerSceneHandleToClientSceneHandle[sceneEventData.SceneHandle];
if (!ScenesLoaded.ContainsKey(sceneHandle))
{
// Error scene handle not found!
throw new Exception($"Client failed to unload scene {sceneName} " +
$"because the client scene handle {sceneHandle} was not found in ScenesLoaded!");
}
var scene = ScenesLoaded[sceneHandle];
// 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.
var networkManager = NetworkManager;
SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
m_IsSceneEventActive = true;
var sceneEventProgress = new SceneEventProgress(NetworkManager)
{
SceneEventId = sceneEventData.SceneEventId,
OnSceneEventCompleted = OnSceneUnloaded,
};
if (NetworkManager.DistributedAuthorityMode)
{
SceneEventProgressTracking.Add(sceneEventData.SceneEventProgressId, sceneEventProgress);
}
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, NetworkManager);
// Remove our server to scene handle lookup
if (!RemoveServerClientSceneHandle(sceneEventData.SceneHandle, sceneHandle))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Failed to remove server scene handle ({sceneEventData.SceneHandle}) or client scene handle({sceneHandle})! Happened during scene unload for {sceneName}.");
}
// 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);
}
///
/// Server and Client:
/// Invoked when an additively loaded scene is unloaded
///
private void OnSceneUnloaded(uint sceneEventId)
{
// If we are shutdown or about to shutdown, then ignore this event
if (!NetworkManager.IsListening || NetworkManager.ShutdownInProgress)
{
EndSceneEvent(sceneEventId);
return;
}
// Migrate the NetworkObjects marked to not be destroyed with the scene into the currently active scene
MoveObjectsFromDontDestroyOnLoadToScene(SceneManager.GetActiveScene());
var sceneEventData = SceneEventDataStore[sceneEventId];
if (HasSceneAuthority())
{
var sessionOwner = NetworkManager.DistributedAuthorityMode ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
// Server sends the unload scene notification after unloading because it will despawn all scene relative in-scene NetworkObjects
// If we send this event to all clients before the server is finished unloading they will get warning about an object being
// despawned that no longer exists
SendSceneEventData(sceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != sessionOwner).ToArray());
//Only if we are session owner do we want register having loaded for the associated SceneEventProgress
if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && HasSceneAuthority())
{
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(sessionOwner);
}
}
else if (NetworkManager.DistributedAuthorityMode)
{
SceneEventProgressTracking.Remove(sceneEventData.SceneEventProgressId);
m_IsSceneEventActive = false;
}
// Next we prepare to send local notifications for unload complete
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));
if (!HasSceneAuthority())
{
sceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner;
sceneEventData.SenderClientId = NetworkManager.LocalClientId;
var message = new SceneEventMessage
{
EventData = sceneEventData,
};
// This might seem like it needs more logic to determine the target, but the only scenario where we send to the session owner is if the
// current instance is the DAHost.
var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target);
NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size);
}
EndSceneEvent(sceneEventId);
// This scene event is now considered "complete"
m_IsSceneEventActive = false;
}
private void EmptySceneUnloadedOperation(uint sceneEventId)
{
// Do nothing (this is a stub call since it is only used to flush all additively loaded scenes)
}
///
/// Clears all scenes when loading in single mode
/// Since we assume a single mode loaded scene will be considered the "currently active scene",
/// we only unload any additively loaded scenes.
///
internal void UnloadAdditivelyLoadedScenes(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
// Unload all additive scenes while making sure we don't try to unload the base scene ( loaded in single mode ).
var currentActiveScene = SceneManager.GetActiveScene();
foreach (var keyHandleEntry in ScenesLoaded)
{
// Validate the scene as well as ignore the DDOL (which will have a negative buildIndex)
if (currentActiveScene.name != keyHandleEntry.Value.name && keyHandleEntry.Value.buildIndex >= 0)
{
var sceneEventProgress = new SceneEventProgress(NetworkManager)
{
SceneEventId = sceneEventId,
OnSceneEventCompleted = EmptySceneUnloadedOperation
};
if (ClientSceneHandleToServerSceneHandle.ContainsKey(keyHandleEntry.Value.handle))
{
var serverSceneHandle = ClientSceneHandleToServerSceneHandle[keyHandleEntry.Value.handle];
ServerSceneHandleToClientSceneHandle.Remove(serverSceneHandle);
}
ClientSceneHandleToServerSceneHandle.Remove(keyHandleEntry.Value.handle);
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress);
SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload);
}
}
// clear out our scenes loaded list
ScenesLoaded.Clear();
SceneManagerHandler.ClearSceneTracking(NetworkManager);
}
///
/// Server side:
/// Loads the scene name in either additive or single loading mode.
/// When applicable, the is delivered within the via
///
/// the name of the scene to be loaded
/// how the scene will be loaded (single or additive mode)
/// ( means it was successful)
public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSceneMode)
{
var sceneEventProgress = ValidateSceneEventLoading(sceneName);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{
return sceneEventProgress.Status;
}
// This will be the message we send to everyone when this scene event sceneEventProgress is complete
sceneEventProgress.SceneEventType = SceneEventType.LoadEventCompleted;
sceneEventProgress.LoadSceneMode = loadSceneMode;
var sceneEventData = BeginSceneEvent();
// Now set up the current scene event
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Load;
sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName);
sceneEventData.LoadSceneMode = loadSceneMode;
var sceneEventId = sceneEventData.SceneEventId;
// 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)
{
EndSceneEvent(sceneEventId);
return SceneEventProgressStatus.SceneFailedVerification;
}
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned
// they need to be moved into the do not destroy temporary scene
// When it is set: Just before starting the asynchronous loading call
// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do
// not destroy temporary scene are moved into the active scene
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
// Destroy current scene objects before switching.
NetworkManager.SpawnManager.ServerDestroySpawnedSceneObjects();
// Preserve the objects that should not be destroyed during the scene event
MoveObjectsToDontDestroyOnLoad();
// Now Unload all currently additively loaded scenes
UnloadAdditivelyLoadedScenes(sceneEventId);
// Register the active scene for unload scene event notifications
SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single);
}
// Now start loading the scene
sceneEventProgress.SceneEventId = sceneEventId;
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.LocalClientId
});
OnLoad?.Invoke(NetworkManager.LocalClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad);
//Return our scene progress instance
return sceneEventProgress.Status;
}
///
/// Helper class used to handle "odd ball" scene unload event notification scenarios
/// when scene switching.
///
internal class SceneUnloadEventHandler
{
private static Dictionary> s_Instances = new Dictionary>();
internal static void RegisterScene(NetworkSceneManager networkSceneManager, Scene scene, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation = null)
{
var networkManager = networkSceneManager.NetworkManager;
if (!s_Instances.ContainsKey(networkManager))
{
s_Instances.Add(networkManager, new List());
}
var clientId = networkManager.LocalClientId;
s_Instances[networkManager].Add(new SceneUnloadEventHandler(networkSceneManager, scene, clientId, loadSceneMode, asyncOperation));
}
private static void SceneUnloadComplete(SceneUnloadEventHandler sceneUnloadEventHandler)
{
if (sceneUnloadEventHandler == null || sceneUnloadEventHandler.m_NetworkSceneManager == null || sceneUnloadEventHandler.m_NetworkSceneManager.NetworkManager == null)
{
return;
}
var networkManager = sceneUnloadEventHandler.m_NetworkSceneManager.NetworkManager;
if (s_Instances.ContainsKey(networkManager))
{
s_Instances[networkManager].Remove(sceneUnloadEventHandler);
if (s_Instances[networkManager].Count == 0)
{
s_Instances.Remove(networkManager);
}
}
}
///
/// Called by NetworkSceneManager when it is disposing
///
internal static void Shutdown()
{
foreach (var instanceEntry in s_Instances)
{
foreach (var instance in instanceEntry.Value)
{
instance.OnShutdown();
}
instanceEntry.Value.Clear();
}
s_Instances.Clear();
}
private NetworkSceneManager m_NetworkSceneManager;
private AsyncOperation m_AsyncOperation;
private LoadSceneMode m_LoadSceneMode;
private ulong m_ClientId;
private Scene m_Scene;
private bool m_ShuttingDown;
private void OnShutdown()
{
m_ShuttingDown = true;
SceneManager.sceneUnloaded -= SceneUnloaded;
}
private void SceneUnloaded(Scene scene)
{
if (m_Scene.handle == scene.handle && !m_ShuttingDown)
{
if (m_NetworkSceneManager != null && m_NetworkSceneManager.NetworkManager != null)
{
m_NetworkSceneManager.OnSceneEvent?.Invoke(new SceneEvent()
{
AsyncOperation = m_AsyncOperation,
SceneEventType = SceneEventType.UnloadComplete,
SceneName = m_Scene.name,
LoadSceneMode = m_LoadSceneMode,
ClientId = m_ClientId
});
m_NetworkSceneManager.OnUnloadComplete?.Invoke(m_ClientId, m_Scene.name);
}
SceneManager.sceneUnloaded -= SceneUnloaded;
SceneUnloadComplete(this);
}
}
private SceneUnloadEventHandler(NetworkSceneManager networkSceneManager, Scene scene, ulong clientId, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation = null)
{
m_LoadSceneMode = loadSceneMode;
m_AsyncOperation = asyncOperation;
m_NetworkSceneManager = networkSceneManager;
m_ClientId = clientId;
m_Scene = scene;
SceneManager.sceneUnloaded += SceneUnloaded;
// Send the initial unload event notification
m_NetworkSceneManager.OnSceneEvent?.Invoke(new SceneEvent()
{
AsyncOperation = m_AsyncOperation,
SceneEventType = SceneEventType.Unload,
SceneName = m_Scene.name,
LoadSceneMode = m_LoadSceneMode,
ClientId = clientId
});
m_NetworkSceneManager.OnUnload?.Invoke(networkSceneManager.NetworkManager.LocalClientId, m_Scene.name, null);
}
}
///
/// Client Side:
/// Handles both forms of scene loading
///
/// Stream data associated with the event
private void OnClientSceneLoadingEvent(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.SceneHash);
// Run scene validation before loading a scene
if (!ValidateSceneBeforeLoading(sceneEventData.SceneHash, sceneEventData.LoadSceneMode))
{
EndSceneEvent(sceneEventId);
return;
}
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
// Move ALL NetworkObjects to the temp scene
MoveObjectsToDontDestroyOnLoad();
// Now Unload all currently additively loaded scenes
UnloadAdditivelyLoadedScenes(sceneEventData.SceneEventId);
}
// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned
// they need to be moved into the do not destroy temporary scene
// When it is set: Just before starting the asynchronous loading call
// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do
// not destroy temporary scene are moved into the active scene
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
// Register the active scene for unload scene event notifications
SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single);
}
var sceneEventProgress = new SceneEventProgress(NetworkManager)
{
SceneEventId = sceneEventId,
OnSceneEventCompleted = OnSceneLoaded,
Status = SceneEventProgressStatus.Started,
};
if (NetworkManager.DistributedAuthorityMode)
{
SceneEventProgressTracking.Add(sceneEventData.SceneEventProgressId, sceneEventProgress);
m_IsSceneEventActive = true;
}
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);
}
///
/// Client and Server:
/// Generic on scene loaded callback method to be called upon a scene loading
///
private void OnSceneLoaded(uint sceneEventId)
{
// If we are shutdown or about to shutdown, then ignore this event
if (!NetworkManager.IsListening || NetworkManager.ShutdownInProgress)
{
EndSceneEvent(sceneEventId);
return;
}
var sceneEventData = SceneEventDataStore[sceneEventId];
var nextScene = GetAndAddNewlyLoadedSceneByName(SceneNameFromHash(sceneEventData.SceneHash));
if (!nextScene.isLoaded || !nextScene.IsValid())
{
throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!");
}
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
SceneManager.SetActiveScene(nextScene);
}
if (NetworkManager.DistributedAuthorityMode)
{
var networkSceneHandle = nextScene.handle;
if (!HasSceneAuthority())
{
networkSceneHandle = sceneEventData.SceneHandle;
}
// Update the server scene handle to client scene handle look up table
if (!UpdateServerClientSceneHandle(networkSceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
Debug.LogWarning($"Server Scene Handle ({networkSceneHandle}) already exist! Happened during scene load of {nextScene.name} with the local handle ({nextScene.handle})");
}
}
else if (NetworkManager.IsServer)
{
// Update the server scene handle to client scene handle look up table
if (!UpdateServerClientSceneHandle(nextScene.handle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
Debug.LogWarning($"Server Scene Handle ({nextScene.handle}) already exist! Happened during scene load of {nextScene.name} with the local handle ({nextScene.handle})");
}
}
//Get all NetworkObjects loaded by the scene
PopulateScenePlacedObjects(nextScene);
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
// Move all objects to the new scene
MoveObjectsFromDontDestroyOnLoadToScene(nextScene);
}
// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned
// they need to be moved into the do not destroy temporary scene
// When it is set: Just before starting the asynchronous loading call
// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do
// not destroy temporary scene are moved into the active scene
IsSpawnedObjectsPendingInDontDestroyOnLoad = false;
if (HasSceneAuthority())
{
OnSessionOwnerLoadedScene(sceneEventId, nextScene);
}
else
{
if (!NetworkManager.DistributedAuthorityMode)
{
// For the client, we make a server scene handle to client scene handle look up table
if (!UpdateServerClientSceneHandle(sceneEventData.SceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
}
}
OnClientLoadedScene(sceneEventId, nextScene);
}
}
///
/// Server/SessionOwner side:
/// On scene loaded callback method invoked by OnSceneLoading only
///
private void OnSessionOwnerLoadedScene(uint sceneEventId, Scene scene)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
// Register in-scene placed NetworkObjects with spawn manager
foreach (var keyValuePairByGlobalObjectIdHash in ScenePlacedObjects)
{
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
{
if (!keyValuePairBySceneHandle.Value.IsPlayerObject)
{
// All in-scene placed NetworkObjects default to being owned by the server
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value,
NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, NetworkManager.LocalClientId, true);
}
}
}
foreach (var keyValuePairByGlobalObjectIdHash in ScenePlacedObjects)
{
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
{
if (!keyValuePairBySceneHandle.Value.IsPlayerObject)
{
keyValuePairBySceneHandle.Value.InternalInSceneNetworkObjectsSpawned();
}
}
}
// Add any despawned when spawned in-scene placed NetworkObjects to the scene event data
sceneEventData.AddDespawnedInSceneNetworkObjects();
// Set the server's scene's handle so the client can build a look up table
sceneEventData.SceneHandle = scene.handle;
var sessionOwner = NetworkManager.ServerClientId;
// Send all clients the scene load event
if (NetworkManager.DistributedAuthorityMode)
{
sessionOwner = NetworkManager.CurrentSessionOwner;
}
SendSceneEventData(sceneEventData.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != sessionOwner).ToArray());
m_IsSceneEventActive = false;
//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.LocalClientId,
Scene = scene,
});
OnLoadComplete?.Invoke(NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode);
//Second, only if we are a host do we want register having loaded for the associated SceneEventProgress
if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && NetworkManager.IsClient)
{
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(NetworkManager.LocalClientId);
}
EndSceneEvent(sceneEventId);
}
///
/// Client side:
/// On scene loaded callback method invoked by OnSceneLoading only
///
private void OnClientLoadedScene(uint sceneEventId, Scene scene)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
sceneEventData.DeserializeScenePlacedObjects();
sceneEventData.SceneEventType = SceneEventType.LoadComplete;
if (NetworkManager.DistributedAuthorityMode)
{
sceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner;
sceneEventData.SenderClientId = NetworkManager.LocalClientId;
var message = new SceneEventMessage
{
EventData = sceneEventData,
};
var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target);
NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size);
}
else
{
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
}
m_IsSceneEventActive = false;
// Process any pending create object messages that the client received while loading a scene
ProcessDeferredCreateObjectMessages();
if (NetworkManager.DistributedAuthorityMode)
{
SceneEventProgressTracking.Remove(sceneEventData.SceneEventProgressId);
m_IsSceneEventActive = false;
}
// 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);
EndSceneEvent(sceneEventId);
}
///
/// Used for integration testing, due to the complexities of having all clients loading scenes
/// this is needed to "filter" out the scenes not loaded by NetworkSceneManager
/// (i.e. we don't want a late joining player to load all of the other client scenes)
///
internal Func ExcludeSceneFromSychronization;
///
/// Server Side:
/// This is used for players that have just had their connection approved and will assure they are synchronized
/// properly if they are late joining
/// Note: We write out all of the scenes to be loaded first and then all of the NetworkObjects that need to be
/// synchronized.
///
/// newly joined client identifier
internal void SynchronizeNetworkObjects(ulong clientId)
{
// Update the clients
NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
var sceneEventData = BeginSceneEvent();
sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode;
sceneEventData.InitializeForSynch();
sceneEventData.TargetClientId = clientId;
sceneEventData.LoadSceneMode = ClientSynchronizationMode;
var activeScene = SceneManager.GetActiveScene();
sceneEventData.SceneEventType = SceneEventType.Synchronize;
if (BuildIndexToHash.ContainsKey(activeScene.buildIndex))
{
sceneEventData.ActiveSceneHash = BuildIndexToHash[activeScene.buildIndex];
}
// Organize how (and when) we serialize our NetworkObjects
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
// NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager
// unless the scene in question is the currently active scene.
if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene))
{
continue;
}
if (scene == DontDestroyOnLoadScene)
{
continue;
}
// This would depend upon whether we are additive or not
// If we are the base scene, then we set the root scene index;
if (activeScene == scene)
{
if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, sceneEventData.LoadSceneMode))
{
continue;
}
sceneEventData.SceneHash = SceneHashFromNameOrPath(scene.path);
// If we are just a normal client, then always use the server scene handle
if (NetworkManager.DistributedAuthorityMode)
{
sceneEventData.SenderClientId = NetworkManager.LocalClientId;
sceneEventData.SceneHandle = ClientSceneHandleToServerSceneHandle[scene.handle];
}
else
{
sceneEventData.SceneHandle = scene.handle;
}
}
else if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, LoadSceneMode.Additive))
{
continue;
}
// If we are just a normal client and in distributed authority mode, then always use the known server scene handle
if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
{
sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), ClientSceneHandleToServerSceneHandle[scene.handle]);
}
else
{
sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), scene.handle);
}
}
sceneEventData.AddSpawnedNetworkObjects();
sceneEventData.AddDespawnedInSceneNetworkObjects();
var message = new SceneEventMessage
{
EventData = sceneEventData
};
var size = 0;
if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
{
size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId);
}
else
{
size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, clientId);
}
NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEventData.SceneEventType, "", size);
// Notify the local server that the client has been sent the synchronize event
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
ClientId = clientId
});
OnSynchronize?.Invoke(clientId);
EndSceneEvent(sceneEventData.SceneEventId);
}
///
/// This is called when the client receives the event
/// Note: This can recurse one additional time by the client if the current scene loaded by the client
/// is already loaded.
///
private void OnClientBeginSync(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneHash = sceneEventData.GetNextSceneSynchronizationHash();
var sceneHandle = sceneEventData.GetNextSceneSynchronizationHandle();
var sceneName = SceneNameFromHash(sceneHash);
var activeScene = SceneManager.GetActiveScene();
var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive;
// Store the sceneHandle and hash
sceneEventData.NetworkSceneHandle = sceneHandle;
sceneEventData.ClientSceneHash = sceneHash;
// If this is the beginning of the synchronization event, then send client a notification that synchronization has begun
if (sceneHash == sceneEventData.SceneHash)
{
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = SceneEventType.Synchronize,
ClientId = NetworkManager.LocalClientId,
});
OnSynchronize?.Invoke(NetworkManager.LocalClientId);
}
// Always check to see if the scene needs to be validated
if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode))
{
HandleClientSceneEvent(sceneEventId);
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo($"Client declined to load the scene {sceneName}, continuing with synchronization.");
}
return;
}
var sceneLoad = (AsyncOperation)null;
// Determines if the client has the scene to be loaded already loaded, if so will return true and the client will skip loading this scene
// For ClientSynchronizationMode LoadSceneMode.Single, we pass in whether the scene being loaded is the first/primary active scene and if it is already loaded
// it should pass through to post load processing (ClientLoadedSynchronization).
// For ClientSynchronizationMode LoadSceneMode.Additive, if the scene is already loaded or the active scene is the scene to be loaded (does not require it to
// be the initial primary scene) then go ahead and pass through to post load processing (ClientLoadedSynchronization).
var shouldPassThrough = SceneManagerHandler.ClientShouldPassThrough(sceneName, sceneHash == sceneEventData.SceneHash, ClientSynchronizationMode, NetworkManager);
if (!shouldPassThrough)
{
// If not, then load the scene
var sceneEventProgress = new SceneEventProgress(NetworkManager)
{
SceneEventId = sceneEventId,
OnSceneEventCompleted = ClientLoadedSynchronization
};
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);
}
else
{
// If so, then pass through
ClientLoadedSynchronization(sceneEventId);
}
}
///
/// Once a scene is loaded ( or if it was already loaded) this gets called.
/// This handles all of the in-scene and dynamically spawned NetworkObject synchronization
///
/// Netcode scene index that was loaded
private void ClientLoadedSynchronization(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash);
var nextScene = SceneManagerHandler.GetSceneFromLoadedScenes(sceneName, NetworkManager);
if (!nextScene.IsValid())
{
nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
}
if (!nextScene.isLoaded || !nextScene.IsValid())
{
throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!");
}
var loadSceneMode = (sceneEventData.ClientSceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive);
// For now, during a synchronization event, we will make the first scene the "base/master" scene that denotes a "complete scene switch"
if (loadSceneMode == LoadSceneMode.Single)
{
SceneManager.SetActiveScene(nextScene);
}
// For the client, we make a server scene handle to client scene handle look up table
if (!UpdateServerClientSceneHandle(sceneEventData.NetworkSceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
}
// Apply all in-scene placed NetworkObjects loaded by the scene
PopulateScenePlacedObjects(nextScene, false);
// Send notification back to server that we finished loading this scene
var responseSceneEventData = BeginSceneEvent();
responseSceneEventData.LoadSceneMode = loadSceneMode;
responseSceneEventData.SceneEventType = SceneEventType.LoadComplete;
responseSceneEventData.SceneHash = sceneEventData.ClientSceneHash;
var target = NetworkManager.ServerClientId;
if (NetworkManager.DistributedAuthorityMode)
{
responseSceneEventData.SenderClientId = NetworkManager.LocalClientId;
responseSceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner;
target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
}
var message = new SceneEventMessage
{
EventData = responseSceneEventData
};
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target);
NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size);
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);
// Check to see if we still have scenes to load and synchronize with
HandleClientSceneEvent(sceneEventId);
}
///
/// Makes sure that client-side instantiated dynamically spawned NetworkObjects are migrated
/// into the same scene (if not already) as they are on the server-side during the initial
/// client connection synchronization process.
///
private void SynchronizeNetworkObjectScene()
{
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
// This is only done for dynamically spawned NetworkObjects
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
// then skip it (it will reside in the currently active scene in this scenario on the client-side)
if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
{
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
// If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
// and move it to that scene.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
{
if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
{
var scene = ScenesLoaded[networkObject.SceneOriginHandle];
if (scene == DontDestroyOnLoadScene)
{
Debug.Log($"{networkObject.gameObject.name} migrating into DDOL!");
}
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
else if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
$"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
}
}
}
}
}
///
/// Client Side:
/// Handles incoming Scene_Event messages for clients
///
/// data associated with the event
private void HandleClientSceneEvent(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
switch (sceneEventData.SceneEventType)
{
case SceneEventType.ActiveSceneChanged:
{
if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
{
var scene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
if (scene.isLoaded)
{
SceneManager.SetActiveScene(scene);
}
}
EndSceneEvent(sceneEventId);
break;
}
case SceneEventType.ObjectSceneChanged:
{
EndSceneEvent(sceneEventId);
break;
}
case SceneEventType.Load:
{
OnClientSceneLoadingEvent(sceneEventId);
break;
}
case SceneEventType.Unload:
{
OnClientUnloadScene(sceneEventId);
break;
}
case SceneEventType.Synchronize:
{
if (!sceneEventData.IsDoneWithSynchronization())
{
OnClientBeginSync(sceneEventId);
}
else
{
// Include anything in the DDOL scene
PopulateScenePlacedObjects(DontDestroyOnLoadScene, false);
// If needed, set the currently active scene
if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
{
var targetActiveScene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
if (targetActiveScene.isLoaded && targetActiveScene.handle != SceneManager.GetActiveScene().handle)
{
SceneManager.SetActiveScene(targetActiveScene);
}
}
// Spawn and Synchronize all NetworkObjects
sceneEventData.SynchronizeSceneNetworkObjects(NetworkManager);
// If needed, migrate dynamically spawned NetworkObjects to the same scene as they are on the server
SynchronizeNetworkObjectScene();
// Process any pending create object messages that the client received during synchronization
ProcessDeferredCreateObjectMessages();
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
if (NetworkManager.DistributedAuthorityMode)
{
if (NetworkManager.CMBServiceConnection)
{
foreach (var clientId in NetworkManager.ConnectedClientsIds)
{
if (clientId == NetworkManager.LocalClientId)
{
continue;
}
sceneEventData.TargetClientId = clientId;
sceneEventData.SenderClientId = NetworkManager.LocalClientId;
var message = new SceneEventMessage
{
EventData = sceneEventData,
};
var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target);
NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size);
}
}
else
{
sceneEventData.TargetClientId = NetworkManager.CurrentSessionOwner;
sceneEventData.SenderClientId = NetworkManager.LocalClientId;
var message = new SceneEventMessage
{
EventData = sceneEventData,
};
var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId;
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target);
NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size);
}
}
else
{
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
}
// All scenes are synchronized, let the server know we are done synchronizing
NetworkManager.IsConnectedClient = true;
// With distributed authority, either the client-side automatically spawns the default assigned player prefab or
// if AutoSpawnPlayerPrefabClientSide is disabled the client-side will determine what player prefab to spawn and
// when it gets spawned.
if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide)
{
NetworkManager.ConnectionManager.CreateAndSpawnPlayer(NetworkManager.LocalClientId);
}
// Client is now synchronized and fully "connected". This also means the client can send "RPCs" at this time
NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(NetworkManager.LocalClientId);
// Notify the client that they have finished synchronizing
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
ClientId = NetworkManager.LocalClientId, // Client sent this to the server
});
// Process any SceneEventType.ObjectSceneChanged messages that
// were deferred while synchronizing and migrate the associated
// NetworkObjects to their newly assigned scenes.
sceneEventData.ProcessDeferredObjectSceneChangedEvents();
// Only if PostSynchronizationSceneUnloading is set and we are running in client synchronization
// mode additive do we unload any remaining scene that was not synchronized (otherwise any loaded
// scene not synchronized by the server will remain loaded)
if (PostSynchronizationSceneUnloading && ClientSynchronizationMode == LoadSceneMode.Additive)
{
SceneManagerHandler.UnloadUnassignedScenes(NetworkManager);
}
OnSynchronizeComplete?.Invoke(NetworkManager.LocalClientId);
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo($"[Client-{NetworkManager.LocalClientId}][Scene Management Enabled] Synchronization complete!");
}
// For convenience, notify all NetworkBehaviours that synchronization is complete.
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
networkObject.InternalNetworkSessionSynchronized();
}
if (NetworkManager.DistributedAuthorityMode && HasSceneAuthority() && IsRestoringSession)
{
IsRestoringSession = false;
PostSynchronizationSceneUnloading = m_OriginalPostSynchronizationSceneUnloading;
}
EndSceneEvent(sceneEventId);
}
break;
}
case SceneEventType.ReSynchronize:
{
// Notify the local client that they have been re-synchronized after being synchronized with an in progress game session
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
ClientId = NetworkManager.ServerClientId, // Server sent this to client
});
EndSceneEvent(sceneEventId);
break;
}
case SceneEventType.LoadEventCompleted:
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);
}
EndSceneEvent(sceneEventId);
break;
}
default:
{
Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!");
break;
}
}
}
///
/// Session Owner Side:
/// Handles incoming Scene_Event messages for the current session owner
///
private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
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:
{
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;
}
case SceneEventType.SynchronizeComplete:
{
// At this point the client is considered fully "connected"
if ((NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClient.IsSessionOwner) || !NetworkManager.DistributedAuthorityMode)
{
if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
{
// DANGO-EXP TODO: Remove this once service is sending the synchronization message to all clients
if (NetworkManager.ConnectedClients.ContainsKey(clientId) && NetworkManager.ConnectionManager.ConnectedClientIds.Contains(clientId) && NetworkManager.ConnectedClientsList.Contains(NetworkManager.ConnectedClients[clientId]))
{
EndSceneEvent(sceneEventId);
return;
}
NetworkManager.ConnectionManager.AddClient(clientId);
}
// Notify the local server that a client has finished synchronizing
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
SceneName = string.Empty,
ClientId = clientId
});
if (NetworkManager.ConnectedClients.ContainsKey(clientId))
{
NetworkManager.ConnectedClients[clientId].IsConnected = true;
}
}
else
{
// DANGO-EXP TODO: Remove this once service distributes objects
// Non-session owners receive this notification from newly connected clients and upon receiving
// the event they will redistribute their NetworkObjects
NetworkManager.SpawnManager.DistributeNetworkObjects(clientId);
EndSceneEvent(sceneEventId);
return;
}
// All scenes are synchronized, let the server know we are done synchronizing
OnSynchronizeComplete?.Invoke(clientId);
// At this time the client is fully synchronized with all loaded scenes and
// NetworkObjects and should be considered "fully connected". Send the
// notification that the client is connected.
// TODO 2023: We should have a better name for this or have multiple states the
// client progresses through (the name and associated legacy behavior/expected state
// of the client was persisted since MLAPI)
NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(clientId);
if (NetworkManager.IsHost)
{
NetworkManager.ConnectionManager.InvokeOnPeerConnectedCallback(clientId);
}
// Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid
// a potential crash within the MessageSystem (i.e. sending to a client that no longer exists)
if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization && NetworkManager.ConnectedClients.ContainsKey(clientId))
{
sceneEventData.SceneEventType = SceneEventType.ReSynchronize;
SendSceneEventData(sceneEventId, new ulong[] { clientId });
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
SceneName = string.Empty,
ClientId = clientId
});
}
// DANGO-EXP TODO: Remove this once service distributes objects
NetworkManager.SpawnManager.DistributeNetworkObjects(clientId);
EndSceneEvent(sceneEventId);
break;
}
default:
{
Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!");
break;
}
}
}
///
/// Skips scene handling to be able to test CMB DA_NGO Codec tests
///
internal bool SkipSceneHandling;
private bool m_OriginalPostSynchronizationSceneUnloading;
///
/// Both Client and Server: Incoming scene event entry point
///
/// client who sent the scene event
/// data associated with the scene event
internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
{
if (NetworkManager != null)
{
var sceneEventData = BeginSceneEvent();
sceneEventData.Deserialize(reader);
if (SkipSceneHandling)
{
return;
}
// DA HOST Will keep track of session owner and if it is not the scene owner it will forward the message
// to the current session owner
if (NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost)
{
// If the event is server directed
if (!sceneEventData.IsSceneEventClientSide())
{
// If the DAHost is not the session owner, then forward the message to the current session owner
if (NetworkManager.CurrentSessionOwner != NetworkManager.LocalClientId)
{
var message = new SceneEventMessage()
{
EventData = sceneEventData,
};
// Forward synchronization to client then exit early because DAHost is not the current session owner
NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.CurrentSessionOwner);
EndSceneEvent(sceneEventData.SceneEventId);
return;
}
}
else
{
// DAHost will forward any messages not targeting the DAHost to the targeted client
if (sceneEventData.TargetClientId != NetworkManager.LocalClientId)
{
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogInfoServer($"[Forward To: Client-{sceneEventData.TargetClientId}][{Enum.GetName(typeof(SceneEventType), sceneEventData.SceneEventType)}]");
}
sceneEventData.ForwardSynchronization = sceneEventData.SceneEventType == SceneEventType.Synchronize;
sceneEventData.IsForwarding = true;
var message = new SceneEventMessage()
{
EventData = sceneEventData,
};
NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, sceneEventData.TargetClientId);
EndSceneEvent(sceneEventData.SceneEventId);
return;
}
}
}
NetworkManager.NetworkMetrics.TrackSceneEventReceived(
clientId, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), reader.Length);
if (sceneEventData.IsSceneEventClientSide())
{
// If the client is being synchronized for the first time do some initialization
if (sceneEventData.SceneEventType == SceneEventType.Synchronize)
{
ScenePlacedObjects.Clear();
// Set the server's configured client synchronization mode on the client side
ClientSynchronizationMode = sceneEventData.ClientSynchronizationMode;
// Only if ClientSynchronizationMode is Additive and the client receives a synchronize scene event
if (ClientSynchronizationMode == LoadSceneMode.Additive)
{
if (NetworkManager.DistributedAuthorityMode && HasSceneAuthority() && IsRestoringSession && clientId == NetworkManager.ServerClientId)
{
m_OriginalPostSynchronizationSceneUnloading = PostSynchronizationSceneUnloading;
PostSynchronizationSceneUnloading = true;
}
// Check for scenes already loaded and create a table of scenes already loaded (SceneEntries) that will be
// used if the server is synchronizing the same scenes (i.e. if a matching scene is already loaded on the
// client side, then that scene will be used as opposed to loading another scene). This allows for clients
// to reconnect to a network session without having to unload all of the scenes and reload all of the scenes.
SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager);
}
}
HandleClientSceneEvent(sceneEventData.SceneEventId);
}
else
{
var sendingClient = clientId;
if (NetworkManager.DistributedAuthorityMode)
{
sendingClient = sceneEventData.SenderClientId;
}
HandleSessionOwnerEvent(sceneEventData.SceneEventId, sendingClient);
}
}
else
{
Debug.LogError($"{nameof(HandleSceneEvent)} was invoked but {nameof(Netcode.NetworkManager)} reference was null!");
}
}
///
/// Moves all NetworkObjects that don't have the set to
/// the "Do not destroy on load" scene.
///
internal void MoveObjectsToDontDestroyOnLoad()
{
// Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
// are despawned.
var localSpawnedObjectsHashSet = new HashSet(NetworkManager.SpawnManager.SpawnedObjectsList);
foreach (var networkObject in localSpawnedObjectsHashSet)
{
if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
{
continue;
}
// Only NetworkObjects marked to not be destroyed with the scene
if (!networkObject.DestroyWithScene)
{
// Only move dynamically spawned NetworkObjects with no parent as the children will follow
if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
// When temporarily migrating to the DDOL, adjust the network and origin scene handles so no messages are generated
// about objects being moved to a new scene.
networkObject.NetworkSceneHandle = ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle];
networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle;
}
}
else if (networkObject.HasAuthority)
{
networkObject.Despawn();
}
}
}
///
/// Should be invoked on both the client and server side after:
/// -- A new scene has been loaded
/// -- Before any "DontDestroyOnLoad" NetworkObjects have been added back into the scene.
/// Added the ability to choose not to clear the scene placed objects for additive scene loading.
/// We organize our ScenePlacedObjects by:
/// [GlobalObjectIdHash][SceneHandle][NetworkObject]
/// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to
/// distinguish between duplicate in-scene placed NetworkObjects
///
internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true)
{
if (clearScenePlacedObjects)
{
ScenePlacedObjects.Clear();
}
#if UNITY_2023_1_OR_NEWER
var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID);
#else
var networkObjects = UnityEngine.Object.FindObjectsOfType();
#endif
// Just add every NetworkObject found that isn't already in the list
// With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value
// During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then
// at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects
foreach (var networkObjectInstance in networkObjects)
{
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == NetworkManager ||
networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary());
}
if (!ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle))
{
ScenePlacedObjects[globalObjectIdHash].Add(sceneHandle, networkObjectInstance);
}
else
{
var exitingEntryName = ScenePlacedObjects[globalObjectIdHash][sceneHandle] != null ? ScenePlacedObjects[globalObjectIdHash][sceneHandle].name : "Null Entry";
throw new Exception($"{networkObjectInstance.name} tried to registered with {nameof(ScenePlacedObjects)} which already contains " +
$"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!");
}
}
}
}
///
/// Moves all spawned NetworkObjects (from do not destroy on load) to the scene specified
///
/// scene to move the NetworkObjects to
internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
{
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
if (networkObject == null)
{
continue;
}
// If it is in the DDOL then
if (networkObject.gameObject.scene == DontDestroyOnLoadScene && !networkObject.DestroyWithScene)
{
// only move dynamically spawned network objects, with no parent as child objects will follow,
// back into the currently active scene
if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
if (NetworkManager.DistributedAuthorityMode)
{
// When migrating out of the DDOL to the currently active scene, adjust the network and origin scene handles so no messages are generated
// about objects being moved to a new scene.
if (SceneManagerHandler.IsIntegrationTest() && SceneManager.GetActiveScene() == scene)
{
networkObject.NetworkSceneHandle = scene.handle;
}
else
{
networkObject.NetworkSceneHandle = ClientSceneHandleToServerSceneHandle[scene.handle];
}
networkObject.SceneOriginHandle = scene.handle;
}
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
}
}
}
///
/// Holds a list of scene handles (server-side relative) and NetworkObjects migrated into it
/// during the current frame.
///
internal Dictionary>> ObjectsMigratedIntoNewScene = new Dictionary>>();
internal bool IsSceneEventInProgress()
{
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
{
return false;
}
foreach (var sceneEventEntry in SceneEventProgressTracking)
{
if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.SceneEventType != SceneEventType.Synchronize && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
{
return true;
}
}
return false;
}
///
/// Handles notifying clients when a NetworkObject has been migrated into a new scene
///
internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject)
{
// Really, this should never happen but in case it does
if (!networkObject.HasAuthority)
{
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] A client is trying to notify of an object's scene change!");
}
return;
}
// Ignore in-scene placed NetworkObjects
if (networkObject.IsSceneObject != false)
{
// Really, this should ever happen but in case it does
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] Trying to notify in-scene placed object scene change!");
}
return;
}
// Ignore if the scene is the currently active scene and the NetworkObject is auto synchronizing/migrating
// to the currently active scene.
if (networkObject.gameObject.scene == SceneManager.GetActiveScene() && networkObject.ActiveSceneSynchronization)
{
return;
}
// Don't notify if a scene event is in progress
// Note: This does not apply to SceneEventType.Synchronize since synchronization isn't a global connected client event.
if (IsSceneEventInProgress())
{
return;
}
// Otherwise, add the NetworkObject into the list of NetworkObjects who's scene has changed
if (!ObjectsMigratedIntoNewScene.ContainsKey(networkObject.NetworkSceneHandle))
{
ObjectsMigratedIntoNewScene.Add(networkObject.NetworkSceneHandle, new Dictionary>());
}
if (!ObjectsMigratedIntoNewScene[networkObject.NetworkSceneHandle].ContainsKey(NetworkManager.LocalClientId))
{
ObjectsMigratedIntoNewScene[networkObject.NetworkSceneHandle].Add(NetworkManager.LocalClientId, new List());
}
ObjectsMigratedIntoNewScene[networkObject.NetworkSceneHandle][NetworkManager.LocalClientId].Add(networkObject);
}
///
/// Invoked by clients when processing a event
/// or invoked by when a client finishes
/// synchronization.
///
internal void MigrateNetworkObjectsIntoScenes()
{
try
{
foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
{
if (ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEntry.Key))
{
var clientSceneHandle = ServerSceneHandleToClientSceneHandle[sceneEntry.Key];
foreach (var ownerEntry in sceneEntry.Value)
{
if (ownerEntry.Key == NetworkManager.LocalClientId)
{
continue;
}
if (ScenesLoaded.ContainsKey(clientSceneHandle))
{
var scene = ScenesLoaded[clientSceneHandle];
foreach (var networkObject in ownerEntry.Value)
{
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
networkObject.NetworkSceneHandle = sceneEntry.Key;
networkObject.SceneOriginHandle = scene.handle;
}
}
}
}
}
}
catch (Exception ex)
{
NetworkLog.LogErrorServer($"{ex.Message}\n Stack Trace:\n {ex.StackTrace}");
}
}
private List m_ScenesToRemoveFromObjectMigration = new List();
///
/// Should be invoked during PostLateUpdate just prior to the NetworkMessageManager processes its outbound message queue.
///
internal void CheckForAndSendNetworkObjectSceneChanged()
{
// Early exit if not the server or there is nothing pending
if (ObjectsMigratedIntoNewScene.Count == 0)
{
return;
}
MigrateNetworkObjectsIntoScenes();
// Double check that the NetworkObjects to migrate still exist
m_ScenesToRemoveFromObjectMigration.Clear();
foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
{
if (!sceneEntry.Value.ContainsKey(NetworkManager.LocalClientId))
{
continue;
}
var ownerSceneEntry = sceneEntry.Value[NetworkManager.LocalClientId];
for (int i = sceneEntry.Value[NetworkManager.LocalClientId].Count - 1; i >= 0; i--)
{
// Remove NetworkObjects that are no longer spawned
if (!sceneEntry.Value[NetworkManager.LocalClientId][i].IsSpawned)
{
sceneEntry.Value[NetworkManager.LocalClientId].RemoveAt(i);
}
}
// If the scene entry no longer has any NetworkObjects to migrate
// then add it to the list of scenes to be removed from the table
// of scenes containing NetworkObjects to migrate.
if (sceneEntry.Value.Count == 0)
{
m_ScenesToRemoveFromObjectMigration.Add(sceneEntry.Key);
}
}
// Remove owner sceneHandle entries that no longer have any NetworkObjects remaining
foreach (var sceneHandle in m_ScenesToRemoveFromObjectMigration)
{
ObjectsMigratedIntoNewScene[sceneHandle].Remove(NetworkManager.LocalClientId);
}
var localOwnerHasEntries = false;
foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
{
if (sceneEntry.Value.ContainsKey(NetworkManager.LocalClientId))
{
localOwnerHasEntries = true;
break;
}
}
// If the local owner has no entries, then exit
if (!localOwnerHasEntries)
{
ObjectsMigratedIntoNewScene.Clear();
return;
}
// Some NetworkObjects still exist, send the message
var sceneEvent = BeginSceneEvent();
sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray());
ObjectsMigratedIntoNewScene.Clear();
EndSceneEvent(sceneEvent.SceneEventId);
}
// Used to handle client-side scene migration messages received while
// a client is synchronizing
internal struct DeferredObjectsMovedEvent
{
internal ulong OwnerId;
internal Dictionary> ObjectsMigratedTable;
}
internal List DeferredObjectsMovedEvents = new List();
internal struct DeferredObjectCreation
{
internal ulong SenderId;
internal uint MessageSize;
// When we transfer session owner and we are using a DAHost, this will be pertinent (otherwise it is not when connected to a DA service)
internal ulong[] ObserverIds;
internal ulong[] NewObserverIds;
internal NetworkObject.SceneObject SceneObject;
internal FastBufferReader FastBufferReader;
}
internal List DeferredObjectCreationList = new List();
internal int DeferredObjectCreationCount;
// The added clientIds is specific to DAHost when session ownership changes and a normal client is controlling scene loading
internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader, ulong[] observerIds, ulong[] newObserverIds)
{
var deferredObjectCreationEntry = new DeferredObjectCreation()
{
SenderId = senderId,
MessageSize = messageSize,
ObserverIds = observerIds,
NewObserverIds = newObserverIds,
SceneObject = sceneObject,
};
unsafe
{
deferredObjectCreationEntry.FastBufferReader = new FastBufferReader(fastBufferReader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, fastBufferReader.Length - fastBufferReader.Position);
}
DeferredObjectCreationList.Add(deferredObjectCreationEntry);
}
private void ProcessDeferredCreateObjectMessages()
{
// If no pending create object messages exit early
if (DeferredObjectCreationList.Count == 0)
{
return;
}
var networkManager = NetworkManager;
// Process all deferred create object messages.
for (int i = 0; i < DeferredObjectCreationList.Count; i++)
{
var deferredObjectCreation = DeferredObjectCreationList[i];
CreateObjectMessage.CreateObject(ref networkManager, ref deferredObjectCreation);
}
DeferredObjectCreationCount = DeferredObjectCreationList.Count;
DeferredObjectCreationList.Clear();
}
public enum MapTypes
{
ServerToClient,
ClientToServer
}
public struct SceneMap : INetworkSerializable
{
public MapTypes MapType;
public Scene Scene;
public bool ScenePresent;
public string SceneName;
public int ServerHandle;
public int MappedLocalHandle;
public int LocalHandle;
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref MapType);
serializer.SerializeValue(ref ScenePresent);
if (serializer.IsReader)
{
SceneName = "Not Present";
}
if (ScenePresent)
{
serializer.SerializeValue(ref SceneName);
serializer.SerializeValue(ref LocalHandle);
}
serializer.SerializeValue(ref ServerHandle);
serializer.SerializeValue(ref MappedLocalHandle);
}
}
public List GetSceneMapping(MapTypes mapType)
{
var mapping = new List();
if (mapType == MapTypes.ServerToClient)
{
foreach (var entry in ServerSceneHandleToClientSceneHandle)
{
var scene = ScenesLoaded[entry.Value];
var sceneIsPresent = scene.IsValid() && scene.isLoaded;
var sceneMap = new SceneMap()
{
MapType = mapType,
ServerHandle = entry.Key,
MappedLocalHandle = entry.Value,
LocalHandle = scene.handle,
Scene = scene,
ScenePresent = sceneIsPresent,
SceneName = sceneIsPresent ? scene.name : "NotPresent",
};
mapping.Add(sceneMap);
}
}
else
{
foreach (var entry in ClientSceneHandleToServerSceneHandle)
{
var scene = ScenesLoaded[entry.Key];
var sceneIsPresent = scene.IsValid() && scene.isLoaded;
var sceneMap = new SceneMap()
{
MapType = mapType,
ServerHandle = entry.Value,
MappedLocalHandle = entry.Key,
LocalHandle = scene.handle,
Scene = scene,
ScenePresent = sceneIsPresent,
SceneName = sceneIsPresent ? scene.name : "NotPresent",
};
mapping.Add(sceneMap);
}
}
return mapping;
}
}
}