using FishNet.Connection; using FishNet.Managing.Client; using FishNet.Managing.Logging; using FishNet.Managing.Server; using FishNet.Object; using FishNet.Serializing.Helping; using FishNet.Transporting; using FishNet.Utility.Extension; using FishNet.Utility.Performance; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; using UnitySceneManager = UnityEngine.SceneManagement.SceneManager; namespace FishNet.Managing.Scened { /// /// Handles loading, unloading, and scene visibility for clients. /// [DisallowMultipleComponent] [AddComponentMenu("FishNet/Manager/SceneManager")] public sealed class SceneManager : MonoBehaviour { #region Types. internal enum LightProbeUpdateType { Asynchronous = 0, BlockThread = 1, Off = 2, } #endregion #region Public. /// /// Called after the active scene has been set, immediately after scene loads. This will occur before NetworkBehaviour callbacks run for the scene's objects. /// public event Action OnActiveSceneSet; /// /// Called when a client loads initial scenes after connecting. Boolean will be true if asServer. /// public event Action OnClientLoadedStartScenes; /// /// Called when a scene change queue has begun. This will only call if a scene has succesfully begun to load or unload. The queue may process any number of scene events. For example: if a scene is told to unload while a load is still in progress, then the unload will be placed in the queue. /// public event Action OnQueueStart; /// /// Called when the scene queue is emptied. /// public event Action OnQueueEnd; /// /// Called when a scene load starts. /// public event Action OnLoadStart; /// /// Called when completion percentage changes while loading a scene. Value is between 0f and 1f, while 1f is 100% done. Can be used for custom progress bars when loading scenes. /// public event Action OnLoadPercentChange; /// /// Called when a scene load ends. /// public event Action OnLoadEnd; /// /// Called when a scene unload starts. /// public event Action OnUnloadStart; /// /// Called when a scene unload ends. /// public event Action OnUnloadEnd; /// /// Called when a client presence changes within a scene, before the server rebuilds observers. /// public event Action OnClientPresenceChangeStart; /// /// Called when a client presence changes within a scene, after the server rebuilds observers. /// public event Action OnClientPresenceChangeEnd; /// /// Connections within each scene. /// public Dictionary> SceneConnections { get; private set; } = new Dictionary>(); #endregion #region Internal. /// /// Called after the active scene has been set, immediately after scene loads. /// internal event Action OnActiveSceneSetInternal; #endregion #region Serialized. /// /// Script to handle addressables loading and unloading. This field may be blank if addressables are not being used. /// [Tooltip("Script to handle addressables loading and unloading. This field may be blank if addressables are not being used.")] [SerializeField] private SceneProcessorBase _sceneProcessor; /// /// How to update light probes after loading or unloading scenes. /// [Tooltip("How to update light probes after loading or unloading scenes.")] [SerializeField] private LightProbeUpdateType _lightProbeUpdating = LightProbeUpdateType.Asynchronous; /// /// True to move objects visible to clientHost that are within an unloading scene. This ensures the objects are despawned on the client side rather than when the scene is destroyed. /// [Tooltip("True to move objects visible to clientHost that are within an unloading scene. This ensures the objects are despawned on the client side rather than when the scene is destroyed.")] [SerializeField] private bool _moveClientHostObjects = true; /// /// True to automatically set active scenes when loading and unloading scenes. /// [Tooltip("True to automatically set active scenes when loading and unloading scenes.")] [SerializeField] private bool _setActiveScene = true; #endregion #region Private. /// /// NetworkManager for this script. /// private NetworkManager _networkManager; /// /// ServerManager for this script. /// private ServerManager _serverManager => _networkManager.ServerManager; /// /// ClientManager for this script. /// private ClientManager _clientManager => _networkManager.ClientManager; /// /// Scenes which are currently loaded as networked scenes. All players should have networked scenes loaded. /// private string[] _globalScenes = new string[0]; /// /// Lastest SceneLoadData for a global load. /// private SceneLoadData _globalSceneLoadData = new SceneLoadData(); /// /// Scenes to load or unload, in order. /// private List _queuedOperations = new List(); /// /// Scenes which must be manually unloaded, even when emptied. /// private HashSet _manualUnloadScenes = new HashSet(); /// /// Scene containing moved objects when changing single scene. On client this will contain all objects moved until the server destroys them. /// The network only sends spawn messages once per-client, per server side scene load. If a scene load is performed only for specific connections /// then the server is not resetting their single scene, but rather the single scene for those connections only. Because of this, any objects /// which are to be moved will not receive a second respawn message, as they are never destroyed on server, only on client. /// While on server only this scene contains objects being moved temporarily, before being moved to the new scene. /// private Scene _movedObjectsScene; /// /// Scene containing objects awaiting to be destroyed by the client-host. /// This is required when unloading scenes where the client-host has visibility. /// Otherwise the objects would become destroyed when the scene unloads on the server /// which would cause missing networkobjects on clients when receiving despawn messages. /// private Scene _delayedDestroyScene; /// /// A scene to be set as the active scene where there are no global scenes. /// This is used to prevent connection scenes and MovedObjectsScene from becoming the active scene. /// private Scene _fallbackActiveScene; /// /// Becomes true when when a scene first successfully begins to load or unload. Value is reset to false when the scene queue is emptied. /// private bool _sceneQueueStartInvoked; /// /// Objects being moved from MovedObjects scene to another. /// private List _movingObjects = new List(); /// /// How many scene load confirmations the server is expecting from a client. /// Unloads do not need to be checked because server does not require confirmation for those. /// This is used to prevent attacks. /// private Dictionary _pendingClientSceneChanges = new Dictionary(); #endregion #region Consts. /// /// String to use when scene data used to load is invalid. /// private const string INVALID_SCENELOADDATA = "One or more datas in SceneLoadData are invalid.This generally occurs when calling this method without specifying any scenes or when data fields are null."; /// /// String to use when scene data used to unload is invalid. /// private const string INVALID_SCENEUNLOADDATA = "One or more datas in SceneLoadData are invalid.This generally occurs when calling this method without specifying any scenes or when data fields are null."; #endregion #region Unity callbacks and initialization. private void Awake() { UnitySceneManager.sceneUnloaded += SceneManager_SceneUnloaded; if (_sceneProcessor == null) _sceneProcessor = gameObject.AddComponent(); _sceneProcessor.Initialize(this); } private void Start() { //No need to unregister since managers are on the same object. _networkManager.ServerManager.OnRemoteConnectionState += ServerManager_OnRemoteConnectionState; _networkManager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState; _clientManager.RegisterBroadcast(OnLoadScenes); _clientManager.RegisterBroadcast(OnUnloadScenes); _serverManager.RegisterBroadcast(OnClientLoadedScenes); _clientManager.RegisterBroadcast(OnClientEmptyStartScenes); _serverManager.RegisterBroadcast(OnServerEmptyStartScenes); } private void OnDestroy() { UnitySceneManager.sceneUnloaded -= SceneManager_SceneUnloaded; } /// /// Called when the server connection state changes. /// private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj) { //If no servers are started. if (!_networkManager.ServerManager.AnyServerStarted()) ResetValues(); } /// /// Resets as if first use. /// private void ResetValues() { SceneConnections.Clear(); _globalScenes = new string[0]; _globalSceneLoadData = new SceneLoadData(); _queuedOperations.Clear(); _manualUnloadScenes.Clear(); _sceneQueueStartInvoked = false; _movingObjects.Clear(); } /// /// Called when a connection state changes for a remote client. /// private void ServerManager_OnRemoteConnectionState(NetworkConnection arg1, RemoteConnectionStateArgs arg2) { if (arg2.ConnectionState == RemoteConnectionState.Stopped) ClientDisconnected(arg1); } /// /// Initializes this script for use. /// /// internal void InitializeOnceInternal(NetworkManager manager) { _networkManager = manager; } /// /// Received when a scene is unloaded. /// /// private void SceneManager_SceneUnloaded(Scene scene) { if (!_networkManager.IsServer) return; /* Remove any unloaded scenes from local variables. This shouldn't * be needed if the user properly utilizes this scene manager, * but just incase, we don't want a memory leak. */ SceneConnections.Remove(scene); _manualUnloadScenes.Remove(scene); RemoveFromGlobalScenes(scene); } #endregion #region Initial synchronizing. /// /// Invokes OnClientLoadedStartScenes if connection just loaded start scenes. /// /// private void TryInvokeLoadedStartScenes(NetworkConnection connection, bool asServer) { if (connection.SetLoadedStartScenes(asServer)) OnClientLoadedStartScenes?.Invoke(connection, asServer); } /// /// Called when authenitcator has concluded a result for a connection. Boolean is true if authentication passed, false if failed. This invokes before OnClientAuthenticated so FishNet may run operations on authenticated clients before user code does. /// /// internal void OnClientAuthenticated(NetworkConnection connection) { AddPendingLoad(connection); //No global scenes to load. if (_globalScenes.Length == 0) { EmptyStartScenesBroadcast msg = new EmptyStartScenesBroadcast(); connection.Broadcast(msg); } else { SceneLoadData sld = new SceneLoadData(_globalScenes); sld.Params = _globalSceneLoadData.Params; sld.Options = _globalSceneLoadData.Options; sld.ReplaceScenes = _globalSceneLoadData.ReplaceScenes; LoadQueueData qd = new LoadQueueData(SceneScopeType.Global, new NetworkConnection[0], sld, _globalScenes, false); //Send message to load the networked scenes. LoadScenesBroadcast msg = new LoadScenesBroadcast() { QueueData = qd }; connection.Broadcast(msg, true); } } /// /// Received on client when the server has no start scenes. /// private void OnClientEmptyStartScenes(EmptyStartScenesBroadcast msg) { _clientManager.Broadcast(msg); } /// /// Received on server when client confirms there are no start scenes. /// private void OnServerEmptyStartScenes(NetworkConnection conn, EmptyStartScenesBroadcast msg) { //Already received, shouldn't be happening again. if (conn.LoadedStartScenes) { if (_networkManager.CanLog(LoggingType.Common)) Debug.LogError($"Received multiple EmptyStartSceneBroadcast from connectionId {conn.ClientId}. Connection will be kicked immediately."); _networkManager.TransportManager.Transport.StopConnection(conn.ClientId, true); } else { OnClientLoadedScenes(conn, new ClientScenesLoadedBroadcast()); } } #endregion #region Player disconnect. /// /// Received when a player disconnects from the server. /// /// //finish. private void ClientDisconnected(NetworkConnection conn) { _pendingClientSceneChanges.Remove(conn); /* Remove connection from all scenes. While doing so check * if scene should be unloaded provided there are no more clients * in the scene, and it's set to automatically unload. This situation is a bit * unique since a client disconnect happens outside the manager, so there * isn't much code we can re-use to perform this operation. */ List scenesToUnload = new List(); //Current active scene. Scene activeScene = UnitySceneManager.GetActiveScene(); foreach (KeyValuePair> item in SceneConnections) { Scene scene = item.Key; HashSet hs = item.Value; bool removed = hs.Remove(conn); /* If no more observers for scene, not a global scene, and not to be manually unloaded * then remove scene from SceneConnections and unload it. */ if (removed && hs.Count == 0 && !IsGlobalScene(scene) && !_manualUnloadScenes.Contains(scene) && (scene != activeScene)) scenesToUnload.Add(scene); } //If scenes should be unloaded. if (scenesToUnload.Count > 0) { foreach (Scene s in scenesToUnload) SceneConnections.Remove(s); SceneUnloadData sud = new SceneUnloadData(SceneLookupData.CreateData(scenesToUnload)); UnloadConnectionScenes(new NetworkConnection[0], sud); } } #endregion #region Server received messages. /// /// Received on server when a client loads scenes. /// /// /// private void OnClientLoadedScenes(NetworkConnection conn, ClientScenesLoadedBroadcast msg) { int pendingLoads; _pendingClientSceneChanges.TryGetValueIL2CPP(conn, out pendingLoads); //There's no loads or unloads pending, kick client. if (pendingLoads == 0) { if (_networkManager.CanLog(LoggingType.Common)) Debug.LogError($"Received excessive ClientScenesLoadedBroadcast from connectionId {conn.ClientId}. Connection will be kicked immediately."); _networkManager.TransportManager.Transport.StopConnection(conn.ClientId, true); return; } //If there is a load pending then update pending count. else { pendingLoads--; if (pendingLoads == 0) _pendingClientSceneChanges.Remove(conn); else _pendingClientSceneChanges[conn] = pendingLoads; } if (!Comparers.IsDefault(msg)) { foreach (SceneLookupData item in msg.SceneLookupDatas) { Scene s = item.GetScene(out _); if (s.IsValid()) AddConnectionToScene(conn, s); } } TryInvokeLoadedStartScenes(conn, true); } #endregion #region Events. /// /// Checks if OnQueueStart should invoke, and if so invokes. /// private void TryInvokeOnQueueStart() { if (_sceneQueueStartInvoked) return; _sceneQueueStartInvoked = true; OnQueueStart?.Invoke(); } /// /// Checks if OnQueueEnd should invoke, and if so invokes. /// private void TryInvokeOnQueueEnd() { if (!_sceneQueueStartInvoked) return; _sceneQueueStartInvoked = false; OnQueueEnd?.Invoke(); } /// /// Invokes that a scene load has started. Only called when valid scenes will be loaded. /// /// private void InvokeOnSceneLoadStart(LoadQueueData qd) { TryInvokeOnQueueStart(); OnLoadStart?.Invoke(new SceneLoadStartEventArgs(qd)); } /// /// Invokes that a scene load has ended. Only called after a valid scene has loaded. /// /// private void InvokeOnSceneLoadEnd(LoadQueueData qd, List requestedLoadScenes, List loadedScenes, string[] unloadedSceneNames) { //Make new list to not destroy original data. List skippedScenes = requestedLoadScenes.ToList(); //Remove loaded scenes from requested scenes. for (int i = 0; i < loadedScenes.Count; i++) skippedScenes.Remove(loadedScenes[i].name); SceneLoadEndEventArgs args = new SceneLoadEndEventArgs(qd, skippedScenes.ToArray(), loadedScenes.ToArray(), unloadedSceneNames); OnLoadEnd?.Invoke(args); } /// /// Invokes that a scene unload has started. Only called when valid scenes will be unloaded. /// /// private void InvokeOnSceneUnloadStart(UnloadQueueData sqd) { TryInvokeOnQueueStart(); OnUnloadStart?.Invoke(new SceneUnloadStartEventArgs(sqd)); } /// /// Invokes that a scene unload has ended. Only called after a valid scene has unloaded. /// /// private void InvokeOnSceneUnloadEnd(UnloadQueueData sqd, List unloadedScenes) { int[] handles = new int[unloadedScenes.Count]; OnUnloadEnd?.Invoke(new SceneUnloadEndEventArgs(sqd, handles)); } /// /// Invokes when completion percentage changes while unloading or unloading a scene. Value is between 0f and 1f, while 1f is 100% done. /// /// private void InvokeOnScenePercentChange(LoadQueueData qd, float value) { value = Mathf.Clamp(value, 0f, 1f); SceneLoadPercentEventArgs slp = new SceneLoadPercentEventArgs(qd, value); OnLoadPercentChange?.Invoke(slp); } #endregion #region Scene queue processing. /// /// Queues a load or unload operation and starts queue if needed. /// /// private void QueueOperation(object data) { //Add to scene queue data. _queuedOperations.Add(data); /* If only one entry then scene operations are not currently in progress. * Should there be more than one entry then scene operations are already * occuring. The coroutine will automatically load in order. */ if (_queuedOperations.Count == 1) StartCoroutine(__ProcessSceneQueue()); } /// /// Processes queued scene operations. /// /// /// private IEnumerator __ProcessSceneQueue() { /* Queue start won't invoke unless a scene load or unload actually occurs. * For example: if a scene is already loaded, and nothing needs to be loaded, * queue start will not invoke. */ while (_queuedOperations.Count > 0) { //If a load scene. if (_queuedOperations[0] is LoadQueueData) yield return StartCoroutine(__LoadScenes()); //If an unload scene. else if (_queuedOperations[0] is UnloadQueueData) yield return StartCoroutine(__UnloadScenes()); if (_queuedOperations.Count > 0) _queuedOperations.RemoveAt(0); } TryInvokeOnQueueEnd(); } #endregion #region LoadScenes /// /// Loads scenes on the server and for all clients. Future clients will automatically load these scenes. /// /// Data about which scenes to load. public void LoadGlobalScenes(SceneLoadData sceneLoadData) { LoadGlobalScenesInternal(sceneLoadData, _globalScenes, true); } /// /// Adds to load scene queue. /// /// /// private void LoadGlobalScenesInternal(SceneLoadData sceneLoadData, string[] globalScenes, bool asServer) { if (!CanExecute(asServer, true)) return; if (SceneDataInvalid(sceneLoadData, true)) return; LoadQueueData lqd = new LoadQueueData(SceneScopeType.Global, new NetworkConnection[0], sceneLoadData, globalScenes, asServer); QueueOperation(lqd); } /// /// Loads scenes on server and tells connections to load them as well. Other connections will not load this scene. /// /// Connections to load scenes for. /// Data about which scenes to load. public void LoadConnectionScenes(NetworkConnection conn, SceneLoadData sceneLoadData) { LoadConnectionScenes(new NetworkConnection[] { conn }, sceneLoadData); } /// /// Loads scenes on server and tells connections to load them as well. Other connections will not load this scene. /// /// Connections to load scenes for. /// Data about which scenes to load. public void LoadConnectionScenes(NetworkConnection[] conns, SceneLoadData sceneLoadData) { LoadConnectionScenesInternal(conns, sceneLoadData, _globalScenes, true); } /// /// Loads scenes on server without telling clients to load the scenes. /// /// Data about which scenes to load. public void LoadConnectionScenes(SceneLoadData sceneLoadData) { LoadConnectionScenesInternal(new NetworkConnection[0], sceneLoadData, _globalScenes, true); } /// /// Adds to load scene queue. /// /// /// private void LoadConnectionScenesInternal(NetworkConnection[] conns, SceneLoadData sceneLoadData, string[] globalScenes, bool asServer) { if (!CanExecute(asServer, true)) return; if (SceneDataInvalid(sceneLoadData, true)) return; LoadQueueData lqd = new LoadQueueData(SceneScopeType.Connections, conns, sceneLoadData, globalScenes, asServer); QueueOperation(lqd); } /// /// Returns if a NetworkObject can be moved. /// /// /// private bool CanMoveNetworkObject(NetworkObject nob) { bool canLog = _networkManager.CanLog(LoggingType.Warning); //Null. if (nob == null) { if (canLog) Debug.LogWarning($"NetworkObject is null."); return false; } //Not networked. if (!nob.IsNetworked) { if (canLog) Debug.LogWarning($"NetworkObject {nob.name} cannot be moved as it is not networked."); return false; } //Not spawned. if (!nob.IsSpawned) { if (canLog) Debug.LogWarning($"NetworkObject {nob.name} canot be moved as it is not spawned."); return false; } //SceneObject. if (nob.IsSceneObject) { if (canLog) Debug.LogWarning($"NetworkObject {nob.name} cannot be moved as it is a scene object."); return false; } //Not root. if (nob.transform.parent != null) { if (canLog) Debug.LogWarning($"NetworkObject {nob.name} cannot be moved because it is not the root object. Unity can only move root objects between scenes."); return false; } return true; } /// /// Loads a connection scene queue data. This behaves just like a networked scene load except it sends only to the specified connections, and it always loads as an additive scene on server. /// /// private IEnumerator __LoadScenes() { LoadQueueData data = _queuedOperations[0] as LoadQueueData; //True if running as server. bool asServer = data.AsServer; //True if running as client, while network server is active. bool asHost = (!asServer && _networkManager.IsServer); //If connection went inactive. if (!ConnectionActive(asServer)) yield break; /* Scene sanity checks. */ if (data.SceneLoadData.SceneLookupDatas.Length == 0) { if (_networkManager.CanLog(LoggingType.Warning)) Debug.LogWarning($"No scenes specified to load."); yield break; } //True if replacing scenes with specified ones. ReplaceOption replaceScenes = data.SceneLoadData.ReplaceScenes; /* Immediately set new global scenes. If on client this is whatever * server passes in. This should be set even if scope type * is not global because clients might get a connection scene first. */ if (!asServer) { if (!asHost) _globalScenes = data.GlobalScenes; } /* However, if server, then only update global scenes if scope * is global. */ else if (asServer && data.ScopeType == SceneScopeType.Global) { _globalSceneLoadData = data.SceneLoadData; string[] names = data.SceneLoadData.SceneLookupDatas.GetNames(); //If replacing. if (replaceScenes != ReplaceOption.None) { _globalScenes = names; } //Add onto. else { int index = _globalScenes.Length; Array.Resize(ref _globalScenes, _globalScenes.Length + names.Length); Array.Copy(names, 0, _globalScenes, index, names.Length); } data.GlobalScenes = _globalScenes; } /* Scene queue data scenes. * All scenes in the scene queue data whether they will be loaded or not. */ List requestedLoadSceneNames = new List(); List requestedLoadSceneHandles = new List(); /* Make a null filled array. This will be populated * using loaded scenes, or already loaded (eg cannot be loaded) scenes. */ SceneLookupData[] broadcastLookupDatas = new SceneLookupData[data.SceneLoadData.SceneLookupDatas.Length]; /* LoadableScenes and SceneReferenceDatas. /* Will contain scenes which may be loaded. * Scenes might not be added to loadableScenes * if for example loadOnlyUnloaded is true and * the scene is already loaded. */ List loadableScenes = new List(); for (int i = 0; i < data.SceneLoadData.SceneLookupDatas.Length; i++) { SceneLookupData sld = data.SceneLoadData.SceneLookupDatas[i]; //Scene to load. bool byHandle; Scene s = sld.GetScene(out byHandle); //If found then add it to requestedLoadScenes. if (s.IsValid()) { requestedLoadSceneNames.Add(s.name); if (byHandle) requestedLoadSceneHandles.Add(s.handle); } if (CanLoadScene(data, sld)) { //Don't load if as host, server side would have loaded already. if (!asHost) loadableScenes.Add(sld); } //Only the server needs to find scene handles to send to client. Client will send these back to the server. else if (asServer) { /* If here then scene cannot be loaded, which * can only happen if the scene already exists. * Find the scene using sld and set to datas. */ /* Set at the index of i. This way should the current * SLD not be the first scene it won't fill the * first slot in broadcastLookupDatas. This is important * because the first slot is used for the single scene * when using replace scenes. */ broadcastLookupDatas[i] = new SceneLookupData(s); } } /* Move identities * to holder scene to preserve them. * Required if a single scene is specified. Cannot rely on * loadSingleScene since it is only true if the single scene * must be loaded, which may be false if it's already loaded on * the server. */ //Do not run if running as client, and server is active. This would have already run as server. if (!asHost) { foreach (NetworkObject nob in data.SceneLoadData.MovedNetworkObjects) { //NetworkObject might be null if client lost observation of it. if (nob != null && CanMoveNetworkObject(nob)) UnitySceneManager.MoveGameObjectToScene(nob.gameObject, GetMovedObjectsScene()); } } /* Resetting SceneConnections. */ /* If server and replacing scenes. * It's important to run this AFTER moving MovedNetworkObjects * so that they are no longer in the scenes they are leaving. Otherwise * the scene condition would pick them up as still in the leaving scene. */ if (asServer && (replaceScenes != ReplaceOption.None)) { //If global then remove all connections from all scenes. if (data.ScopeType == SceneScopeType.Global) { Scene[] scenes = SceneConnections.Keys.ToArray(); foreach (Scene s in scenes) RemoveAllConnectionsFromScene(s); } //Connections. else if (data.ScopeType == SceneScopeType.Connections) { RemoveConnectionsFromNonGlobalScenes(data.Connections); } } /* Scene unloading if replacing scenes. * * Unload all scenes except MovedObjectsHolder. Also don't * unload GlobalScenes if loading as connection. */ List unloadableScenes = new List(); //Do not run if running as client, and server is active. This would have already run as server. if ((replaceScenes != ReplaceOption.None) && !asHost) { //Unload all other scenes. for (int i = 0; i < UnitySceneManager.sceneCount; i++) { Scene s = UnitySceneManager.GetSceneAt(i); //MovedObjectsScene will never be unloaded. if (s == GetMovedObjectsScene()) continue; /* Scene is in one of the scenes being loaded. * This can occur when trying to load additional clients * into an existing scene. */ if (requestedLoadSceneNames.Contains(s.name)) continue; //Same as above but using handles. if (requestedLoadSceneHandles.Contains(s.handle)) continue; /* Cannot unload global scenes. If * replace scenes was used for a global * load then global scenes would have been reset * before this. */ if (IsGlobalScene(s)) continue; //If scene must be manually unloaded then it cannot be unloaded here. if (_manualUnloadScenes.Contains(s)) continue; HashSet conns; if (SceneConnections.TryGetValueIL2CPP(s, out conns)) { //Still has clients in scene. if (conns != null && conns.Count > 0) continue; } //An offline scene. else { //If not replacing all scenes then skip offline scenes. if (replaceScenes != ReplaceOption.All) continue; } unloadableScenes.Add(s); } } /* Start event. */ if (unloadableScenes.Count > 0 || loadableScenes.Count > 0) { InvokeOnSceneLoadStart(data); _sceneProcessor.LoadStart(data); } //Unloaded scenes by name. Only used for information within callbacks. string[] unloadedNames = new string[unloadableScenes.Count]; for (int i = 0; i < unloadableScenes.Count; i++) unloadedNames[i] = unloadableScenes[i].name; /* Before unloading if !asServer and !asHost and replacing scenes * then move all non scene networked objects to the moved * objects holder. Otherwise network objects would get destroyed * on the scene change and never respawned if server doesn't * have a reason to update visibility. */ if (!data.AsServer && !asHost && (replaceScenes != ReplaceOption.None)) { Scene s = GetMovedObjectsScene(); foreach (NetworkObject nob in _networkManager.ClientManager.Objects.Spawned.Values) { if (!nob.IsSceneObject) UnitySceneManager.MoveGameObjectToScene(nob.gameObject, s); } } /* Unloading scenes. */ _sceneProcessor.UnloadStart(data); for (int i = 0; i < unloadableScenes.Count; i++) { MoveClientHostObjects(unloadableScenes[i], asServer); //Unload one at a time. _sceneProcessor.BeginUnloadAsync(unloadableScenes[i]); while (!_sceneProcessor.IsPercentComplete()) yield return null; } _sceneProcessor.UnloadEnd(data); //Scenes loaded. List loadedScenes = new List(); /* Scene loading. /* Use additive to not thread lock server. */ for (int i = 0; i < loadableScenes.Count; i++) { //Start load async and wait for it to finish. LoadSceneParameters loadSceneParameters = new LoadSceneParameters() { loadSceneMode = LoadSceneMode.Additive, localPhysicsMode = data.SceneLoadData.Options.LocalPhysics }; /* How much percentage each scene load can be worth * at maximum completion. EG: if there are two scenes * 1f / 2f is 0.5f. */ float maximumIndexWorth = (1f / (float)loadableScenes.Count); _sceneProcessor.BeginLoadAsync(loadableScenes[i].Name, loadSceneParameters); while (!_sceneProcessor.IsPercentComplete()) { float percent = _sceneProcessor.GetPercentComplete(); InvokePercentageChange(i, maximumIndexWorth, percent); yield return null; } //Invokes OnScenePercentChange with progress. void InvokePercentageChange(int index, float maximumWorth, float currentScenePercent) { /* Total percent will be how much percentage is complete * in total. Initialize it with a value based on how many * scenes are already fully loaded. */ float totalPercent = (index * maximumWorth); //Add this scenes progress onto total percent. totalPercent += Mathf.Lerp(0f, maximumWorth, currentScenePercent); //Dispatch with total percent. InvokeOnScenePercentChange(data, totalPercent); } //Add to loaded scenes. Scene loaded = UnitySceneManager.GetSceneAt(UnitySceneManager.sceneCount - 1); loadedScenes.Add(loaded); _sceneProcessor.AddLoadedScene(loaded); } //When all scenes are loaded invoke with 100% done. InvokeOnScenePercentChange(data, 1f); /* Add to ManuallyUnloadScenes. */ if (data.AsServer && !data.SceneLoadData.Options.AutomaticallyUnload) { foreach (Scene s in loadedScenes) _manualUnloadScenes.Add(s); } /* Move identities to first scene. */ if (!asHost) { //Find the first valid scene to move objects to. Scene firstValidScene = default; //If to stack scenes. if (data.SceneLoadData.Options.AllowStacking) { Scene firstScene = GetFirstLookupScene(data.SceneLoadData.SceneLookupDatas); /* If the first lookup data contains a handle and the scene * is found for that handle then use that as the moved to scene. * Nobs always move to the first specified scene. */ if (data.SceneLoadData.SceneLookupDatas[0].Handle != 0 && !string.IsNullOrEmpty(firstScene.name)) { firstValidScene = firstScene; } //If handle is not specified then used the last scene that has the same name as the first lookupData. else { Scene lastSameSceneName = default; for (int i = 0; i < UnitySceneManager.sceneCount; i++) { Scene s = UnitySceneManager.GetSceneAt(i); if (s.name == firstScene.name) lastSameSceneName = s; } /* Shouldn't be possible since the scene will always exist either by * just being loaded or already loaded. */ if (string.IsNullOrEmpty(lastSameSceneName.name)) { if (_networkManager.CanLog(LoggingType.Error)) Debug.LogError($"Scene {data.SceneLoadData.SceneLookupDatas[0].Name} could not be found in loaded scenes."); } else { firstValidScene = lastSameSceneName; } } } //Not stacking. else { firstValidScene = GetFirstLookupScene(data.SceneLoadData.SceneLookupDatas); //If not found by look then try firstloaded. if (string.IsNullOrEmpty(firstValidScene.name)) firstValidScene = GetFirstLoadedScene(); } //Gets first scene loaded this method call. Scene GetFirstLoadedScene() { if (loadedScenes.Count > 0) return loadedScenes[0]; else return default; } //Gets first found scene in datas. Scene GetFirstLookupScene(SceneLookupData[] datas) { foreach (SceneLookupData sld in datas) { Scene result = sld.GetScene(out _); if (!string.IsNullOrEmpty(result.name)) return result; } return default; } //If firstValidScene is still invalid then throw. if (string.IsNullOrEmpty(firstValidScene.name)) { if (_networkManager.CanLog(LoggingType.Error)) Debug.LogError($"Unable to move objects to a new scene because new scene lookup has failed."); } //Move objects. else { Scene s = GetMovedObjectsScene(); s.GetRootGameObjects(_movingObjects); foreach (GameObject go in _movingObjects) UnitySceneManager.MoveGameObjectToScene(go, firstValidScene); } } _sceneProcessor.ActivateLoadedScenes(); //Wait until everything is loaded (done). yield return _sceneProcessor.AsyncsIsDone(); _sceneProcessor.LoadEnd(data); /* Wait until loadedScenes are all marked as done. * This is an extra precautionary step because on some devices * the AsyncIsDone returns true before scenes are actually loaded. */ bool allScenesLoaded = true; do { foreach (Scene s in loadedScenes) { if (!s.isLoaded) { allScenesLoaded = false; break; } } yield return null; } while (!allScenesLoaded); SetActiveScene(); //Only the server needs to find scene handles to send to client. Client will send these back to the server. if (asServer) { //Populate broadcastLookupDatas with any loaded scenes. foreach (Scene s in loadedScenes) { SetInFirstNullIndex(s); //Sets scene in the first null index of broadcastLookupDatas. void SetInFirstNullIndex(Scene scene) { for (int i = 0; i < broadcastLookupDatas.Length; i++) { if (broadcastLookupDatas[i] == null) { broadcastLookupDatas[i] = new SceneLookupData(scene); return; } } //If here there are no null entries. if (_networkManager.CanLog(LoggingType.Error)) Debug.LogError($"Cannot add scene to broadcastLookupDatas, collection is full."); } } } /* If running as server and server is * active then send scene changes to client. * Making sure server is still active should it maybe * have dropped during scene loading. */ if (data.AsServer && _networkManager.IsServer) { //Tell clients to load same scenes. LoadScenesBroadcast msg = new LoadScenesBroadcast() { QueueData = data }; //Replace scene lookup datas with ones intended to broadcast to client. msg.QueueData.SceneLoadData.SceneLookupDatas = broadcastLookupDatas; //If networked scope then send to all. if (data.ScopeType == SceneScopeType.Global) { NetworkConnection[] conns = _serverManager.Clients.Values.ToArray(); AddPendingLoad(conns); _serverManager.Broadcast(msg, true); } //If connections scope then only send to connections. else if (data.ScopeType == SceneScopeType.Connections) { AddPendingLoad(data.Connections); for (int i = 0; i < data.Connections.Length; i++) { if (data.Connections[i].Authenticated) data.Connections[i].Broadcast(msg, true); } } } /* If running as client then send a message * to the server to tell them the scene was loaded. * This allows the server to add the client * to the scene for checkers. */ else if (!data.AsServer && _networkManager.IsClient) { ClientScenesLoadedBroadcast msg = new ClientScenesLoadedBroadcast() { SceneLookupDatas = data.SceneLoadData.SceneLookupDatas }; _clientManager.Broadcast(msg); } InvokeOnSceneLoadEnd(data, requestedLoadSceneNames, loadedScenes, unloadedNames); } /// /// Received on client when connection scenes must be loaded. /// /// /// private void OnLoadScenes(LoadScenesBroadcast msg) { //Null data is sent by the server when there are no start scenes to load. if (msg.QueueData == null) { TryInvokeLoadedStartScenes(_clientManager.Connection, false); return; } LoadQueueData qd = msg.QueueData; if (qd.ScopeType == SceneScopeType.Global) LoadGlobalScenesInternal(qd.SceneLoadData, qd.GlobalScenes, false); else LoadConnectionScenesInternal(new NetworkConnection[0], qd.SceneLoadData, qd.GlobalScenes, false); } #endregion #region UnloadScenes. /// /// Unloads scenes on the server and for all clients. /// /// Data about which scenes to unload. public void UnloadGlobalScenes(SceneUnloadData sceneUnloadData) { if (!CanExecute(true, true)) return; UnloadGlobalScenesInternal(sceneUnloadData, _globalScenes, true); } /// /// /// /// /// /// /// private void UnloadGlobalScenesInternal(SceneUnloadData sceneUnloadData, string[] globalScenes, bool asServer) { UnloadQueueData uqd = new UnloadQueueData(SceneScopeType.Global, new NetworkConnection[0], sceneUnloadData, globalScenes, asServer); QueueOperation(uqd); } /// /// Unloads scenes on server and tells a connection to unload them as well. Other connections will not unload this scene. /// /// Connection to unload scenes for. /// Data about which scenes to unload. public void UnloadConnectionScenes(NetworkConnection connection, SceneUnloadData sceneUnloadData) { UnloadConnectionScenes(new NetworkConnection[] { connection }, sceneUnloadData); } /// /// Unloads scenes on server and tells connections to unload them as well. Other connections will not unload this scene. /// /// Connections to unload scenes for. /// Data about which scenes to unload. public void UnloadConnectionScenes(NetworkConnection[] connections, SceneUnloadData sceneUnloadData) { UnloadConnectionScenesInternal(connections, sceneUnloadData, _globalScenes, true); } /// /// Unloads scenes on server without telling any connections to unload them. /// /// Data about which scenes to unload. public void UnloadConnectionScenes(SceneUnloadData sceneUnloadData) { UnloadConnectionScenesInternal(new NetworkConnection[0], sceneUnloadData, _globalScenes, true); } /// /// Unloads scenes for connections. /// /// /// /// /// private void UnloadConnectionScenesInternal(NetworkConnection[] connections, SceneUnloadData sceneUnloadData, string[] globalScenes, bool asServer) { if (!CanExecute(asServer, true)) return; if (SceneDataInvalid(sceneUnloadData, true)) return; UnloadQueueData uqd = new UnloadQueueData(SceneScopeType.Connections, connections, sceneUnloadData, globalScenes, asServer); QueueOperation(uqd); } /// /// Loads scenes within QueuedSceneLoads. /// /// private IEnumerator __UnloadScenes() { UnloadQueueData data = _queuedOperations[0] as UnloadQueueData; //If connection went inactive. if (!ConnectionActive(data.AsServer)) yield break; /* Some actions should not run as client if server is also active. * This is to keep things from running twice. */ bool asClientHost = (!data.AsServer && _networkManager.IsServer); ///True if running asServer. bool asServer = data.AsServer; //Get scenes to unload. Scene[] scenes = GetScenes(data.SceneUnloadData.SceneLookupDatas); /* No scenes found. Only run this if not asHost. * While asHost scenes will possibly not exist because * server side has already unloaded them. But rest of * the unload should continue. */ if (scenes.Length == 0 && !asClientHost) { if (_networkManager.CanLog(LoggingType.Warning)) Debug.LogWarning($"No scenes were found to unload."); yield break; } /* Remove from global scenes * if server and scope is global. * All passed in scenes should be removed from global * regardless of if they're valid or not. If they are invalid, * then they shouldn't be in global to begin with. */ if (asServer && data.ScopeType == SceneScopeType.Global) { RemoveFromGlobalScenes(data.SceneUnloadData.SceneLookupDatas); //Update queue data. data.GlobalScenes = _globalScenes; } /* Remove connections. */ if (asServer) { foreach (Scene s in scenes) { //If global then remove all connections. if (data.ScopeType == SceneScopeType.Global) RemoveAllConnectionsFromScene(s); //Connections. else if (data.ScopeType == SceneScopeType.Connections) RemoveConnectionsFromScene(data.Connections, s); } } /* This will contain all scenes which can be unloaded. * The collection will be modified through various checks. */ List unloadableScenes = scenes.ToList(); /* If asServer and KeepUnused then clear all unloadables. * The clients will still unload the scenes. */ if ((asServer || asClientHost) && data.SceneUnloadData.Options.Mode == UnloadOptions.ServerUnloadMode.KeepUnused) unloadableScenes.Clear(); /* Check to remove global scenes unloadableScenes. * This will need to be done if scenes are being unloaded * for connections. Global scenes cannot be unloaded as * connection. */ if (data.ScopeType == SceneScopeType.Connections) RemoveGlobalScenes(unloadableScenes); //If set to unload unused only. if (data.SceneUnloadData.Options.Mode == UnloadOptions.ServerUnloadMode.UnloadUnused) RemoveOccupiedScenes(unloadableScenes); //If there are scenes to unload. if (unloadableScenes.Count > 0) { InvokeOnSceneUnloadStart(data); _sceneProcessor.UnloadStart(data); //Begin unloading. foreach (Scene s in unloadableScenes) { MoveClientHostObjects(s, asServer); /* Remove from manualUnloadedScenes. * Scene may not be in this collection * but removing is one call vs checking * then removing. */ _manualUnloadScenes.Remove(s); _sceneProcessor.BeginUnloadAsync(s); while (!_sceneProcessor.IsPercentComplete()) yield return null; } _sceneProcessor.UnloadEnd(data); } /* Must yield after sceneProcessor handles things. * This is a Unity bug of sorts. I'm not entirely sure what * is happening, but without the yield it seems as though * the processor logic doesn't complete. This doesn't make much * sense given unity is supposed to be single threaded. Must be * something to do with the coroutine. */ yield return null; SetActiveScene(); /* If running as server then make sure server * is still active after the unloads. If so * send out unloads to clients. */ if (asServer && ConnectionActive(true)) { //Tell clients to unload same scenes. UnloadScenesBroadcast msg = new UnloadScenesBroadcast() { QueueData = data }; //Global. if (data.ScopeType == SceneScopeType.Global) { _serverManager.Broadcast(msg, true); } //Connections. else if (data.ScopeType == SceneScopeType.Connections) { if (data.Connections != null) { for (int i = 0; i < data.Connections.Length; i++) { if (data.Connections[i] != null) data.Connections[i].Broadcast(msg, true); } } } } InvokeOnSceneUnloadEnd(data, unloadableScenes); } /// /// Received on clients when networked scenes must be unloaded. /// /// /// private void OnUnloadScenes(UnloadScenesBroadcast msg) { UnloadQueueData qd = msg.QueueData; if (qd.ScopeType == SceneScopeType.Global) UnloadGlobalScenesInternal(qd.SceneUnloadData, qd.GlobalScenes, false); else UnloadConnectionScenesInternal(new NetworkConnection[0], qd.SceneUnloadData, qd.GlobalScenes, false); } #endregion /// /// Move objects visible to clientHost that are within an unloading scene.This ensures the objects are despawned on the client side rather than when the scene is destroyed. /// /// private void MoveClientHostObjects(Scene scene, bool asServer) { if (!_moveClientHostObjects) return; /* The asServer isn't really needed. I could only call * this method when asServer is true. But for the sake * of preventing user-error (me being the user this time) * I've included it into the parameters. */ if (!asServer) return; //Don't need to perform if not host. if (!_networkManager.IsClient) return; NetworkConnection clientConn = _networkManager.ClientManager.Connection; /* It would be nice to see if the client wasn't even in the scene * here using SceneConnections but it's possible that the scene had been * wiped from SceneConnections earlier depending on how scenes are * loaded or unloaded. Instead we must iterate through spawned objects. */ ListCache movingNobs = ListCaches.GetNetworkObjectCache(); /* Rather than a get all networkobjects in scene * let's iterate the spawned objects instead. I imagine * in most scenarios iterating spawned would be faster. * That's a long one! */ foreach (NetworkObject nob in _networkManager.ServerManager.Objects.Spawned.Values) { //Not in the scene being destroyed. if (nob.gameObject.scene != scene) continue; //ClientHost doesn't have visibility. if (!nob.Observers.Contains(clientConn)) continue; //Cannot move if not root. if (nob.transform.root != null) continue; /* If here nob is in the same being * destroyed and clientHost has visiblity. */ movingNobs.AddValue(nob); } int count = movingNobs.Written; if (count > 0) { Scene moveScene = GetDelayedDestroyScene(); List collection = movingNobs.Collection; for (int i = 0; i < count; i++) { NetworkObject nob = collection[i]; /* Force as not a scene object * so that it becomes destroyed * rather than disabled. */ nob.ClearRuntimeSceneObject(); /* If the object is already being despawned then *just disable and move it. Otherwise despawn it * on the server then move it. */ //Not deinitializing, despawn it then. if (!nob.IsDeinitializing) nob.Despawn(); else nob.gameObject.SetActive(false); UnitySceneManager.MoveGameObjectToScene(nob.gameObject, moveScene); } } ListCaches.StoreCache(movingNobs); } /// /// Returns if a connection is in a scene using SceneConnections. /// /// /// /// internal bool InSceneConnections(NetworkConnection conn, Scene scene) { if (!SceneConnections.TryGetValueIL2CPP(scene, out HashSet hs)) return false; else return hs.Contains(conn); } /// /// Adds the owner of nob to the gameObjects scene if there are no global scenes. /// public void AddOwnerToDefaultScene(NetworkObject nob) { //No owner. if (!nob.Owner.IsValid) { if (_networkManager.CanLog(LoggingType.Warning)) Debug.LogWarning($"NetworkObject {nob.name} does not have an owner."); return; } //Won't add to default if there are globals. if (_globalScenes != null && _globalScenes.Length > 0) return; AddConnectionToScene(nob.Owner, nob.gameObject.scene); } /// /// Adds a connection to a scene. This will always be called one connection at a time because connections are only added after they invidually validate loading the scene. /// /// /// private void AddConnectionToScene(NetworkConnection conn, Scene scene) { HashSet hs; //Scene doesn't have any connections yet. bool inSceneConnections = SceneConnections.TryGetValueIL2CPP(scene, out hs); if (!inSceneConnections) hs = new HashSet(); bool added = hs.Add(conn); if (added) { conn.AddToScene(scene); //If not yet added to scene connections. if (!inSceneConnections) SceneConnections[scene] = hs; NetworkConnection[] arrayConn = new NetworkConnection[] { conn }; InvokeClientPresenceChange(scene, arrayConn, true, true); RebuildObservers(arrayConn.ToArray()); InvokeClientPresenceChange(scene, arrayConn, true, false); /* Also need to rebuild all networkobjects * for connection so other players can * see them. */ RebuildObservers(conn.Objects.ToArray()); } } /// /// Removes connections from any scene which is not global. /// /// /// private void RemoveConnectionsFromNonGlobalScenes(NetworkConnection[] conns) { List removedScenes = new List(); foreach (KeyValuePair> item in SceneConnections) { Scene scene = item.Key; //Cannot remove from globla scenes. if (IsGlobalScene(scene)) continue; HashSet hs = item.Value; List connectionsRemoved = new List(); //Remove every connection from the scene. foreach (NetworkConnection c in conns) { bool removed = hs.Remove(c); if (removed) { c.RemoveFromScene(scene); connectionsRemoved.Add(c); } } //If hashset is empty then remove scene from SceneConnections. if (hs.Count == 0) removedScenes.Add(scene); if (connectionsRemoved.Count > 0) { NetworkConnection[] connectionsRemovedArray = connectionsRemoved.ToArray(); InvokeClientPresenceChange(scene, connectionsRemovedArray, false, true); RebuildObservers(connectionsRemovedArray); InvokeClientPresenceChange(scene, connectionsRemovedArray, false, false); } } foreach (Scene s in removedScenes) SceneConnections.Remove(s); /* Also rebuild observers for objects owned by connection. * This ensures other connections will lose visibility if * they no longer share a scene. */ foreach (NetworkConnection c in conns) RebuildObservers(c.Objects.ToArray()); } /// /// Removes connections from specified scenes. /// /// /// private void RemoveConnectionsFromScene(NetworkConnection[] conns, Scene scene) { HashSet hs; //No hashset for scene, so no connections are in scene. if (!SceneConnections.TryGetValueIL2CPP(scene, out hs)) return; List connectionsRemoved = new List(); //Remove every connection from the scene. foreach (NetworkConnection c in conns) { bool removed = hs.Remove(c); if (removed) { c.RemoveFromScene(scene); connectionsRemoved.Add(c); } } //If hashset is empty then remove scene from SceneConnections. if (hs.Count == 0) SceneConnections.Remove(scene); if (connectionsRemoved.Count > 0) { NetworkConnection[] connectionsRemovedArray = connectionsRemoved.ToArray(); InvokeClientPresenceChange(scene, connectionsRemovedArray, false, true); RebuildObservers(connectionsRemovedArray); InvokeClientPresenceChange(scene, connectionsRemovedArray, false, false); } /* Also rebuild observers for objects owned by connection. * This ensures other connections will lose visibility if * they no longer share a scene. */ foreach (NetworkConnection c in conns) RebuildObservers(c.Objects.ToArray()); } /// /// Removes all connections from a scene. /// /// private void RemoveAllConnectionsFromScene(Scene scene) { HashSet hs; //No hashset for scene, so no connections are in scene. if (!SceneConnections.TryGetValueIL2CPP(scene, out hs)) return; //On each connection remove them from specified scene. foreach (NetworkConnection c in hs) c.RemoveFromScene(scene); //Make hashset into list for presence change. NetworkConnection[] connectionsRemoved = hs.ToArray(); //Clear hashset and remove entry from sceneconnections. hs.Clear(); SceneConnections.Remove(scene); if (connectionsRemoved.Length > 0) { InvokeClientPresenceChange(scene, connectionsRemoved, false, true); RebuildObservers(connectionsRemoved); InvokeClientPresenceChange(scene, connectionsRemoved, false, false); } /* Also rebuild observers for objects owned by connection. * This ensures other connections will lose visibility if * they no longer share a scene. */ foreach (NetworkConnection c in connectionsRemoved) RebuildObservers(c.Objects.ToArray()); } #region Can Load/Unload Scene. /// /// Returns if a scene can be loaded locally. /// /// private bool CanLoadScene(LoadQueueData qd, SceneLookupData sld) { bool foundByHandle; Scene s = sld.GetScene(out foundByHandle); //Try to find if scene is already loaded. bool alreadyLoaded = !string.IsNullOrEmpty(s.name); if (alreadyLoaded) { //Only servers can load the same scene multiple times for stacking. if (!qd.AsServer) return false; //If can only load scenes which aren't loaded yet and scene is already loaded. if (!qd.SceneLoadData.Options.AllowStacking) return false; /* Found by handle, this means the user is trying to specify * exactly which scene to load into. When a handle is specified * new instances will not be created, so a new scene cannot * be loaded. */ if (alreadyLoaded && foundByHandle) return false; } //Fall through. return true; } #endregion #region Helpers. /// /// Rebuilds observers for networkObjects. /// /// private void RebuildObservers(NetworkObject[] networkObjects) { foreach (NetworkObject nob in networkObjects) { if (nob != null && nob.IsSpawned) _serverManager.Objects.RebuildObservers(nob); } } /// /// Rebuilds all NetworkObjects for connection. /// internal void RebuildObservers(NetworkConnection connection) { RebuildObservers(new NetworkConnection[] { connection }); } /// /// Rebuilds all NetworkObjects for connections. /// internal void RebuildObservers(NetworkConnection[] connections) { foreach (NetworkConnection c in connections) _serverManager.Objects.RebuildObservers(c); } /// /// Invokes OnClientPresenceChange start or end. /// /// /// /// /// private void InvokeClientPresenceChange(Scene scene, NetworkConnection[] conns, bool added, bool start) { foreach (NetworkConnection c in conns) { ClientPresenceChangeEventArgs cpc = new ClientPresenceChangeEventArgs(scene, c, added); if (start) OnClientPresenceChangeStart?.Invoke(cpc); else OnClientPresenceChangeEnd?.Invoke(cpc); } } #endregion #region GetScene. /// /// Gets scenes from SceneLookupData. /// /// /// private Scene[] GetScenes(SceneLookupData[] datas) { List result = new List(); foreach (SceneLookupData sld in datas) { Scene s = sld.GetScene(out _); if (!string.IsNullOrEmpty(s.name)) result.Add(s); } return result.ToArray(); } /// /// Returns a scene by name. /// /// /// public static Scene GetScene(string sceneName) { return UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName); } /// /// Returns a scene by handle. /// /// /// public static Scene GetScene(int sceneHandle) { int count = UnityEngine.SceneManagement.SceneManager.sceneCount; for (int i = 0; i < count; i++) { Scene s = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); if (s.handle == sceneHandle) return s; } return new Scene(); } #endregion /// /// Returns if GlobalScenes contains scene. /// /// /// private bool IsGlobalScene(Scene scene) { for (int i = 0; i < _globalScenes.Length; i++) { if (_globalScenes[i] == scene.name) return true; } return false; } /// /// Removes datas from GlobalScenes. /// /// private void RemoveFromGlobalScenes(Scene scene) { RemoveFromGlobalScenes(new SceneLookupData[] { SceneLookupData.CreateData(scene) }); } /// /// Removes datas from GlobalScenes. /// /// private void RemoveFromGlobalScenes(SceneLookupData[] datas) { List newGlobalScenes = _globalScenes.ToList(); int startCount = newGlobalScenes.Count; //Remove scenes. for (int i = 0; i < datas.Length; i++) newGlobalScenes.Remove(datas[i].Name); //If any were removed remake globalscenes. if (startCount != newGlobalScenes.Count) _globalScenes = newGlobalScenes.ToArray(); } /// /// Removes GlobalScenes from scenes. /// /// /// private void RemoveGlobalScenes(List scenes) { for (int i = 0; i < scenes.Count; i++) { foreach (string gs in _globalScenes) { if (gs == scenes[i].name) { scenes.RemoveAt(i); i--; } } } } /// /// Removes occupied scenes from scenes. /// /// private void RemoveOccupiedScenes(List scenes) { for (int i = 0; i < scenes.Count; i++) { if (SceneConnections.TryGetValueIL2CPP(scenes[i], out _)) { scenes.RemoveAt(i); i--; } } } /// /// Adds a pending load for a connection. /// private void AddPendingLoad(NetworkConnection conn) { AddPendingLoad(new NetworkConnection[] { conn }); } /// /// Adds a pending load for a connection. /// private void AddPendingLoad(NetworkConnection[] conns) { foreach (NetworkConnection c in conns) { /* Make sure connection is active. This should always be true * but perhaps disconnect happened as scene was loading on server * therefor it cannot be sent to the client. * Also only authenticated clients can load scenes. */ if (!c.IsActive || !c.Authenticated) continue; if (_pendingClientSceneChanges.TryGetValue(c, out int result)) _pendingClientSceneChanges[c] = (result + 1); else _pendingClientSceneChanges[c] = 1; } } /// /// Sets the first global scene as the active scene. /// If a global scene is not available then FallbackActiveScene is used. /// private void SetActiveScene() { if (!_setActiveScene) return; Scene s = default; if (_globalScenes != null && _globalScenes.Length > 0) s = GetScene(_globalScenes[0]); /* If scene isn't set from global then make * sure currently active isn't the movedobjectscene. * If it is, then use the fallback scene. */ if (string.IsNullOrEmpty(s.name) && UnitySceneManager.GetActiveScene() == _movedObjectsScene) s = GetFallbackActiveScene(); //If was changed then update active scene. if (!string.IsNullOrEmpty(s.name)) UnitySceneManager.SetActiveScene(s); OnActiveSceneSet?.Invoke(); OnActiveSceneSetInternal?.Invoke(); //Also update light probes. if (_lightProbeUpdating == LightProbeUpdateType.Asynchronous) LightProbes.TetrahedralizeAsync(); else if (_lightProbeUpdating == LightProbeUpdateType.BlockThread) LightProbes.Tetrahedralize(); } /// /// Returns the FallbackActiveScene. /// /// private Scene GetFallbackActiveScene() { if (string.IsNullOrEmpty(_fallbackActiveScene.name)) _fallbackActiveScene = UnitySceneManager.CreateScene("FallbackActiveScene"); return _fallbackActiveScene; } /// /// Returns the MovedObejctsScene. /// /// private Scene GetMovedObjectsScene() { //Create moved objects scene. It will probably be used eventually. If not, no harm either way. if (string.IsNullOrEmpty(_movedObjectsScene.name)) _movedObjectsScene = UnitySceneManager.CreateScene("MovedObjectsHolder"); return _movedObjectsScene; } /// /// Returns the DelayedDestroyScene. /// /// private Scene GetDelayedDestroyScene() { //Create moved objects scene. It will probably be used eventually. If not, no harm either way. if (string.IsNullOrEmpty(_delayedDestroyScene.name)) _delayedDestroyScene = UnityEngine.SceneManagement.SceneManager.CreateScene("DelayedDestroy"); return _delayedDestroyScene; } #region Sanity checks. /// /// Returns if a SceneLoadData is valid. /// /// /// /// private bool SceneDataInvalid(SceneLoadData data, bool error) { bool result = data.DataInvalid(); if (result && error) { if (_networkManager.CanLog(LoggingType.Error)) Debug.LogError(INVALID_SCENELOADDATA); } return result; } /// /// Returns if a SceneLoadData is valid. /// /// /// /// private bool SceneDataInvalid(SceneUnloadData data, bool error) { bool result = data.DataInvalid(); if (result && error) { if (_networkManager.CanLog(LoggingType.Error)) Debug.LogError(INVALID_SCENEUNLOADDATA); } return result; } /// /// Returns if connection is active for server or client in association with AsServer. /// /// /// private bool ConnectionActive(bool asServer) { return (asServer) ? _networkManager.IsServer : _networkManager.IsClient; } /// /// Returns if a method can execute. /// /// /// /// private bool CanExecute(bool asServer, bool warn) { bool result; if (asServer) { result = _networkManager.IsServer; if (!result && warn) { if (_networkManager.CanLog(LoggingType.Warning)) Debug.LogWarning($"Method cannot be called as the server is not active."); } } else { result = _networkManager.IsClient; if (!result && warn) { if (_networkManager.CanLog(LoggingType.Warning)) Debug.LogWarning($"Method cannot be called as the client is not active."); } } return result; } #endregion } }