fishbait/UnityProject/Assets/FishNet/Runtime/Managing/Scened/SceneManager.cs
NIMFER bf403a8f97 Ducktaped together a FishNet version of the Transport
Ducktaped together a FishNet version of the Transport, now I need to edit the LoadBalancer and Server so it connects and actually trades information
2022-08-14 03:55:25 +02:00

2054 lines
No EOL
85 KiB
C#

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
{
/// <summary>
/// Handles loading, unloading, and scene visibility for clients.
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("FishNet/Manager/SceneManager")]
public sealed class SceneManager : MonoBehaviour
{
#region Types.
internal enum LightProbeUpdateType
{
Asynchronous = 0,
BlockThread = 1,
Off = 2,
}
#endregion
#region Public.
/// <summary>
/// Called after the active scene has been set, immediately after scene loads. This will occur before NetworkBehaviour callbacks run for the scene's objects.
/// </summary>
public event Action OnActiveSceneSet;
/// <summary>
/// Called when a client loads initial scenes after connecting. Boolean will be true if asServer.
/// </summary>
public event Action<NetworkConnection, bool> OnClientLoadedStartScenes;
/// <summary>
/// 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.
/// </summary>
public event Action OnQueueStart;
/// <summary>
/// Called when the scene queue is emptied.
/// </summary>
public event Action OnQueueEnd;
/// <summary>
/// Called when a scene load starts.
/// </summary>
public event Action<SceneLoadStartEventArgs> OnLoadStart;
/// <summary>
/// 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.
/// </summary>
public event Action<SceneLoadPercentEventArgs> OnLoadPercentChange;
/// <summary>
/// Called when a scene load ends.
/// </summary>
public event Action<SceneLoadEndEventArgs> OnLoadEnd;
/// <summary>
/// Called when a scene unload starts.
/// </summary>
public event Action<SceneUnloadStartEventArgs> OnUnloadStart;
/// <summary>
/// Called when a scene unload ends.
/// </summary>
public event Action<SceneUnloadEndEventArgs> OnUnloadEnd;
/// <summary>
/// Called when a client presence changes within a scene, before the server rebuilds observers.
/// </summary>
public event Action<ClientPresenceChangeEventArgs> OnClientPresenceChangeStart;
/// <summary>
/// Called when a client presence changes within a scene, after the server rebuilds observers.
/// </summary>
public event Action<ClientPresenceChangeEventArgs> OnClientPresenceChangeEnd;
/// <summary>
/// Connections within each scene.
/// </summary>
public Dictionary<Scene, HashSet<NetworkConnection>> SceneConnections { get; private set; } = new Dictionary<Scene, HashSet<NetworkConnection>>();
#endregion
#region Internal.
/// <summary>
/// Called after the active scene has been set, immediately after scene loads.
/// </summary>
internal event Action OnActiveSceneSetInternal;
#endregion
#region Serialized.
/// <summary>
/// Script to handle addressables loading and unloading. This field may be blank if addressables are not being used.
/// </summary>
[Tooltip("Script to handle addressables loading and unloading. This field may be blank if addressables are not being used.")]
[SerializeField]
private SceneProcessorBase _sceneProcessor;
/// <summary>
/// How to update light probes after loading or unloading scenes.
/// </summary>
[Tooltip("How to update light probes after loading or unloading scenes.")]
[SerializeField]
private LightProbeUpdateType _lightProbeUpdating = LightProbeUpdateType.Asynchronous;
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>
/// True to automatically set active scenes when loading and unloading scenes.
/// </summary>
[Tooltip("True to automatically set active scenes when loading and unloading scenes.")]
[SerializeField]
private bool _setActiveScene = true;
#endregion
#region Private.
/// <summary>
/// NetworkManager for this script.
/// </summary>
private NetworkManager _networkManager;
/// <summary>
/// ServerManager for this script.
/// </summary>
private ServerManager _serverManager => _networkManager.ServerManager;
/// <summary>
/// ClientManager for this script.
/// </summary>
private ClientManager _clientManager => _networkManager.ClientManager;
/// <summary>
/// Scenes which are currently loaded as networked scenes. All players should have networked scenes loaded.
/// </summary>
private string[] _globalScenes = new string[0];
/// <summary>
/// Lastest SceneLoadData for a global load.
/// </summary>
private SceneLoadData _globalSceneLoadData = new SceneLoadData();
/// <summary>
/// Scenes to load or unload, in order.
/// </summary>
private List<object> _queuedOperations = new List<object>();
/// <summary>
/// Scenes which must be manually unloaded, even when emptied.
/// </summary>
private HashSet<Scene> _manualUnloadScenes = new HashSet<Scene>();
/// <summary>
/// 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.
/// </summary>
private Scene _movedObjectsScene;
/// <summary>
/// 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.
/// </summary>
private Scene _delayedDestroyScene;
/// <summary>
/// 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.
/// </summary>
private Scene _fallbackActiveScene;
/// <summary>
/// Becomes true when when a scene first successfully begins to load or unload. Value is reset to false when the scene queue is emptied.
/// </summary>
private bool _sceneQueueStartInvoked;
/// <summary>
/// Objects being moved from MovedObjects scene to another.
/// </summary>
private List<GameObject> _movingObjects = new List<GameObject>();
/// <summary>
/// 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.
/// </summary>
private Dictionary<NetworkConnection, int> _pendingClientSceneChanges = new Dictionary<NetworkConnection, int>();
#endregion
#region Consts.
/// <summary>
/// String to use when scene data used to load is invalid.
/// </summary>
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.";
/// <summary>
/// String to use when scene data used to unload is invalid.
/// </summary>
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<DefaultSceneProcessor>();
_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<LoadScenesBroadcast>(OnLoadScenes);
_clientManager.RegisterBroadcast<UnloadScenesBroadcast>(OnUnloadScenes);
_serverManager.RegisterBroadcast<ClientScenesLoadedBroadcast>(OnClientLoadedScenes);
_clientManager.RegisterBroadcast<EmptyStartScenesBroadcast>(OnClientEmptyStartScenes);
_serverManager.RegisterBroadcast<EmptyStartScenesBroadcast>(OnServerEmptyStartScenes);
}
private void OnDestroy()
{
UnitySceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
}
/// <summary>
/// Called when the server connection state changes.
/// </summary>
private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj)
{
//If no servers are started.
if (!_networkManager.ServerManager.AnyServerStarted())
ResetValues();
}
/// <summary>
/// Resets as if first use.
/// </summary>
private void ResetValues()
{
SceneConnections.Clear();
_globalScenes = new string[0];
_globalSceneLoadData = new SceneLoadData();
_queuedOperations.Clear();
_manualUnloadScenes.Clear();
_sceneQueueStartInvoked = false;
_movingObjects.Clear();
}
/// <summary>
/// Called when a connection state changes for a remote client.
/// </summary>
private void ServerManager_OnRemoteConnectionState(NetworkConnection arg1, RemoteConnectionStateArgs arg2)
{
if (arg2.ConnectionState == RemoteConnectionState.Stopped)
ClientDisconnected(arg1);
}
/// <summary>
/// Initializes this script for use.
/// </summary>
/// <param name="manager"></param>
internal void InitializeOnceInternal(NetworkManager manager)
{
_networkManager = manager;
}
/// <summary>
/// Received when a scene is unloaded.
/// </summary>
/// <param name="arg0"></param>
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.
/// <summary>
/// Invokes OnClientLoadedStartScenes if connection just loaded start scenes.
/// </summary>
/// <param name="connection"></param>
private void TryInvokeLoadedStartScenes(NetworkConnection connection, bool asServer)
{
if (connection.SetLoadedStartScenes(asServer))
OnClientLoadedStartScenes?.Invoke(connection, asServer);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="obj"></param>
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);
}
}
/// <summary>
/// Received on client when the server has no start scenes.
/// </summary>
private void OnClientEmptyStartScenes(EmptyStartScenesBroadcast msg)
{
_clientManager.Broadcast(msg);
}
/// <summary>
/// Received on server when client confirms there are no start scenes.
/// </summary>
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.
/// <summary>
/// Received when a player disconnects from the server.
/// </summary>
/// <param name="conn"></param> //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<Scene> scenesToUnload = new List<Scene>();
//Current active scene.
Scene activeScene = UnitySceneManager.GetActiveScene();
foreach (KeyValuePair<Scene, HashSet<NetworkConnection>> item in SceneConnections)
{
Scene scene = item.Key;
HashSet<NetworkConnection> 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.
/// <summary>
/// Received on server when a client loads scenes.
/// </summary>
/// <param name="conn"></param>
/// <param name="msg"></param>
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.
/// <summary>
/// Checks if OnQueueStart should invoke, and if so invokes.
/// </summary>
private void TryInvokeOnQueueStart()
{
if (_sceneQueueStartInvoked)
return;
_sceneQueueStartInvoked = true;
OnQueueStart?.Invoke();
}
/// <summary>
/// Checks if OnQueueEnd should invoke, and if so invokes.
/// </summary>
private void TryInvokeOnQueueEnd()
{
if (!_sceneQueueStartInvoked)
return;
_sceneQueueStartInvoked = false;
OnQueueEnd?.Invoke();
}
/// <summary>
/// Invokes that a scene load has started. Only called when valid scenes will be loaded.
/// </summary>
/// <param name="qd"></param>
private void InvokeOnSceneLoadStart(LoadQueueData qd)
{
TryInvokeOnQueueStart();
OnLoadStart?.Invoke(new SceneLoadStartEventArgs(qd));
}
/// <summary>
/// Invokes that a scene load has ended. Only called after a valid scene has loaded.
/// </summary>
/// <param name="qd"></param>
private void InvokeOnSceneLoadEnd(LoadQueueData qd, List<string> requestedLoadScenes, List<Scene> loadedScenes, string[] unloadedSceneNames)
{
//Make new list to not destroy original data.
List<string> 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);
}
/// <summary>
/// Invokes that a scene unload has started. Only called when valid scenes will be unloaded.
/// </summary>
/// <param name="sqd"></param>
private void InvokeOnSceneUnloadStart(UnloadQueueData sqd)
{
TryInvokeOnQueueStart();
OnUnloadStart?.Invoke(new SceneUnloadStartEventArgs(sqd));
}
/// <summary>
/// Invokes that a scene unload has ended. Only called after a valid scene has unloaded.
/// </summary>
/// <param name="sqd"></param>
private void InvokeOnSceneUnloadEnd(UnloadQueueData sqd, List<Scene> unloadedScenes)
{
int[] handles = new int[unloadedScenes.Count];
OnUnloadEnd?.Invoke(new SceneUnloadEndEventArgs(sqd, handles));
}
/// <summary>
/// Invokes when completion percentage changes while unloading or unloading a scene. Value is between 0f and 1f, while 1f is 100% done.
/// </summary>
/// <param name="value"></param>
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.
/// <summary>
/// Queues a load or unload operation and starts queue if needed.
/// </summary>
/// <param name="data"></param>
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());
}
/// <summary>
/// Processes queued scene operations.
/// </summary>
/// <param name="asServer"></param>
/// <returns></returns>
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
/// <summary>
/// Loads scenes on the server and for all clients. Future clients will automatically load these scenes.
/// </summary>
/// <param name="sceneLoadData">Data about which scenes to load.</param>
public void LoadGlobalScenes(SceneLoadData sceneLoadData)
{
LoadGlobalScenesInternal(sceneLoadData, _globalScenes, true);
}
/// <summary>
/// Adds to load scene queue.
/// </summary>
/// <param name="sceneLoadData"></param>
/// <param name="asServer"></param>
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);
}
/// <summary>
/// Loads scenes on server and tells connections to load them as well. Other connections will not load this scene.
/// </summary>
/// <param name="conn">Connections to load scenes for.</param>
/// <param name="sceneLoadData">Data about which scenes to load.</param>
public void LoadConnectionScenes(NetworkConnection conn, SceneLoadData sceneLoadData)
{
LoadConnectionScenes(new NetworkConnection[] { conn }, sceneLoadData);
}
/// <summary>
/// Loads scenes on server and tells connections to load them as well. Other connections will not load this scene.
/// </summary>
/// <param name="conns">Connections to load scenes for.</param>
/// <param name="sceneLoadData">Data about which scenes to load.</param>
public void LoadConnectionScenes(NetworkConnection[] conns, SceneLoadData sceneLoadData)
{
LoadConnectionScenesInternal(conns, sceneLoadData, _globalScenes, true);
}
/// <summary>
/// Loads scenes on server without telling clients to load the scenes.
/// </summary>
/// <param name="sceneLoadData">Data about which scenes to load.</param>
public void LoadConnectionScenes(SceneLoadData sceneLoadData)
{
LoadConnectionScenesInternal(new NetworkConnection[0], sceneLoadData, _globalScenes, true);
}
/// <summary>
/// Adds to load scene queue.
/// </summary>
/// <param name="sceneLoadData"></param>
/// <param name="asServer"></param>
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);
}
/// <summary>
/// Returns if a NetworkObject can be moved.
/// </summary>
/// <param name="warn"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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<string> requestedLoadSceneNames = new List<string>();
List<int> requestedLoadSceneHandles = new List<int>();
/* 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<SceneLookupData> loadableScenes = new List<SceneLookupData>();
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<Scene> unloadableScenes = new List<Scene>();
//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<NetworkConnection> 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<Scene> loadedScenes = new List<Scene>();
/* 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);
}
/// <summary>
/// Received on client when connection scenes must be loaded.
/// </summary>
/// <param name="conn"></param>
/// <param name="msg"></param>
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.
/// <summary>
/// Unloads scenes on the server and for all clients.
/// </summary>
/// <param name="sceneUnloadData">Data about which scenes to unload.</param>
public void UnloadGlobalScenes(SceneUnloadData sceneUnloadData)
{
if (!CanExecute(true, true))
return;
UnloadGlobalScenesInternal(sceneUnloadData, _globalScenes, true);
}
/// <summary>
///
/// </summary>
/// <param name="scope"></param>
/// <param name="conns"></param>
/// <param name="additiveScenes"></param>
/// <param name="asServer"></param>
private void UnloadGlobalScenesInternal(SceneUnloadData sceneUnloadData, string[] globalScenes, bool asServer)
{
UnloadQueueData uqd = new UnloadQueueData(SceneScopeType.Global, new NetworkConnection[0], sceneUnloadData, globalScenes, asServer);
QueueOperation(uqd);
}
/// <summary>
/// Unloads scenes on server and tells a connection to unload them as well. Other connections will not unload this scene.
/// </summary>
/// <param name="connection">Connection to unload scenes for.</param>
/// <param name="sceneUnloadData">Data about which scenes to unload.</param>
public void UnloadConnectionScenes(NetworkConnection connection, SceneUnloadData sceneUnloadData)
{
UnloadConnectionScenes(new NetworkConnection[] { connection }, sceneUnloadData);
}
/// <summary>
/// Unloads scenes on server and tells connections to unload them as well. Other connections will not unload this scene.
/// </summary>
/// <param name="connections">Connections to unload scenes for.</param>
/// <param name="sceneUnloadData">Data about which scenes to unload.</param>
public void UnloadConnectionScenes(NetworkConnection[] connections, SceneUnloadData sceneUnloadData)
{
UnloadConnectionScenesInternal(connections, sceneUnloadData, _globalScenes, true);
}
/// <summary>
/// Unloads scenes on server without telling any connections to unload them.
/// </summary>
/// <param name="sceneUnloadData">Data about which scenes to unload.</param>
public void UnloadConnectionScenes(SceneUnloadData sceneUnloadData)
{
UnloadConnectionScenesInternal(new NetworkConnection[0], sceneUnloadData, _globalScenes, true);
}
/// <summary>
/// Unloads scenes for connections.
/// </summary>
/// <param name="connections"></param>
/// <param name="sceneUnloadData"></param>
/// <param name="globalScenes"></param>
/// <param name="asServer"></param>
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);
}
/// <summary>
/// Loads scenes within QueuedSceneLoads.
/// </summary>
/// <returns></returns>
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<Scene> 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);
}
/// <summary>
/// Received on clients when networked scenes must be unloaded.
/// </summary>
/// <param name="conn"></param>
/// <param name="msg"></param>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="scene"></param>
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<NetworkObject> 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<NetworkObject> 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);
}
/// <summary>
/// Returns if a connection is in a scene using SceneConnections.
/// </summary>
/// <param name="conn"></param>
/// <param name="scene"></param>
/// <returns></returns>
internal bool InSceneConnections(NetworkConnection conn, Scene scene)
{
if (!SceneConnections.TryGetValueIL2CPP(scene, out HashSet<NetworkConnection> hs))
return false;
else
return hs.Contains(conn);
}
/// <summary>
/// Adds the owner of nob to the gameObjects scene if there are no global scenes.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sceneName"></param>
/// <param name="conn"></param>
private void AddConnectionToScene(NetworkConnection conn, Scene scene)
{
HashSet<NetworkConnection> hs;
//Scene doesn't have any connections yet.
bool inSceneConnections = SceneConnections.TryGetValueIL2CPP(scene, out hs);
if (!inSceneConnections)
hs = new HashSet<NetworkConnection>();
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());
}
}
/// <summary>
/// Removes connections from any scene which is not global.
/// </summary>
/// <param name="conns"></param>
/// <param name="asd"></param>
private void RemoveConnectionsFromNonGlobalScenes(NetworkConnection[] conns)
{
List<Scene> removedScenes = new List<Scene>();
foreach (KeyValuePair<Scene, HashSet<NetworkConnection>> item in SceneConnections)
{
Scene scene = item.Key;
//Cannot remove from globla scenes.
if (IsGlobalScene(scene))
continue;
HashSet<NetworkConnection> hs = item.Value;
List<NetworkConnection> connectionsRemoved = new List<NetworkConnection>();
//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());
}
/// <summary>
/// Removes connections from specified scenes.
/// </summary>
/// <param name="conns"></param>
/// <param name="asd"></param>
private void RemoveConnectionsFromScene(NetworkConnection[] conns, Scene scene)
{
HashSet<NetworkConnection> hs;
//No hashset for scene, so no connections are in scene.
if (!SceneConnections.TryGetValueIL2CPP(scene, out hs))
return;
List<NetworkConnection> connectionsRemoved = new List<NetworkConnection>();
//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());
}
/// <summary>
/// Removes all connections from a scene.
/// </summary>
/// <param name="scene"></param>
private void RemoveAllConnectionsFromScene(Scene scene)
{
HashSet<NetworkConnection> 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.
/// <summary>
/// Returns if a scene can be loaded locally.
/// </summary>
/// <returns></returns>
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.
/// <summary>
/// Rebuilds observers for networkObjects.
/// </summary>
/// <param name="networkObjects"></param>
private void RebuildObservers(NetworkObject[] networkObjects)
{
foreach (NetworkObject nob in networkObjects)
{
if (nob != null && nob.IsSpawned)
_serverManager.Objects.RebuildObservers(nob);
}
}
/// <summary>
/// Rebuilds all NetworkObjects for connection.
/// </summary>
internal void RebuildObservers(NetworkConnection connection)
{
RebuildObservers(new NetworkConnection[] { connection });
}
/// <summary>
/// Rebuilds all NetworkObjects for connections.
/// </summary>
internal void RebuildObservers(NetworkConnection[] connections)
{
foreach (NetworkConnection c in connections)
_serverManager.Objects.RebuildObservers(c);
}
/// <summary>
/// Invokes OnClientPresenceChange start or end.
/// </summary>
/// <param name="scene"></param>
/// <param name="conns"></param>
/// <param name="added"></param>
/// <param name="start"></param>
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.
/// <summary>
/// Gets scenes from SceneLookupData.
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
private Scene[] GetScenes(SceneLookupData[] datas)
{
List<Scene> result = new List<Scene>();
foreach (SceneLookupData sld in datas)
{
Scene s = sld.GetScene(out _);
if (!string.IsNullOrEmpty(s.name))
result.Add(s);
}
return result.ToArray();
}
/// <summary>
/// Returns a scene by name.
/// </summary>
/// <param name="sceneName"></param>
/// <returns></returns>
public static Scene GetScene(string sceneName)
{
return UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
}
/// <summary>
/// Returns a scene by handle.
/// </summary>
/// <param name="sceneHandle"></param>
/// <returns></returns>
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
/// <summary>
/// Returns if GlobalScenes contains scene.
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
private bool IsGlobalScene(Scene scene)
{
for (int i = 0; i < _globalScenes.Length; i++)
{
if (_globalScenes[i] == scene.name)
return true;
}
return false;
}
/// <summary>
/// Removes datas from GlobalScenes.
/// </summary>
/// <param name="scenes"></param>
private void RemoveFromGlobalScenes(Scene scene)
{
RemoveFromGlobalScenes(new SceneLookupData[] { SceneLookupData.CreateData(scene) });
}
/// <summary>
/// Removes datas from GlobalScenes.
/// </summary>
/// <param name="scenes"></param>
private void RemoveFromGlobalScenes(SceneLookupData[] datas)
{
List<string> 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();
}
/// <summary>
/// Removes GlobalScenes from scenes.
/// </summary>
/// <param name="scenes"></param>
/// <returns></returns>
private void RemoveGlobalScenes(List<Scene> scenes)
{
for (int i = 0; i < scenes.Count; i++)
{
foreach (string gs in _globalScenes)
{
if (gs == scenes[i].name)
{
scenes.RemoveAt(i);
i--;
}
}
}
}
/// <summary>
/// Removes occupied scenes from scenes.
/// </summary>
/// <param name="scenes"></param>
private void RemoveOccupiedScenes(List<Scene> scenes)
{
for (int i = 0; i < scenes.Count; i++)
{
if (SceneConnections.TryGetValueIL2CPP(scenes[i], out _))
{
scenes.RemoveAt(i);
i--;
}
}
}
/// <summary>
/// Adds a pending load for a connection.
/// </summary>
private void AddPendingLoad(NetworkConnection conn)
{
AddPendingLoad(new NetworkConnection[] { conn });
}
/// <summary>
/// Adds a pending load for a connection.
/// </summary>
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;
}
}
/// <summary>
/// Sets the first global scene as the active scene.
/// If a global scene is not available then FallbackActiveScene is used.
/// </summary>
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();
}
/// <summary>
/// Returns the FallbackActiveScene.
/// </summary>
/// <returns></returns>
private Scene GetFallbackActiveScene()
{
if (string.IsNullOrEmpty(_fallbackActiveScene.name))
_fallbackActiveScene = UnitySceneManager.CreateScene("FallbackActiveScene");
return _fallbackActiveScene;
}
/// <summary>
/// Returns the MovedObejctsScene.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Returns the DelayedDestroyScene.
/// </summary>
/// <returns></returns>
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.
/// <summary>
/// Returns if a SceneLoadData is valid.
/// </summary>
/// <param name="data"></param>
/// <param name="error"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Returns if a SceneLoadData is valid.
/// </summary>
/// <param name="data"></param>
/// <param name="error"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Returns if connection is active for server or client in association with AsServer.
/// </summary>
/// <param name="asServer"></param>
/// <returns></returns>
private bool ConnectionActive(bool asServer)
{
return (asServer) ? _networkManager.IsServer : _networkManager.IsClient;
}
/// <summary>
/// Returns if a method can execute.
/// </summary>
/// <param name="asServer"></param>
/// <param name="warn"></param>
/// <returns></returns>
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
}
}