using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using AsyncOperation = UnityEngine.AsyncOperation;
namespace Unity.Netcode
{
///
/// Used by to determine if a server invoked scene event has started.
/// The returned status is stored in the property.
/// Note: This was formally known as SwitchSceneProgress which contained the .
/// All s are now delivered by the event handler
/// via the parameter.
///
public enum SceneEventProgressStatus
{
///
/// No scene event progress status can be used to initialize a variable that will be checked over time.
///
None,
///
/// The scene event was successfully started.
///
Started,
///
/// Returned if you try to unload a scene that was not yet loaded.
///
SceneNotLoaded,
///
/// Returned if you try to start a new scene event before a previous one is finished.
///
SceneEventInProgress,
///
/// Returned if the scene name used with
/// or is invalid.
///
InvalidSceneName,
///
/// Server side: Returned if the delegate handler returns false
/// (i.e. scene is considered not valid/safe to load).
///
SceneFailedVerification,
///
/// This is used for internal error notifications.
/// If you receive this event then it is most likely due to a bug (please open a GitHub issue with steps to replicate).
///
InternalNetcodeError,
}
///
/// Server side only:
/// This tracks the progress of clients during a load or unload scene event
///
internal class SceneEventProgress
{
///
/// List of clientIds of those clients that is done loading the scene.
///
internal Dictionary ClientsProcessingSceneEvent { get; } = new Dictionary();
internal List ClientsThatDisconnected = new List();
///
/// This is when the current scene event will have timed out
///
internal float WhenSceneEventHasTimedOut;
///
/// Delegate type for when the switch scene progress is completed. Either by all clients done loading the scene or by time out.
///
internal delegate bool OnCompletedDelegate(SceneEventProgress sceneEventProgress);
///
/// The callback invoked when the switch scene progress is completed. Either by all clients done loading the scene or by time out.
///
internal OnCompletedDelegate OnComplete;
internal Action OnSceneEventCompleted;
///
/// This will make sure that we only have timed out if we never completed
///
internal bool HasTimedOut()
{
return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup;
}
///
/// The hash value generated from the full scene path
///
internal uint SceneHash { get; set; }
internal Guid Guid { get; } = Guid.NewGuid();
internal uint SceneEventId;
private Coroutine m_TimeOutCoroutine;
private AsyncOperation m_AsyncOperation;
private NetworkManager m_NetworkManager { get; }
internal SceneEventProgressStatus Status { get; set; }
internal SceneEventType SceneEventType { get; set; }
internal LoadSceneMode LoadSceneMode;
internal List GetClientsWithStatus(bool completedSceneEvent)
{
var clients = new List();
if (completedSceneEvent)
{
// If we are the host, then add the host-client to the list
// of clients that completed if the AsyncOperation is done.
if (m_NetworkManager.IsHost && m_AsyncOperation.isDone)
{
clients.Add(m_NetworkManager.LocalClientId);
}
// Add all clients that completed the scene event
foreach (var clientStatus in ClientsProcessingSceneEvent)
{
if (clientStatus.Value == completedSceneEvent)
{
clients.Add(clientStatus.Key);
}
}
}
else
{
// If we are the host, then add the host-client to the list
// of clients that did not complete if the AsyncOperation is
// not done.
if (m_NetworkManager.IsHost && !m_AsyncOperation.isDone)
{
clients.Add(m_NetworkManager.LocalClientId);
}
// If we are getting the list of clients that have not completed the
// scene event, then add any clients that disconnected during this
// scene event.
clients.AddRange(ClientsThatDisconnected);
}
return clients;
}
internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressStatus status = SceneEventProgressStatus.Started)
{
if (status == SceneEventProgressStatus.Started)
{
m_NetworkManager = networkManager;
if (networkManager.IsServer)
{
m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback;
// Track the clients that were connected when we started this event
foreach (var connectedClientId in networkManager.ConnectedClientsIds)
{
// Ignore the host client
if (NetworkManager.ServerClientId == connectedClientId)
{
continue;
}
ClientsProcessingSceneEvent.Add(connectedClientId, false);
}
WhenSceneEventHasTimedOut = Time.realtimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut;
m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress());
}
}
Status = status;
}
///
/// Remove the client from the clients processing the current scene event
/// Add this client to the clients that disconnected list
///
private void OnClientDisconnectCallback(ulong clientId)
{
if (ClientsProcessingSceneEvent.ContainsKey(clientId))
{
ClientsThatDisconnected.Add(clientId);
ClientsProcessingSceneEvent.Remove(clientId);
}
}
///
/// Coroutine that checks to see if the scene event is complete every network tick period.
/// This will handle completing the scene event when one or more client(s) disconnect(s)
/// during a scene event and if it does not complete within the scene loading time out period
/// it will time out the scene event.
///
internal IEnumerator TimeOutSceneEventProgress()
{
var waitForNetworkTick = new WaitForSeconds(1.0f / m_NetworkManager.NetworkConfig.TickRate);
while (!HasTimedOut())
{
yield return waitForNetworkTick;
TryFinishingSceneEventProgress();
}
}
///
/// Sets the client's scene event progress to finished/true
///
internal void ClientFinishedSceneEvent(ulong clientId)
{
if (ClientsProcessingSceneEvent.ContainsKey(clientId))
{
ClientsProcessingSceneEvent[clientId] = true;
TryFinishingSceneEventProgress();
}
}
///
/// Determines if the scene event has finished for both
/// client(s) and server.
///
///
/// The server checks if all known clients processing this scene event
/// have finished and then it returns its local AsyncOperation status.
/// Clients finish when their AsyncOperation finishes.
///
private bool HasFinished()
{
// If the network session is terminated/terminating then finish tracking
// this scene event
if (!IsNetworkSessionActive())
{
return true;
}
// Clients skip over this
foreach (var clientStatus in ClientsProcessingSceneEvent)
{
if (!clientStatus.Value)
{
return false;
}
}
// Return the local scene event's AsyncOperation status
// Note: Integration tests process scene loading through a queue
// and the AsyncOperation could not be assigned for several
// network tick periods. Return false if that is the case.
return m_AsyncOperation == null ? false : m_AsyncOperation.isDone;
}
///
/// Sets the AsyncOperation for the scene load/unload event
///
internal void SetAsyncOperation(AsyncOperation asyncOperation)
{
m_AsyncOperation = asyncOperation;
m_AsyncOperation.completed += new Action(asyncOp2 =>
{
// Don't invoke the callback if the network session is disconnected
// during a SceneEventProgress
if (IsNetworkSessionActive())
{
OnSceneEventCompleted?.Invoke(SceneEventId);
}
// Go ahead and try finishing even if the network session is terminated/terminating
// as we might need to stop the coroutine
TryFinishingSceneEventProgress();
});
}
internal bool IsNetworkSessionActive()
{
return m_NetworkManager != null && m_NetworkManager.IsListening && !m_NetworkManager.ShutdownInProgress;
}
///
/// Will try to finish the current scene event in progress as long as
/// all conditions are met.
///
internal void TryFinishingSceneEventProgress()
{
if (HasFinished() || HasTimedOut())
{
// Don't attempt to finalize this scene event if we are no longer listening or a shutdown is in progress
if (IsNetworkSessionActive())
{
OnComplete?.Invoke(this);
m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid);
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnectCallback;
}
if (m_TimeOutCoroutine != null)
{
m_NetworkManager.StopCoroutine(m_TimeOutCoroutine);
}
}
}
}
}