using System.Collections.Generic;
using System;
using System.IO;
using MLAPI.Configuration;
using MLAPI.Exceptions;
using MLAPI.Logging;
using MLAPI.Messaging;
using MLAPI.Serialization.Pooled;
using MLAPI.Spawning;
using UnityEngine;
using UnityEngine.SceneManagement;
using MLAPI.Messaging.Buffering;
using MLAPI.Transports;
namespace MLAPI.SceneManagement
{
///
/// Main class for managing network scenes
///
public static class NetworkSceneManager
{
///
/// Delegate for when the scene has been switched
///
public delegate void SceneSwitchedDelegate();
///
/// Delegate for when a scene switch has been initiated
///
public delegate void SceneSwitchStartedDelegate(AsyncOperation operation);
///
/// Event that is invoked when the scene is switched
///
public static event SceneSwitchedDelegate OnSceneSwitched;
///
/// Event that is invoked when a local scene switch has started
///
public static event SceneSwitchStartedDelegate OnSceneSwitchStarted;
internal static readonly HashSet RegisteredSceneNames = new HashSet();
internal static readonly Dictionary SceneNameToIndex = new Dictionary();
internal static readonly Dictionary SceneIndexToString = new Dictionary();
internal static readonly Dictionary SceneSwitchProgresses = new Dictionary();
private static Scene s_LastScene;
private static string s_NextSceneName;
private static bool s_IsSwitching = false;
internal static uint CurrentSceneIndex = 0;
internal static Guid CurrentSceneSwitchProgressGuid = new Guid();
internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad = false;
internal static void SetCurrentSceneIndex()
{
if (!SceneNameToIndex.ContainsKey(SceneManager.GetActiveScene().name))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"The current scene ({SceneManager.GetActiveScene().name}) is not regisered as a network scene.");
}
return;
}
CurrentSceneIndex = SceneNameToIndex[SceneManager.GetActiveScene().name];
CurrentActiveSceneIndex = CurrentSceneIndex;
}
internal static uint CurrentActiveSceneIndex { get; private set; } = 0;
///
/// Adds a scene during runtime.
/// The index is REQUIRED to be unique AND the same across all instances.
///
/// Scene name.
/// Index.
public static void AddRuntimeSceneName(string sceneName, uint index)
{
if (!NetworkManager.Singleton.NetworkConfig.AllowRuntimeSceneChanges)
{
throw new NetworkConfigurationException($"Cannot change the scene configuration when {nameof(NetworkConfig.AllowRuntimeSceneChanges)} is false");
}
RegisteredSceneNames.Add(sceneName);
SceneIndexToString.Add(index, sceneName);
SceneNameToIndex.Add(sceneName, index);
}
///
/// Switches to a scene with a given name. Can only be called from Server
///
/// The name of the scene to switch to
public static SceneSwitchProgress SwitchScene(string sceneName)
{
if (!NetworkManager.Singleton.IsServer)
{
throw new NotServerException("Only server can start a scene switch");
}
if (s_IsSwitching)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Scene switch already in progress");
}
return null;
}
if (!RegisteredSceneNames.Contains(sceneName))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"The scene {sceneName} is not registered as a switchable scene.");
}
return null;
}
NetworkSpawnManager.ServerDestroySpawnedSceneObjects(); //Destroy current scene objects before switching.
s_IsSwitching = true;
s_LastScene = SceneManager.GetActiveScene();
var switchSceneProgress = new SceneSwitchProgress();
SceneSwitchProgresses.Add(switchSceneProgress.Guid, switchSceneProgress);
CurrentSceneSwitchProgressGuid = switchSceneProgress.Guid;
// Move ALL NetworkObjects to the temp scene
MoveObjectsToDontDestroyOnLoad();
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
// Switch scene
AsyncOperation sceneLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
s_NextSceneName = sceneName;
sceneLoad.completed += (AsyncOperation asyncOp2) => { OnSceneLoaded(switchSceneProgress.Guid, null); };
switchSceneProgress.SetSceneLoadOperation(sceneLoad);
OnSceneSwitchStarted?.Invoke(sceneLoad);
return switchSceneProgress;
}
// Called on client
internal static void OnSceneSwitch(uint sceneIndex, Guid switchSceneGuid, Stream objectStream)
{
if (!SceneIndexToString.ContainsKey(sceneIndex) || !RegisteredSceneNames.Contains(SceneIndexToString[sceneIndex]))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Server requested a scene switch to a non-registered scene");
}
return;
}
s_LastScene = SceneManager.GetActiveScene();
// Move ALL NetworkObjects to the temp scene
MoveObjectsToDontDestroyOnLoad();
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
string sceneName = SceneIndexToString[sceneIndex];
var sceneLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
s_NextSceneName = sceneName;
sceneLoad.completed += asyncOp2 => OnSceneLoaded(switchSceneGuid, objectStream);
OnSceneSwitchStarted?.Invoke(sceneLoad);
}
internal static void OnFirstSceneSwitchSync(uint sceneIndex, Guid switchSceneGuid)
{
if (!SceneIndexToString.ContainsKey(sceneIndex) || !RegisteredSceneNames.Contains(SceneIndexToString[sceneIndex]))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Server requested a scene switch to a non-registered scene");
}
return;
}
if (SceneManager.GetActiveScene().name == SceneIndexToString[sceneIndex])
{
return; //This scene is already loaded. This usually happends at first load
}
s_LastScene = SceneManager.GetActiveScene();
string sceneName = SceneIndexToString[sceneIndex];
s_NextSceneName = sceneName;
CurrentActiveSceneIndex = SceneNameToIndex[sceneName];
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
SceneManager.LoadScene(sceneName);
using (var buffer = PooledNetworkBuffer.Get())
using (var writer = PooledNetworkWriter.Get(buffer))
{
writer.WriteByteArray(switchSceneGuid.ToByteArray());
InternalMessageSender.Send(NetworkManager.Singleton.ServerClientId, NetworkConstants.CLIENT_SWITCH_SCENE_COMPLETED, NetworkChannel.Internal, buffer);
}
s_IsSwitching = false;
}
private static void OnSceneLoaded(Guid switchSceneGuid, Stream objectStream)
{
CurrentActiveSceneIndex = SceneNameToIndex[s_NextSceneName];
var nextScene = SceneManager.GetSceneByName(s_NextSceneName);
SceneManager.SetActiveScene(nextScene);
// Move all objects to the new scene
MoveObjectsToScene(nextScene);
IsSpawnedObjectsPendingInDontDestroyOnLoad = false;
CurrentSceneIndex = CurrentActiveSceneIndex;
if (NetworkManager.Singleton.IsServer)
{
OnSceneUnloadServer(switchSceneGuid);
}
else
{
OnSceneUnloadClient(switchSceneGuid, objectStream);
}
}
private static void OnSceneUnloadServer(Guid switchSceneGuid)
{
// Justification: Rare alloc, could(should?) reuse
var newSceneObjects = new List();
{
var networkObjects = MonoBehaviour.FindObjectsOfType();
for (int i = 0; i < networkObjects.Length; i++)
{
if (networkObjects[i].IsSceneObject == null)
{
NetworkSpawnManager.SpawnNetworkObjectLocally(networkObjects[i], NetworkSpawnManager.GetNetworkObjectId(), true, false, null, null, false, 0, false, true);
newSceneObjects.Add(networkObjects[i]);
}
}
}
for (int j = 0; j < NetworkManager.Singleton.ConnectedClientsList.Count; j++)
{
if (NetworkManager.Singleton.ConnectedClientsList[j].ClientId != NetworkManager.Singleton.ServerClientId)
{
using (var buffer = PooledNetworkBuffer.Get())
using (var writer = PooledNetworkWriter.Get(buffer))
{
writer.WriteUInt32Packed(CurrentActiveSceneIndex);
writer.WriteByteArray(switchSceneGuid.ToByteArray());
uint sceneObjectsToSpawn = 0;
for (int i = 0; i < newSceneObjects.Count; i++)
{
if (newSceneObjects[i].m_Observers.Contains(NetworkManager.Singleton.ConnectedClientsList[j].ClientId))
{
sceneObjectsToSpawn++;
}
}
writer.WriteUInt32Packed(sceneObjectsToSpawn);
for (int i = 0; i < newSceneObjects.Count; i++)
{
if (newSceneObjects[i].m_Observers.Contains(NetworkManager.Singleton.ConnectedClientsList[j].ClientId))
{
writer.WriteBool(newSceneObjects[i].IsPlayerObject);
writer.WriteUInt64Packed(newSceneObjects[i].NetworkObjectId);
writer.WriteUInt64Packed(newSceneObjects[i].OwnerClientId);
NetworkObject parentNetworkObject = null;
if (!newSceneObjects[i].AlwaysReplicateAsRoot && newSceneObjects[i].transform.parent != null)
{
parentNetworkObject = newSceneObjects[i].transform.parent.GetComponent();
}
if (parentNetworkObject == null)
{
writer.WriteBool(false);
}
else
{
writer.WriteBool(true);
writer.WriteUInt64Packed(parentNetworkObject.NetworkObjectId);
}
if (!NetworkManager.Singleton.NetworkConfig.EnableSceneManagement || NetworkManager.Singleton.NetworkConfig.UsePrefabSync)
{
writer.WriteUInt64Packed(newSceneObjects[i].PrefabHash);
writer.WriteSinglePacked(newSceneObjects[i].transform.position.x);
writer.WriteSinglePacked(newSceneObjects[i].transform.position.y);
writer.WriteSinglePacked(newSceneObjects[i].transform.position.z);
writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.x);
writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.y);
writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.z);
}
else
{
writer.WriteUInt64Packed(newSceneObjects[i].NetworkInstanceId);
}
if (NetworkManager.Singleton.NetworkConfig.EnableNetworkVariable)
{
newSceneObjects[i].WriteNetworkVariableData(buffer, NetworkManager.Singleton.ConnectedClientsList[j].ClientId);
}
}
}
InternalMessageSender.Send(NetworkManager.Singleton.ConnectedClientsList[j].ClientId, NetworkConstants.SWITCH_SCENE, NetworkChannel.Internal, buffer);
}
}
}
//Tell server that scene load is completed
if (NetworkManager.Singleton.IsHost)
{
OnClientSwitchSceneCompleted(NetworkManager.Singleton.LocalClientId, switchSceneGuid);
}
s_IsSwitching = false;
OnSceneSwitched?.Invoke();
}
private static void OnSceneUnloadClient(Guid switchSceneGuid, Stream objectStream)
{
if (!NetworkManager.Singleton.NetworkConfig.EnableSceneManagement || NetworkManager.Singleton.NetworkConfig.UsePrefabSync)
{
NetworkSpawnManager.DestroySceneObjects();
using (var reader = PooledNetworkReader.Get(objectStream))
{
uint newObjectsCount = reader.ReadUInt32Packed();
for (int i = 0; i < newObjectsCount; i++)
{
bool isPlayerObject = reader.ReadBool();
ulong networkId = reader.ReadUInt64Packed();
ulong owner = reader.ReadUInt64Packed();
bool hasParent = reader.ReadBool();
ulong? parentNetworkId = null;
if (hasParent)
{
parentNetworkId = reader.ReadUInt64Packed();
}
ulong prefabHash = reader.ReadUInt64Packed();
Vector3? position = null;
Quaternion? rotation = null;
if (reader.ReadBool())
{
position = new Vector3(reader.ReadSinglePacked(), reader.ReadSinglePacked(), reader.ReadSinglePacked());
rotation = Quaternion.Euler(reader.ReadSinglePacked(), reader.ReadSinglePacked(), reader.ReadSinglePacked());
}
var networkObject = NetworkSpawnManager.CreateLocalNetworkObject(false, 0, prefabHash, parentNetworkId, position, rotation);
NetworkSpawnManager.SpawnNetworkObjectLocally(networkObject, networkId, true, isPlayerObject, owner, objectStream, false, 0, true, false);
var bufferQueue = BufferManager.ConsumeBuffersForNetworkId(networkId);
// Apply buffered messages
if (bufferQueue != null)
{
while (bufferQueue.Count > 0)
{
BufferManager.BufferedMessage message = bufferQueue.Dequeue();
NetworkManager.Singleton.HandleIncomingData(message.SenderClientId, message.NetworkChannel, new ArraySegment(message.NetworkBuffer.GetBuffer(), (int)message.NetworkBuffer.Position, (int)message.NetworkBuffer.Length), message.ReceiveTime, false);
BufferManager.RecycleConsumedBufferedMessage(message);
}
}
}
}
}
else
{
var networkObjects = MonoBehaviour.FindObjectsOfType();
NetworkSpawnManager.ClientCollectSoftSyncSceneObjectSweep(networkObjects);
using (var reader = PooledNetworkReader.Get(objectStream))
{
uint newObjectsCount = reader.ReadUInt32Packed();
for (int i = 0; i < newObjectsCount; i++)
{
bool isPlayerObject = reader.ReadBool();
ulong networkId = reader.ReadUInt64Packed();
ulong owner = reader.ReadUInt64Packed();
bool hasParent = reader.ReadBool();
ulong? parentNetworkId = null;
if (hasParent)
{
parentNetworkId = reader.ReadUInt64Packed();
}
ulong instanceId = reader.ReadUInt64Packed();
var networkObject = NetworkSpawnManager.CreateLocalNetworkObject(true, instanceId, 0, parentNetworkId, null, null);
NetworkSpawnManager.SpawnNetworkObjectLocally(networkObject, networkId, true, isPlayerObject, owner, objectStream, false, 0, true, false);
var bufferQueue = BufferManager.ConsumeBuffersForNetworkId(networkId);
// Apply buffered messages
if (bufferQueue != null)
{
while (bufferQueue.Count > 0)
{
BufferManager.BufferedMessage message = bufferQueue.Dequeue();
NetworkManager.Singleton.HandleIncomingData(message.SenderClientId, message.NetworkChannel, new ArraySegment(message.NetworkBuffer.GetBuffer(), (int)message.NetworkBuffer.Position, (int)message.NetworkBuffer.Length), message.ReceiveTime, false);
BufferManager.RecycleConsumedBufferedMessage(message);
}
}
}
}
}
using (var buffer = PooledNetworkBuffer.Get())
using (var writer = PooledNetworkWriter.Get(buffer))
{
writer.WriteByteArray(switchSceneGuid.ToByteArray());
InternalMessageSender.Send(NetworkManager.Singleton.ServerClientId, NetworkConstants.CLIENT_SWITCH_SCENE_COMPLETED, NetworkChannel.Internal, buffer);
}
s_IsSwitching = false;
OnSceneSwitched?.Invoke();
}
internal static bool HasSceneMismatch(uint sceneIndex) => SceneManager.GetActiveScene().name != SceneIndexToString[sceneIndex];
// Called on server
internal static void OnClientSwitchSceneCompleted(ulong clientId, Guid switchSceneGuid)
{
if (switchSceneGuid == Guid.Empty)
{
//If Guid is empty it means the client has loaded the start scene of the server and the server would never have a switchSceneProgresses created for the start scene.
return;
}
if (!SceneSwitchProgresses.ContainsKey(switchSceneGuid))
{
return;
}
SceneSwitchProgresses[switchSceneGuid].AddClientAsDone(clientId);
}
internal static void RemoveClientFromSceneSwitchProgresses(ulong clientId)
{
foreach (var switchSceneProgress in SceneSwitchProgresses.Values)
{
switchSceneProgress.RemoveClientAsDone(clientId);
}
}
private static void MoveObjectsToDontDestroyOnLoad()
{
// Move ALL NetworkObjects to the temp scene
var objectsToKeep = NetworkSpawnManager.SpawnedObjectsList;
foreach (var sobj in objectsToKeep)
{
//In case an object has been set as a child of another object it has to be unchilded in order to be moved from one scene to another.
if (sobj.gameObject.transform.parent != null)
{
sobj.gameObject.transform.parent = null;
}
MonoBehaviour.DontDestroyOnLoad(sobj.gameObject);
}
}
private static void MoveObjectsToScene(Scene scene)
{
// Move ALL NetworkObjects to the temp scene
var objectsToKeep = NetworkSpawnManager.SpawnedObjectsList;
foreach (var sobj in objectsToKeep)
{
//In case an object has been set as a child of another object it has to be unchilded in order to be moved from one scene to another.
if (sobj.gameObject.transform.parent != null)
{
sobj.gameObject.transform.parent = null;
}
SceneManager.MoveGameObjectToScene(sobj.gameObject, scene);
}
}
}
}