using System.Collections.Generic;
using System;
using System.Linq;
using Unity.Collections;
using UnityEngine.SceneManagement;
namespace Unity.Netcode
{
///
/// The different types of scene events communicated between a server and client.
/// Used by for messages.
/// Note: This is only when is enabled.
/// See also:
///
///
public enum SceneEventType : byte
{
///
/// Load a scene
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client are notified a load scene event started
///
Load,
///
/// Unload a scene
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client are notified an unload scene event started.
///
Unload,
///
/// Synchronizes current game session state for newly approved clients
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Server and Client receives a local notification (server receives the ClientId being synchronized).
///
Synchronize,
///
/// Game session re-synchronization of NetworkObjects that were destroyed during a event
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client receive a local notification
///
ReSynchronize,
///
/// All clients have finished loading a scene
/// Invocation: Server Side
/// Message Flow: Server to Client
/// Event Notification: Both server and client receive a local notification containing the clients that finished
/// as well as the clients that timed out(if any).
///
LoadEventCompleted,
///
/// All clients have unloaded a scene
/// Invocation: Server Side
/// Message Flow: Server to Client
/// Event Notification: Both server and client receive a local notification containing the clients that finished
/// as well as the clients that timed out(if any).
///
UnloadEventCompleted,
///
/// A client has finished loading a scene
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification.
///
LoadComplete,
///
/// A client has finished unloading a scene
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification.
///
UnloadComplete,
///
/// A client has finished synchronizing from a event
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification.
///
SynchronizeComplete,
}
///
/// Used by for messages
/// Note: This is only when is enabled.
/// See also:
///
internal class SceneEventData : IDisposable
{
internal SceneEventType SceneEventType;
internal LoadSceneMode LoadSceneMode;
internal Guid SceneEventProgressId;
internal uint SceneEventId;
internal uint SceneHash;
internal int SceneHandle;
// Used by the client during synchronization
internal uint ClientSceneHash;
internal int ClientSceneHandle;
/// Only used for scene events, this assures permissions when writing
/// NetworkVariable information. If that process changes, then we need to update this
internal ulong TargetClientId;
private Dictionary> m_SceneNetworkObjects;
private Dictionary m_SceneNetworkObjectDataOffsets;
///
/// Client or Server Side:
/// Client side: Generates a list of all NetworkObjects by their NetworkObjectId that was spawned during th synchronization process
/// Server side: Compares list from client to make sure client didn't drop a message about a NetworkObject being despawned while it
/// was synchronizing (if so server will send another message back to the client informing the client of NetworkObjects to remove)
/// spawned during an initial synchronization.
///
private List m_NetworkObjectsSync = new List();
///
/// Server Side Re-Synchronization:
/// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned,
/// the server will compile a list and send back an Event_ReSync message to the client.
///
private List m_NetworkObjectsToBeRemoved = new List();
private bool m_HasInternalBuffer;
internal FastBufferReader InternalBuffer;
private NetworkManager m_NetworkManager;
internal List ClientsCompleted;
internal List ClientsTimedOut;
internal Queue ScenesToSynchronize;
internal Queue SceneHandlesToSynchronize;
///
/// Server Side:
/// Add a scene and its handle to the list of scenes the client should load before synchronizing
/// Since scene handles are not the same per instance, the client builds a server scene handle to
/// client scene handle lookup table.
/// Why include the scene handle? In order to support loading of the same additive scene more than once
/// we must distinguish which scene we are talking about when the server tells the client to unload a scene.
/// The server will always communicate its local relative scene's handle and the client will determine its
/// local relative handle from the table being built.
/// Look for usage to see where
/// entries are being added to or removed from the table
///
///
///
internal void AddSceneToSynchronize(uint sceneHash, int sceneHandle)
{
ScenesToSynchronize.Enqueue(sceneHash);
SceneHandlesToSynchronize.Enqueue((uint)sceneHandle);
}
///
/// Client Side:
/// Gets the next scene hash to be loaded for approval and/or late joining
///
///
internal uint GetNextSceneSynchronizationHash()
{
return ScenesToSynchronize.Dequeue();
}
///
/// Client Side:
/// Gets the next scene handle to be loaded for approval and/or late joining
///
///
internal int GetNextSceneSynchronizationHandle()
{
return (int)SceneHandlesToSynchronize.Dequeue();
}
///
/// Client Side:
/// Determines if all scenes have been processed during the synchronization process
///
/// true/false
internal bool IsDoneWithSynchronization()
{
if (ScenesToSynchronize.Count == 0 && SceneHandlesToSynchronize.Count == 0)
{
return true;
}
else if (ScenesToSynchronize.Count != SceneHandlesToSynchronize.Count)
{
// This should never happen, but in the event it does...
throw new Exception($"[{nameof(SceneEventData)}-Internal Mismatch Error] {nameof(ScenesToSynchronize)} count != {nameof(SceneHandlesToSynchronize)} count!");
}
return false;
}
///
/// Server Side:
/// Called just before the synchronization process
///
internal void InitializeForSynch()
{
if (m_SceneNetworkObjects == null)
{
m_SceneNetworkObjects = new Dictionary>();
}
else
{
m_SceneNetworkObjects.Clear();
}
if (ScenesToSynchronize == null)
{
ScenesToSynchronize = new Queue();
}
else
{
ScenesToSynchronize.Clear();
}
if (SceneHandlesToSynchronize == null)
{
SceneHandlesToSynchronize = new Queue();
}
else
{
SceneHandlesToSynchronize.Clear();
}
}
internal void AddSpawnedNetworkObjects()
{
m_NetworkObjectsSync.Clear();
foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
if (sobj.Observers.Contains(TargetClientId))
{
m_NetworkObjectsSync.Add(sobj);
}
}
m_NetworkObjectsSync.Sort(SortNetworkObjects);
}
///
/// Server Side:
/// Used during the synchronization process to associate NetworkObjects with scenes
///
///
///
internal void AddNetworkObjectForSynch(uint sceneIndex, NetworkObject networkObject)
{
if (!m_SceneNetworkObjects.ContainsKey(sceneIndex))
{
m_SceneNetworkObjects.Add(sceneIndex, new List());
}
m_SceneNetworkObjects[sceneIndex].Add(networkObject);
}
///
/// Client and Server:
/// Determines if the scene event type was intended for the client ( or server )
///
/// true (client should handle this message) false (server should handle this message)
internal bool IsSceneEventClientSide()
{
switch (SceneEventType)
{
case SceneEventType.Load:
case SceneEventType.Unload:
case SceneEventType.Synchronize:
case SceneEventType.ReSynchronize:
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
{
return true;
}
}
return false;
}
///
/// Server Side:
/// Sorts the NetworkObjects to assure proper instantiation order of operations for
/// registered INetworkPrefabInstanceHandler implementations
///
///
///
///
private int SortNetworkObjects(NetworkObject first, NetworkObject second)
{
var doesFirstHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(first);
var doesSecondHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(second);
if (doesFirstHaveHandler != doesSecondHaveHandler)
{
if (doesFirstHaveHandler)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
///
/// Client and Server Side:
/// Serializes data based on the SceneEvent type ()
///
/// to write the scene event data
internal void Serialize(FastBufferWriter writer)
{
// Write the scene event type
writer.WriteValueSafe(SceneEventType);
// Write the scene loading mode
writer.WriteValueSafe(LoadSceneMode);
// Write the scene event progress Guid
if (SceneEventType != SceneEventType.Synchronize)
{
writer.WriteValueSafe(SceneEventProgressId);
}
// Write the scene index and handle
writer.WriteValueSafe(SceneHash);
writer.WriteValueSafe(SceneHandle);
switch (SceneEventType)
{
case SceneEventType.Synchronize:
{
WriteSceneSynchronizationData(writer);
break;
}
case SceneEventType.Load:
{
SerializeScenePlacedObjects(writer);
break;
}
case SceneEventType.SynchronizeComplete:
{
WriteClientSynchronizationResults(writer);
break;
}
case SceneEventType.ReSynchronize:
{
WriteClientReSynchronizationData(writer);
break;
}
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
{
WriteSceneEventProgressDone(writer);
break;
}
}
}
///
/// Server Side:
/// Called at the end of a event once the scene is loaded and scene placed NetworkObjects
/// have been locally spawned
///
internal void WriteSceneSynchronizationData(FastBufferWriter writer)
{
// Write the scenes we want to load, in the order we want to load them
writer.WriteValueSafe(ScenesToSynchronize.ToArray());
writer.WriteValueSafe(SceneHandlesToSynchronize.ToArray());
// Store our current position in the stream to come back and say how much data we have written
var positionStart = writer.Position;
// Size Place Holder -- Start
// !!NOTE!!: Since this is a placeholder to be set after we know how much we have written,
// for stream offset purposes this MUST not be a packed value!
writer.WriteValueSafe((int)0);
int totalBytes = 0;
// Write the number of NetworkObjects we are serializing
writer.WriteValueSafe(m_NetworkObjectsSync.Count());
for (var i = 0; i < m_NetworkObjectsSync.Count(); ++i)
{
var noStart = writer.Position;
var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId);
writer.WriteValueSafe(m_NetworkObjectsSync[i].gameObject.scene.handle);
sceneObject.Serialize(writer);
var noStop = writer.Position;
totalBytes += (int)(noStop - noStart);
}
// Size Place Holder -- End
var positionEnd = writer.Position;
var bytesWritten = (uint)(positionEnd - (positionStart + sizeof(uint)));
writer.Seek(positionStart);
// Write the total size written to the stream by NetworkObjects being serialized
writer.WriteValueSafe(bytesWritten);
writer.Seek(positionEnd);
}
///
/// Server Side:
/// Called at the end of a event once the scene is loaded and scene placed NetworkObjects
/// have been locally spawned
/// Maximum number of objects that could theoretically be synchronized is 65536
///
internal void SerializeScenePlacedObjects(FastBufferWriter writer)
{
var numberOfObjects = (ushort)0;
var headPosition = writer.Position;
// Write our count place holder (must not be packed!)
writer.WriteValueSafe((ushort)0);
foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects)
{
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
{
if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId))
{
// Write our server relative scene handle for the NetworkObject being serialized
writer.WriteValueSafe(keyValuePairBySceneHandle.Key);
// Serialize the NetworkObject
var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId);
sceneObject.Serialize(writer);
numberOfObjects++;
}
}
}
var tailPosition = writer.Position;
// Reposition to our count position to the head before we wrote our object count
writer.Seek(headPosition);
// Write number of NetworkObjects serialized (must not be packed!)
writer.WriteValueSafe(numberOfObjects);
// Set our position back to the tail
writer.Seek(tailPosition);
}
///
/// Client and Server Side:
/// Deserialize data based on the SceneEvent type.
///
///
internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventType);
reader.ReadValueSafe(out LoadSceneMode);
if (SceneEventType != SceneEventType.Synchronize)
{
reader.ReadValueSafe(out SceneEventProgressId);
}
reader.ReadValueSafe(out SceneHash);
reader.ReadValueSafe(out SceneHandle);
switch (SceneEventType)
{
case SceneEventType.Synchronize:
{
CopySceneSynchronizationData(reader);
break;
}
case SceneEventType.SynchronizeComplete:
{
CheckClientSynchronizationResults(reader);
break;
}
case SceneEventType.Load:
{
unsafe
{
// We store off the trailing in-scene placed serialized NetworkObject data to
// be processed once we are done loading.
m_HasInternalBuffer = true;
// We use Allocator.Persistent since scene loading could take longer than 4 frames
InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, reader.Length - reader.Position);
}
break;
}
case SceneEventType.ReSynchronize:
{
ReadClientReSynchronizationData(reader);
break;
}
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
{
ReadSceneEventProgressDone(reader);
break;
}
}
}
///
/// Client Side:
/// Prepares for a scene synchronization event and copies the scene synchronization data
/// into the internal buffer to be used throughout the synchronization process.
///
///
internal void CopySceneSynchronizationData(FastBufferReader reader)
{
m_NetworkObjectsSync.Clear();
reader.ReadValueSafe(out uint[] scenesToSynchronize);
reader.ReadValueSafe(out uint[] sceneHandlesToSynchronize);
ScenesToSynchronize = new Queue(scenesToSynchronize);
SceneHandlesToSynchronize = new Queue(sceneHandlesToSynchronize);
// is not packed!
reader.ReadValueSafe(out int sizeToCopy);
unsafe
{
if (!reader.TryBeginRead(sizeToCopy))
{
throw new OverflowException("Not enough space in the buffer to read recorded synchronization data size.");
}
m_HasInternalBuffer = true;
// We use Allocator.Persistent since scene synchronization will most likely take longer than 4 frames
InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, sizeToCopy);
}
}
///
/// Client Side:
/// This needs to occur at the end of a event when the scene has finished loading
/// Maximum number of objects that could theoretically be synchronized is 65536
///
internal void DeserializeScenePlacedObjects()
{
try
{
// is not packed!
InternalBuffer.ReadValueSafe(out ushort newObjectsCount);
for (ushort i = 0; i < newObjectsCount; i++)
{
InternalBuffer.ReadValueSafe(out int sceneHandle);
// Set our relative scene to the NetworkObject
m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(sceneHandle);
// Deserialize the NetworkObject
var sceneObject = new NetworkObject.SceneObject();
sceneObject.Deserialize(InternalBuffer);
NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager);
}
}
finally
{
InternalBuffer.Dispose();
m_HasInternalBuffer = false;
}
}
///
/// Client Side:
/// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned,
/// the server will compile a list and send back an Event_ReSync message to the client. This is where the
/// client handles any returned values by the server.
///
///
internal void ReadClientReSynchronizationData(FastBufferReader reader)
{
reader.ReadValueSafe(out uint[] networkObjectsToRemove);
if (networkObjectsToRemove.Length > 0)
{
var networkObjects = UnityEngine.Object.FindObjectsOfType();
var networkObjectIdToNetworkObject = new Dictionary();
foreach (var networkObject in networkObjects)
{
if (!networkObjectIdToNetworkObject.ContainsKey(networkObject.NetworkObjectId))
{
networkObjectIdToNetworkObject.Add(networkObject.NetworkObjectId, networkObject);
}
}
foreach (var networkObjectId in networkObjectsToRemove)
{
if (networkObjectIdToNetworkObject.ContainsKey(networkObjectId))
{
var networkObject = networkObjectIdToNetworkObject[networkObjectId];
networkObjectIdToNetworkObject.Remove(networkObjectId);
networkObject.IsSpawned = false;
if (m_NetworkManager.PrefabHandler.ContainsHandler(networkObject))
{
if (m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
{
m_NetworkManager.SpawnManager.SpawnedObjects.Remove(networkObjectId);
}
if (m_NetworkManager.SpawnManager.SpawnedObjectsList.Contains(networkObject))
{
m_NetworkManager.SpawnManager.SpawnedObjectsList.Remove(networkObject);
}
NetworkManager.Singleton.PrefabHandler.HandleNetworkPrefabDestroy(networkObject);
}
else
{
UnityEngine.Object.DestroyImmediate(networkObject.gameObject);
}
}
}
}
}
///
/// Server Side:
/// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned,
/// the server will compile a list and send back an Event_ReSync message to the client.
///
///
internal void WriteClientReSynchronizationData(FastBufferWriter writer)
{
//Write how many objects need to be removed
writer.WriteValueSafe(m_NetworkObjectsToBeRemoved.ToArray());
}
///
/// Server Side:
/// Determines if the client needs to be slightly re-synchronized if during the deserialization
/// process the server finds NetworkObjects that the client still thinks are spawned.
///
///
internal bool ClientNeedsReSynchronization()
{
return (m_NetworkObjectsToBeRemoved.Count > 0);
}
///
/// Server Side:
/// Determines if the client needs to be re-synchronized if during the deserialization
/// process the server finds NetworkObjects that the client still thinks are spawned but
/// have since been despawned.
///
///
internal void CheckClientSynchronizationResults(FastBufferReader reader)
{
m_NetworkObjectsToBeRemoved.Clear();
reader.ReadValueSafe(out uint networkObjectIdCount);
for (int i = 0; i < networkObjectIdCount; i++)
{
reader.ReadValueSafe(out uint networkObjectId);
if (!m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
{
m_NetworkObjectsToBeRemoved.Add(networkObjectId);
}
}
}
///
/// Client Side:
/// During the deserialization process of the servers Event_Sync, the client builds a list of
/// all NetworkObjectIds that were spawned. Upon responding to the server with the Event_Sync_Complete
/// this list is included for the server to review over and determine if the client needs a minor resynchronization
/// of NetworkObjects that might have been despawned while the client was processing the Event_Sync.
///
///
internal void WriteClientSynchronizationResults(FastBufferWriter writer)
{
//Write how many objects were spawned
writer.WriteValueSafe((uint)m_NetworkObjectsSync.Count);
foreach (var networkObject in m_NetworkObjectsSync)
{
writer.WriteValueSafe((uint)networkObject.NetworkObjectId);
}
}
///
/// Client Side:
/// During the processing of a server sent Event_Sync, this method will be called for each scene once
/// it is finished loading. The client will also build a list of NetworkObjects that it spawned during
/// this process which will be used as part of the Event_Sync_Complete response.
///
///
internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
{
try
{
// Process all NetworkObjects for this scene
InternalBuffer.ReadValueSafe(out int newObjectsCount);
for (int i = 0; i < newObjectsCount; i++)
{
// We want to make sure for each NetworkObject we have the appropriate scene selected as the scene that is
// currently being synchronized. This assures in-scene placed NetworkObjects will use the right NetworkObject
// from the list of populated
InternalBuffer.ReadValueSafe(out int handle);
m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(handle);
var sceneObject = new NetworkObject.SceneObject();
sceneObject.Deserialize(InternalBuffer);
var spawnedNetworkObject = NetworkObject.AddSceneObject(sceneObject, InternalBuffer, networkManager);
if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject))
{
m_NetworkObjectsSync.Add(spawnedNetworkObject);
}
}
}
finally
{
InternalBuffer.Dispose();
m_HasInternalBuffer = false;
}
}
///
/// Writes the all clients loaded or unloaded completed and timed out lists
///
///
internal void WriteSceneEventProgressDone(FastBufferWriter writer)
{
writer.WriteValueSafe((ushort)ClientsCompleted.Count);
foreach (var clientId in ClientsCompleted)
{
writer.WriteValueSafe(clientId);
}
writer.WriteValueSafe((ushort)ClientsTimedOut.Count);
foreach (var clientId in ClientsTimedOut)
{
writer.WriteValueSafe(clientId);
}
}
///
/// Reads the all clients loaded or unloaded completed and timed out lists
///
///
internal void ReadSceneEventProgressDone(FastBufferReader reader)
{
reader.ReadValueSafe(out ushort completedCount);
ClientsCompleted = new List();
for (int i = 0; i < completedCount; i++)
{
reader.ReadValueSafe(out ulong clientId);
ClientsCompleted.Add(clientId);
}
reader.ReadValueSafe(out ushort timedOutCount);
ClientsTimedOut = new List();
for (int i = 0; i < timedOutCount; i++)
{
reader.ReadValueSafe(out ulong clientId);
ClientsTimedOut.Add(clientId);
}
}
///
/// Used to release the pooled network buffer
///
public void Dispose()
{
if (m_HasInternalBuffer)
{
InternalBuffer.Dispose();
m_HasInternalBuffer = false;
}
}
///
/// Constructor for SceneEventData
///
internal SceneEventData(NetworkManager networkManager)
{
m_NetworkManager = networkManager;
SceneEventId = XXHash.Hash32(Guid.NewGuid().ToString());
}
}
}