From de8c695dafe8db3e2bf78183e15ab591ffce2347 Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 23 Apr 2025 13:13:54 +0200 Subject: [PATCH 1/7] Update package.json --- com.unity.netcode.gameobjects/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index 1f09e52ae8..8f572051d0 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.netcode.gameobjects", - "displayName": "Netcode for GameObjects", - "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", + "displayName": "Netcode for GameObjects_Extended", + "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer. This fork extends the spawn system for easy spawn management over network", "version": "2.3.2", "unity": "6000.0", "dependencies": { From 1871eef3e9a38982872ef0e3fd81273449535bd3 Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 23 Apr 2025 19:19:56 +0200 Subject: [PATCH 2/7] Allow send custom spawn data into the spawn command, no breaking changes --- .../Connection/NetworkConnectionManager.cs | 4 +- .../Runtime/Core/NetworkObject.cs | 156 +- .../Messaging/Messages/CreateObjectMessage.cs | 650 +-- .../SceneManagement/NetworkSceneManager.cs | 6 +- .../INetworkCustomSpawnDataReceiver.cs | 18 + .../Runtime/Spawning/NetworkPrefabHandler.cs | 86 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 4578 +++++++++-------- .../Prefabs/NetworkPrefabHandlerTests.cs | 6 +- 8 files changed, 2916 insertions(+), 2588 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 59a187a5fc..115bc2cbf8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -942,7 +942,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne /// /// Client-Side Spawning in distributed authority mode uses this to spawn the player. /// - internal void CreateAndSpawnPlayer(ulong ownerId) + internal void CreateAndSpawnPlayer(ulong ownerId, byte[] customSpawnData = null) { if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide) { @@ -950,7 +950,7 @@ internal void CreateAndSpawnPlayer(ulong ownerId) if (playerPrefab != null) { var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash; - var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation); + var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation, customSpawnData:customSpawnData); networkObject.IsSceneObject = false; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 64c8e96076..c99f1d15a1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1753,7 +1753,7 @@ private void OnDestroy() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) + internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject, byte[] customSpawnData) { if (NetworkManagerOwner == null) { @@ -1823,7 +1823,8 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) { - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); + Debug.Log("customSpawnData: " + (customSpawnData?.Length ?? 0).ToString()); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this, customSpawnData); } } } @@ -1833,7 +1834,8 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla // then we want to send a spawn notification. if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) { - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); + Debug.Log("customSpawnData: " + customSpawnData.Length); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this, customSpawnData); } } else @@ -1841,21 +1843,109 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla NetworkLog.LogWarningServer($"Ran into unknown conditional check during spawn when determining distributed authority mode or not"); } } - - /// - /// This invokes . - /// - /// The NetworkPrefab to instantiate and spawn. - /// The local instance of the NetworkManager connected to an session in progress. - /// The owner of the instance (defaults to server). - /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). - /// Whether the instance is a player object or not (default is false). - /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and - /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). - /// The starting poisiton of the instance. - /// The starting rotation of the instance. - /// The newly instantiated and spawned prefab instance. - public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + //internal void SpawnInternalExtended(bool destroyWithScene, ulong ownerClientId, bool playerObject, byte[] customSpawnData) + //{ + // if (NetworkManagerOwner == null) + // { + // NetworkManagerOwner = NetworkManager.Singleton; + // } + // if (!NetworkManager.IsListening) + // { + // throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects"); + // } + + // if ((!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) || (NetworkManager.DistributedAuthorityMode && !NetworkManager.LocalClient.IsSessionOwner && NetworkManager.LocalClientId != ownerClientId)) + // { + // if (NetworkManager.DistributedAuthorityMode) + // { + // throw new NotServerException($"When distributed authority mode is enabled, you can only spawn NetworkObjects that belong to the local instance! Local instance id {NetworkManager.LocalClientId} is not the same as the assigned owner id: {ownerClientId}!"); + // } + // else + // { + // throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); + // } + // } + + // if (NetworkManager.DistributedAuthorityMode) + // { + // if (NetworkManager.LocalClient == null || !NetworkManager.IsConnectedClient || !NetworkManager.ConnectionManager.LocalClient.IsApproved) + // { + // Debug.LogError($"Cannot spawn {name} until the client is fully connected to the session!"); + // return; + // } + // if (NetworkManager.NetworkConfig.EnableSceneManagement) + // { + // if (!NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(gameObject.scene.handle)) + // { + // // Most likely this issue is due to an integration test + // if (NetworkManager.LogLevel <= LogLevel.Developer) + // { + // NetworkLog.LogWarning($"Failed to find scene handle {gameObject.scene.handle} for {gameObject.name}!"); + // } + // // Just use the existing handle + // NetworkSceneHandle = gameObject.scene.handle; + // } + // else + // { + // NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[gameObject.scene.handle]; + // } + // } + // if (DontDestroyWithOwner && !IsOwnershipDistributable) + // { + // //Ownership |= OwnershipStatus.Distributable; + // // DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set + // if (NetworkManager.LogLevel == LogLevel.Developer) + // { + // NetworkLog.LogWarning("DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set. For now, if the NetworkObject does not destroy with the owner it will set ownership to SessionOwner."); + // } + // } + // } + + // NetworkManager.SpawnManager.SpawnNetworkObjectLocallyExtended(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); + + // if ((NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost) || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer)) + // { + // for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) + // { + // if (NetworkManager.ConnectedClientsList[i].ClientId == NetworkManager.ServerClientId) + // { + // continue; + // } + // if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) + // { + // NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this, customSpawnData); + // } + // } + // } + // else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) + // { + // // If spawning with observers or if not spawning with observers but the observer count is greater than 1 (i.e. owner/authority creating), + // // then we want to send a spawn notification. + // if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) + // { + // NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this, customSpawnData); + // } + // } + // else + // { + // NetworkLog.LogWarningServer($"Ran into unknown conditional check during spawn when determining distributed authority mode or not"); + // } + //} + + /// + /// This invokes . + /// + /// The NetworkPrefab to instantiate and spawn. + /// The local instance of the NetworkManager connected to an session in progress. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { var networkObject = networkPrefab.GetComponent(); if (networkObject == null) @@ -1920,10 +2010,10 @@ public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ow /// Spawns this across the network. Can only be called from the Server /// /// Should the object be destroyed when the scene is changed - public void Spawn(bool destroyWithScene = false) + public void Spawn(bool destroyWithScene = false, byte[] customSpawnData = null) { var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; - SpawnInternal(destroyWithScene, clientId, false); + SpawnInternal(destroyWithScene, clientId, false, customSpawnData); } /// @@ -1931,19 +2021,23 @@ public void Spawn(bool destroyWithScene = false) /// /// The clientId to own the object /// Should the object be destroyed when the scene is changed - public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) + public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) { - SpawnInternal(destroyWithScene, clientId, false); + SpawnInternal(destroyWithScene, clientId, false, customSpawnData); } + //public void SpawnWithOwnershipExtended(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) + //{ + // SpawnInternalExtended(destroyWithScene, clientId, false, customSpawnData); + //} - /// - /// Spawns a across the network and makes it the player object for the given client - /// - /// The clientId who's player object this is - /// Should the object be destroyed when the scene is changed - public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) + /// + /// Spawns a across the network and makes it the player object for the given client + /// + /// The clientId who's player object this is + /// Should the object be destroyed when the scene is changed + public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) { - SpawnInternal(destroyWithScene, clientId, true); + SpawnInternal(destroyWithScene, clientId, true, customSpawnData); } /// @@ -3211,10 +3305,10 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager /// NetworkManager instance /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed - internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) + internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false, byte[] customSpawnData = null) { //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, customSpawnData); if (networkObject == null) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index 02f4263e9d..33053fcaa7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -3,309 +3,349 @@ namespace Unity.Netcode { - internal struct CreateObjectMessage : INetworkMessage - { - public int Version => 0; - - private const string k_Name = "CreateObjectMessage"; - - public NetworkObject.SceneObject ObjectInfo; - private FastBufferReader m_ReceivedNetworkVariableData; - - // DA - NGO CMB SERVICE NOTES: - // The ObserverIds and ExistingObserverIds will only be populated if k_UpdateObservers is set - // ObserverIds is the full list of observers (see below) - internal ulong[] ObserverIds; - - // While this does consume a bit more bandwidth, this is only sent by the authority/owner - // and can be used to determine which clients should receive the ObjectInfo serialized data. - // All other already existing observers just need to receive the NewObserverIds and the - // NetworkObjectId - internal ulong[] NewObserverIds; - - // If !IncludesSerializedObject then the NetworkObjectId will be serialized. - // This happens when we are just sending an update to the observers list - // to clients that already have the NetworkObject spawned - internal ulong NetworkObjectId; - - private const byte k_IncludesSerializedObject = 0x01; - private const byte k_UpdateObservers = 0x02; - private const byte k_UpdateNewObservers = 0x04; - - - private byte m_CreateObjectMessageTypeFlags; - - internal bool IncludesSerializedObject - { - get - { - return GetFlag(k_IncludesSerializedObject); - } - - set - { - SetFlag(value, k_IncludesSerializedObject); - } - } - - internal bool UpdateObservers - { - get - { - return GetFlag(k_UpdateObservers); - } - - set - { - SetFlag(value, k_UpdateObservers); - } - } - - internal bool UpdateNewObservers - { - get - { - return GetFlag(k_UpdateNewObservers); - } - - set - { - SetFlag(value, k_UpdateNewObservers); - } - } - - private bool GetFlag(int flag) - { - return (m_CreateObjectMessageTypeFlags & flag) != 0; - } - - private void SetFlag(bool set, byte flag) - { - if (set) { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags | flag); } - else { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags & ~flag); } - } - - public void Serialize(FastBufferWriter writer, int targetVersion) - { - writer.WriteValueSafe(m_CreateObjectMessageTypeFlags); - - if (UpdateObservers) - { - BytePacker.WriteValuePacked(writer, ObserverIds.Length); - foreach (var clientId in ObserverIds) - { - BytePacker.WriteValuePacked(writer, clientId); - } - } - - if (UpdateNewObservers) - { - BytePacker.WriteValuePacked(writer, NewObserverIds.Length); - foreach (var clientId in NewObserverIds) - { - BytePacker.WriteValuePacked(writer, clientId); - } - } - - if (IncludesSerializedObject) - { - ObjectInfo.Serialize(writer); - } - else - { - BytePacker.WriteValuePacked(writer, NetworkObjectId); - } - } - - public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) - { - var networkManager = (NetworkManager)context.SystemOwner; - if (!networkManager.IsClient) - { - return false; - } - - reader.ReadValueSafe(out m_CreateObjectMessageTypeFlags); - if (UpdateObservers) - { - var length = 0; - ByteUnpacker.ReadValuePacked(reader, out length); - ObserverIds = new ulong[length]; - var clientId = (ulong)0; - for (int i = 0; i < length; i++) - { - ByteUnpacker.ReadValuePacked(reader, out clientId); - ObserverIds[i] = clientId; - } - } - - if (UpdateNewObservers) - { - var length = 0; - ByteUnpacker.ReadValuePacked(reader, out length); - NewObserverIds = new ulong[length]; - var clientId = (ulong)0; - for (int i = 0; i < length; i++) - { - ByteUnpacker.ReadValuePacked(reader, out clientId); - NewObserverIds[i] = clientId; - } - } - - if (IncludesSerializedObject) - { - ObjectInfo.Deserialize(reader); - } - else - { - ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); - } - - if (!networkManager.NetworkConfig.ForceSamePrefabs && !networkManager.SpawnManager.HasPrefab(ObjectInfo)) - { - networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, ObjectInfo.Hash, reader, ref context, k_Name); - return false; - } - m_ReceivedNetworkVariableData = reader; - - return true; - } - - public void Handle(ref NetworkContext context) - { - var networkManager = (NetworkManager)context.SystemOwner; - // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing - if (networkManager.SceneManager.ShouldDeferCreateObject()) - { - networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); - } - else - { - if (networkManager.DistributedAuthorityMode && !IncludesSerializedObject && UpdateObservers) - { - ObjectInfo = new NetworkObject.SceneObject() - { - NetworkObjectId = NetworkObjectId, - }; - } - CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void CreateObject(ref NetworkManager networkManager, ref NetworkSceneManager.DeferredObjectCreation deferredObjectCreation) - { - var senderId = deferredObjectCreation.SenderId; - var observerIds = deferredObjectCreation.ObserverIds; - var newObserverIds = deferredObjectCreation.NewObserverIds; - var messageSize = deferredObjectCreation.MessageSize; - var sceneObject = deferredObjectCreation.SceneObject; - var networkVariableData = deferredObjectCreation.FastBufferReader; - CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds) - { - var networkObject = (NetworkObject)null; - try - { - if (!networkManager.DistributedAuthorityMode) - { - networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager); - } - else - { - var hasObserverIdList = observerIds != null && observerIds.Length > 0; - var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; - // Depending upon visibility of the NetworkObject and the client in question, it could be that - // this client already has visibility of this NetworkObject - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(sceneObject.NetworkObjectId)) - { - // If so, then just get the local instance - networkObject = networkManager.SpawnManager.SpawnedObjects[sceneObject.NetworkObjectId]; - - // This should not happen, logging error just in case - if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) - { - NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {sceneObject.NetworkObjectId}!"); - } - else // Trap to make sure the owner is not receiving any messages it sent - if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) - { - NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{sceneObject.NetworkObjectId}!"); - } - } - else - { - networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, true); - } - - // DA - NGO CMB SERVICE NOTES: - // It is possible for two clients to connect at the exact same time which, due to client-side spawning, can cause each client - // to miss their spawns. For now, all player NetworkObject spawns will always be visible to all known connected clients - var clientList = hasObserverIdList && !networkObject.IsPlayerObject ? observerIds : networkManager.ConnectedClientsIds; - - // Update the observers for this instance - foreach (var clientId in clientList) - { - networkObject.Observers.Add(clientId); - } - - // Mock CMB Service and forward to all clients - if (networkManager.DAHost) - { - // DA - NGO CMB SERVICE NOTES: - // (*** See above notes fist ***) - // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively - // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, - // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each - // player's AOI. - if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) - { - // For same-frame newly spawned players that might not be aware of all other players, update the player's observer - // list. - observerIds = clientList.ToArray(); - } - - var createObjectMessage = new CreateObjectMessage() - { - ObjectInfo = sceneObject, - m_ReceivedNetworkVariableData = networkVariableData, - ObserverIds = hasObserverIdList ? observerIds : null, - NetworkObjectId = networkObject.NetworkObjectId, - IncludesSerializedObject = true, - }; - foreach (var clientId in clientList) - { - // DA - NGO CMB SERVICE NOTES: - // If the authority did not specify the list of clients and the client is not an observer or we are the owner/originator - // or we are the DAHost, then we skip sending the message. - if ((!hasObserverIdList && (!networkObject.Observers.Contains(clientId)) || - clientId == networkObject.OwnerClientId || clientId == NetworkManager.ServerClientId)) - { - continue; - } - - // DA - NGO CMB SERVICE NOTES: - // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. - // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated - // observers list to that client's instance. - createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); - - networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); - } - } - } - if (networkObject != null) - { - networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); - } - } - catch (System.Exception ex) - { - UnityEngine.Debug.LogException(ex); - } - } - } + internal struct CreateObjectMessage : INetworkMessage + { + public int Version => 0; + + private const string k_Name = "CreateObjectMessage"; + + public NetworkObject.SceneObject ObjectInfo; + private FastBufferReader m_ReceivedNetworkVariableData; + public byte[] CustomSpawnData; + + // DA - NGO CMB SERVICE NOTES: + // The ObserverIds and ExistingObserverIds will only be populated if k_UpdateObservers is set + // ObserverIds is the full list of observers (see below) + internal ulong[] ObserverIds; + + // While this does consume a bit more bandwidth, this is only sent by the authority/owner + // and can be used to determine which clients should receive the ObjectInfo serialized data. + // All other already existing observers just need to receive the NewObserverIds and the + // NetworkObjectId + internal ulong[] NewObserverIds; + + // If !IncludesSerializedObject then the NetworkObjectId will be serialized. + // This happens when we are just sending an update to the observers list + // to clients that already have the NetworkObject spawned + internal ulong NetworkObjectId; + + private const byte k_IncludesSerializedObject = 0x01; + private const byte k_UpdateObservers = 0x02; + private const byte k_UpdateNewObservers = 0x04; + private const byte k_HasCreationMetadata = 0x08; + + + private byte m_CreateObjectMessageTypeFlags; + + internal bool IncludesSerializedObject + { + get + { + return GetFlag(k_IncludesSerializedObject); + } + + set + { + SetFlag(value, k_IncludesSerializedObject); + } + } + + internal bool UpdateObservers + { + get + { + return GetFlag(k_UpdateObservers); + } + + set + { + SetFlag(value, k_UpdateObservers); + } + } + + internal bool UpdateNewObservers + { + get + { + return GetFlag(k_UpdateNewObservers); + } + + set + { + SetFlag(value, k_UpdateNewObservers); + } + } + + internal bool HasCustomSpawnData + { + get + { + return GetFlag(k_HasCreationMetadata); + } + set + { + SetFlag(value, k_HasCreationMetadata); + } + } + + private bool GetFlag(int flag) + { + return (m_CreateObjectMessageTypeFlags & flag) != 0; + } + + private void SetFlag(bool set, byte flag) + { + if (set) { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags | flag); } + else { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags & ~flag); } + } + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + writer.WriteValueSafe(m_CreateObjectMessageTypeFlags); + + if (HasCustomSpawnData) + { + BytePacker.WriteValuePacked(writer, CustomSpawnData.Length); + foreach (var mdByte in CustomSpawnData) + { + BytePacker.WriteValuePacked(writer, mdByte); + } + } + + if (UpdateObservers) + { + BytePacker.WriteValuePacked(writer, ObserverIds.Length); + foreach (var clientId in ObserverIds) + { + BytePacker.WriteValuePacked(writer, clientId); + } + } + + if (UpdateNewObservers) + { + BytePacker.WriteValuePacked(writer, NewObserverIds.Length); + foreach (var clientId in NewObserverIds) + { + BytePacker.WriteValuePacked(writer, clientId); + } + } + + if (IncludesSerializedObject) + { + ObjectInfo.Serialize(writer); + } + else + { + BytePacker.WriteValuePacked(writer, NetworkObjectId); + } + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return false; + } + + reader.ReadValueSafe(out m_CreateObjectMessageTypeFlags); + + if (HasCustomSpawnData) + { + var length = 0; + ByteUnpacker.ReadValuePacked(reader, out length); + CustomSpawnData = new byte[length]; + var clientId = (byte)0; + for (int i = 0; i < length; i++) + { + ByteUnpacker.ReadValuePacked(reader, out clientId); + CustomSpawnData[i] = clientId; + } + } + + if (UpdateObservers) + { + var length = 0; + ByteUnpacker.ReadValuePacked(reader, out length); + ObserverIds = new ulong[length]; + var clientId = (ulong)0; + for (int i = 0; i < length; i++) + { + ByteUnpacker.ReadValuePacked(reader, out clientId); + ObserverIds[i] = clientId; + } + } + + if (UpdateNewObservers) + { + var length = 0; + ByteUnpacker.ReadValuePacked(reader, out length); + NewObserverIds = new ulong[length]; + var clientId = (ulong)0; + for (int i = 0; i < length; i++) + { + ByteUnpacker.ReadValuePacked(reader, out clientId); + NewObserverIds[i] = clientId; + } + } + + if (IncludesSerializedObject) + { + ObjectInfo.Deserialize(reader); + } + else + { + ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); + } + + if (!networkManager.NetworkConfig.ForceSamePrefabs && !networkManager.SpawnManager.HasPrefab(ObjectInfo)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, ObjectInfo.Hash, reader, ref context, k_Name); + return false; + } + m_ReceivedNetworkVariableData = reader; + + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing + if (networkManager.SceneManager.ShouldDeferCreateObject()) + { + networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds, CustomSpawnData); + } + else + { + if (networkManager.DistributedAuthorityMode && !IncludesSerializedObject && UpdateObservers) + { + ObjectInfo = new NetworkObject.SceneObject() + { + NetworkObjectId = NetworkObjectId, + }; + } + CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds, CustomSpawnData); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateObject(ref NetworkManager networkManager, ref NetworkSceneManager.DeferredObjectCreation deferredObjectCreation) + { + var senderId = deferredObjectCreation.SenderId; + var observerIds = deferredObjectCreation.ObserverIds; + var newObserverIds = deferredObjectCreation.NewObserverIds; + var messageSize = deferredObjectCreation.MessageSize; + var sceneObject = deferredObjectCreation.SceneObject; + var networkVariableData = deferredObjectCreation.FastBufferReader; + var customSpawnData = deferredObjectCreation.CustomSpawnData; + CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds, customSpawnData); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds, byte[] customSpawnData) + { + var networkObject = (NetworkObject)null; + try + { + if (!networkManager.DistributedAuthorityMode) + { + networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, customSpawnData: customSpawnData); //TODO: Metadata could go into sceneObject structure, as its used more widely + } + else + { + var hasObserverIdList = observerIds != null && observerIds.Length > 0; + var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; + // Depending upon visibility of the NetworkObject and the client in question, it could be that + // this client already has visibility of this NetworkObject + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(sceneObject.NetworkObjectId)) + { + // If so, then just get the local instance + networkObject = networkManager.SpawnManager.SpawnedObjects[sceneObject.NetworkObjectId]; + + // This should not happen, logging error just in case + if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) + { + NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {sceneObject.NetworkObjectId}!"); + } + else // Trap to make sure the owner is not receiving any messages it sent + if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) + { + NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{sceneObject.NetworkObjectId}!"); + } + } + else + { + networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, true); + } + + // DA - NGO CMB SERVICE NOTES: + // It is possible for two clients to connect at the exact same time which, due to client-side spawning, can cause each client + // to miss their spawns. For now, all player NetworkObject spawns will always be visible to all known connected clients + var clientList = hasObserverIdList && !networkObject.IsPlayerObject ? observerIds : networkManager.ConnectedClientsIds; + + // Update the observers for this instance + foreach (var clientId in clientList) + { + networkObject.Observers.Add(clientId); + } + + // Mock CMB Service and forward to all clients + if (networkManager.DAHost) + { + // DA - NGO CMB SERVICE NOTES: + // (*** See above notes fist ***) + // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively + // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, + // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each + // player's AOI. + if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) + { + // For same-frame newly spawned players that might not be aware of all other players, update the player's observer + // list. + observerIds = clientList.ToArray(); + } + + var createObjectMessage = new CreateObjectMessage() + { + ObjectInfo = sceneObject, + m_ReceivedNetworkVariableData = networkVariableData, + ObserverIds = hasObserverIdList ? observerIds : null, + NetworkObjectId = networkObject.NetworkObjectId, + IncludesSerializedObject = true, + CustomSpawnData = customSpawnData, + HasCustomSpawnData = customSpawnData != null && customSpawnData.Length > 0, + }; + foreach (var clientId in clientList) + { + // DA - NGO CMB SERVICE NOTES: + // If the authority did not specify the list of clients and the client is not an observer or we are the owner/originator + // or we are the DAHost, then we skip sending the message. + if ((!hasObserverIdList && (!networkObject.Observers.Contains(clientId)) || + clientId == networkObject.OwnerClientId || clientId == NetworkManager.ServerClientId)) + { + continue; + } + + // DA - NGO CMB SERVICE NOTES: + // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. + // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated + // observers list to that client's instance. + createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); + + networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject, customSpawnData); + } + } + } + if (networkObject != null) + { + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); + } + } + catch (System.Exception ex) + { + UnityEngine.Debug.LogException(ex); + } + } + } } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 331d5ff112..a0ac3a7199 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -3154,6 +3154,7 @@ internal struct DeferredObjectCreation // When we transfer session owner and we are using a DAHost, this will be pertinent (otherwise it is not when connected to a DA service) internal ulong[] ObserverIds; internal ulong[] NewObserverIds; + internal byte[] CustomSpawnData; internal NetworkObject.SceneObject SceneObject; internal FastBufferReader FastBufferReader; } @@ -3162,7 +3163,7 @@ internal struct DeferredObjectCreation internal int DeferredObjectCreationCount; // The added clientIds is specific to DAHost when session ownership changes and a normal client is controlling scene loading - internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader, ulong[] observerIds, ulong[] newObserverIds) + internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader, ulong[] observerIds, ulong[] newObserverIds, byte[] customSpawnData) { var deferredObjectCreationEntry = new DeferredObjectCreation() { @@ -3171,7 +3172,8 @@ internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject. ObserverIds = observerIds, NewObserverIds = newObserverIds, SceneObject = sceneObject, - }; + CustomSpawnData = customSpawnData, + }; unsafe { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs new file mode 100644 index 0000000000..b59a89e146 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs @@ -0,0 +1,18 @@ + +namespace Unity.Netcode +{ + public interface INetworkCustomSpawnDataReceiver + { + /// + /// Called on the client side after receiving custom spawn metadata during the instantiation process. + /// This extends the interface to allow for custom spawn data handling. + /// This method is used to pass additional data from the server to the client to help identify or configure + /// the local object instance that should be linked to the spawned NetworkObject. + /// + /// This is invoked just before is called, + /// allowing you to cache or prepare information needed during instantiation. + /// + /// The metadata buffer sent from the server during the spawn message. + void OnCustomSpawnDataReceived(byte[] customSpawnData); + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index fe0dd270e9..88410e55d9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -10,22 +10,26 @@ namespace Unity.Netcode /// public interface INetworkPrefabInstanceHandler { - /// - /// Client Side Only - /// Once an implementation is registered with the , this method will be called every time - /// a Network Prefab associated is spawned on clients - /// - /// Note On Hosts: Use the - /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client. - /// - /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active - /// via the method. - /// - /// the owner for the to be instantiated - /// the initial/default position for the to be instantiated - /// the initial/default rotation for the to be instantiated - /// The instantiated NetworkObject instance. Returns null if instantiation fails. - NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); + /// + /// Client Side Only + /// Once an implementation is registered with the , this method will be called every time + /// a Network Prefab associated is spawned on clients

+ /// + /// Note On Hosts: Use the + /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client.

+ /// + /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active + /// via the method.

+ /// + ///Note on Custom Spawn Data: If you want to send custom data to be processed during this spawn, you can do so by implementing + /// via the method. + ///
+ /// the owner for the to be instantiated + /// the initial/default position for the to be instantiated + /// the initial/default rotation for the to be instantiated + /// a byte array of custom data sent during the spawn to be instantiated + /// The instantiated NetworkObject instance. Returns null if instantiation fails. + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); /// /// Invoked on Client and Server @@ -44,11 +48,11 @@ public interface INetworkPrefabInstanceHandler void Destroy(NetworkObject networkObject); } - /// - /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) - /// Register custom prefab handlers by implementing the interface. - /// - public class NetworkPrefabHandler + /// + /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) + /// Register custom prefab handlers by implementing the interface. + /// + public class NetworkPrefabHandler { private NetworkManager m_NetworkManager; @@ -252,11 +256,13 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) /// /// /// - internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation) + internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] customSpawnData) { if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) { - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); + if(customSpawnData != null && prefabInstanceHandler is INetworkCustomSpawnDataReceiver receiver) + receiver.OnCustomSpawnDataReceived(customSpawnData); + var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. @@ -270,12 +276,36 @@ internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulo return null; } + - /// - /// Will invoke the implementation's Destroy method - /// - /// - internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) + internal NetworkObject HandleNetworkPrefabSpawnExtended(int handlerId, ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] customSpawnData) + { + if (NetworkSpawnManager.customHandlers.TryGetValue(handlerId, out var prefabInstanceHandler)) + { + if (customSpawnData != null && prefabInstanceHandler is INetworkCustomSpawnDataReceiver receiver) + receiver.OnCustomSpawnDataReceived(customSpawnData); + var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); + + //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) + //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. + if (networkObjectInstance != null && !NetworkSpawnManager.customPrefabInstanceToHandlers.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + { + NetworkSpawnManager.customPrefabInstanceToHandlers.Add(networkObjectInstance.GlobalObjectIdHash, handlerId); + } + + return networkObjectInstance; + } + + return null; + } + + + + /// + /// Will invoke the implementation's Destroy method + /// + /// + internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) { var networkObjectInstanceHash = networkObjectInstance.GlobalObjectIdHash; diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 19227c82ba..28d1afbf5d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -7,2239 +7,2383 @@ namespace Unity.Netcode { - /// - /// Class that handles object spawning - /// - public class NetworkSpawnManager - { - // Stores the objects that need to be shown at end-of-frame - internal Dictionary> ObjectsToShowToClient = new Dictionary>(); - - internal Dictionary> ClientsToShowObject = new Dictionary>(); - - /// - /// The currently spawned objects - /// - public readonly Dictionary SpawnedObjects = new Dictionary(); - - /// - /// A list of the spawned objects - /// - public readonly HashSet SpawnedObjectsList = new HashSet(); - - /// - /// Use to get all NetworkObjects owned by a client - /// Ownership to Objects Table Format: - /// [ClientId][NetworkObjectId][NetworkObject] - /// Server: Keeps track of all clients' ownership - /// Client: Keeps track of only its ownership - /// - public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); - - /// - /// Object to Ownership Table: - /// [NetworkObjectId][ClientId] - /// Used internally to find the client Id that currently owns - /// the NetworkObject - /// - private Dictionary m_ObjectToOwnershipTable = new Dictionary(); - - /// - /// In distributed authority mode, a list of known spawned player NetworkObject instance is maintained by each client. - /// - public IReadOnlyList PlayerObjects => m_PlayerObjects; - // Since NetworkSpawnManager is destroyed when NetworkManager shuts down, it will always be an empty list for each new network session. - // DANGO-TODO: We need to add something like a ConnectionStateMessage that is sent by either the DAHost or CMBService to each client when a client - // is connected and synchronized or when a cient disconnects (but the player objects list we should keep as it is useful to have). - private List m_PlayerObjects = new List(); - - private Dictionary> m_PlayerObjectsTable = new Dictionary>(); - - /// - /// Returns the connect client identifiers - /// - /// connected client identifier list - public List GetConnectedPlayers() - { - return m_PlayerObjectsTable.Keys.ToList(); - } - - /// - /// Adds a player object and updates all other players' observers list - /// - private void AddPlayerObject(NetworkObject playerObject) - { - if (!playerObject.IsPlayerObject) - { - if (NetworkManager.LogLevel == LogLevel.Normal) - { - NetworkLog.LogError($"Attempting to register a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); - return; - } - } - - foreach (var player in m_PlayerObjects) - { - // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer. - if (player.SpawnWithObservers) - { - player.Observers.Add(playerObject.OwnerClientId); - } - - // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object. - if (playerObject.SpawnWithObservers) - { - playerObject.Observers.Add(player.OwnerClientId); - } - } - - // Only if spawn with observers is set or we are using a distributed authority network topology and this is the client's player should we add - // the owner as an observer. - if (playerObject.SpawnWithObservers || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == playerObject.OwnerClientId)) - { - playerObject.Observers.Add(playerObject.OwnerClientId); - } - - m_PlayerObjects.Add(playerObject); - if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) - { - m_PlayerObjectsTable.Add(playerObject.OwnerClientId, new List()); - } - m_PlayerObjectsTable[playerObject.OwnerClientId].Add(playerObject); - } - - internal void UpdateNetworkClientPlayer(NetworkObject playerObject) - { - // If the player's client does not already have a NetworkClient entry - if (!NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId)) - { - // Add the player's client - NetworkManager.ConnectionManager.AddClient(playerObject.OwnerClientId); - } - var playerNetworkClient = NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId]; - - // If a client changes their player object, then we should adjust for the client's new player - if (playerNetworkClient.PlayerObject != null && m_PlayerObjects.Contains(playerNetworkClient.PlayerObject)) - { - // Just remove the previous player object but keep the assigned observers of the NetworkObject - RemovePlayerObject(playerNetworkClient.PlayerObject); - } - - // Now update the associated NetworkClient's player object - NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].AssignPlayerObject(ref playerObject); - AddPlayerObject(playerObject); - } - - /// - /// Removes a player object and updates all other players' observers list - /// - private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObject = false) - { - if (!playerObject.IsPlayerObject) - { - if (NetworkManager.LogLevel == LogLevel.Normal) - { - NetworkLog.LogError($"Attempting to deregister a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); - return; - } - } - playerObject.IsPlayerObject = false; - m_PlayerObjects.Remove(playerObject); - if (m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) - { - m_PlayerObjectsTable[playerObject.OwnerClientId].Remove(playerObject); - if (m_PlayerObjectsTable[playerObject.OwnerClientId].Count == 0) - { - m_PlayerObjectsTable.Remove(playerObject.OwnerClientId); - } - } - - if (NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId) && destroyingObject) - { - NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null; - } - } - - internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId) - { - if (!ObjectsToShowToClient.ContainsKey(clientId)) - { - ObjectsToShowToClient.Add(clientId, new List()); - } - ObjectsToShowToClient[clientId].Add(networkObject); - if (NetworkManager.DistributedAuthorityMode) - { - if (!ClientsToShowObject.ContainsKey(networkObject)) - { - ClientsToShowObject.Add(networkObject, new List()); - } - ClientsToShowObject[networkObject].Add(clientId); - } - } - - // returns whether any matching objects would have become visible and were returned to hidden state - internal bool RemoveObjectFromShowingTo(NetworkObject networkObject, ulong clientId) - { - if (NetworkManager.DistributedAuthorityMode) - { - if (ClientsToShowObject.ContainsKey(networkObject)) - { - ClientsToShowObject[networkObject].Remove(clientId); - if (ClientsToShowObject[networkObject].Count == 0) - { - ClientsToShowObject.Remove(networkObject); - } - } - } - var ret = false; - if (!ObjectsToShowToClient.ContainsKey(clientId)) - { - return false; - } - - // probably overkill, but deals with multiple entries - while (ObjectsToShowToClient[clientId].Contains(networkObject)) - { - Debug.LogWarning( - "Object was shown and hidden from the same client in the same Network frame. As a result, the client will _not_ receive a NetworkSpawn"); - ObjectsToShowToClient[clientId].Remove(networkObject); - ret = true; - } - - if (ret) - { - networkObject.Observers.Remove(clientId); - } - - return ret; - } - - /// - /// Used to update a NetworkObject's ownership - /// - internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false) - { - var previousOwner = newOwner; - - // Use internal lookup table to see if the NetworkObject has a previous owner - if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId)) - { - // Keep track of the previous owner's ClientId - previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId]; - - // We are either despawning (remove) or changing ownership (assign) - if (isRemoving) - { - m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId); - } - else - { - // If we already had this owner in our table then just exit - if (NetworkManager.DistributedAuthorityMode && previousOwner == newOwner) - { - return; - } - m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner; - } - } - else - { - // Otherwise, just add a new lookup entry - m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner); - } - - // Check to see if we had a previous owner - if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner)) - { - // Before updating the previous owner, assure this entry exists - if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId)) - { - // Remove the previous owner's entry - OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); - - // If we are removing the entry (i.e. despawning or client lost ownership) - if (isRemoving) - { - return; - } - } - else - { - // Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen - throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?"); - } - } - - // If the owner doesn't have an entry then create one - if (!OwnershipToObjectsTable.ContainsKey(newOwner)) - { - OwnershipToObjectsTable.Add(newOwner, new Dictionary()); - } - - // Sanity check to make sure we don't already have this entry (we shouldn't) - if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId)) - { - // Add the new ownership entry - OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject); - } - else if (isRemoving) - { - OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); - } - else if (NetworkManager.LogLevel == LogLevel.Developer && previousOwner == newOwner) - { - NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!"); - } - } - - /// - /// Returns an array of all NetworkObjects that belong to a client. - /// - /// the client's id - /// returns an array of the s owned by the client - public NetworkObject[] GetClientOwnedObjects(ulong clientId) - { - if (!OwnershipToObjectsTable.ContainsKey(clientId)) - { - OwnershipToObjectsTable.Add(clientId, new Dictionary()); - } - return OwnershipToObjectsTable[clientId].Values.ToArray(); - } - - /// - /// Gets the NetworkManager associated with this SpawnManager. - /// - public NetworkManager NetworkManager { get; } - - internal readonly Queue ReleasedNetworkObjectIds = new Queue(); - private ulong m_NetworkObjectIdCounter; - - // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations - private List m_TargetClientIds = new List(); - - internal ulong GetNetworkObjectId() - { - if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) - { - return ReleasedNetworkObjectIds.Dequeue().NetworkId; - } - - m_NetworkObjectIdCounter++; - - // DANGO-TODO: Need a more robust solution here. - return m_NetworkObjectIdCounter + (NetworkManager.LocalClientId * 10000); - } - - /// - /// Returns the local player object or null if one does not exist - /// - /// The local player object or null if one does not exist - public NetworkObject GetLocalPlayerObject() - { - return GetPlayerNetworkObject(NetworkManager.LocalClientId); - } - - /// - /// Returns all instances assigned to the client identifier - /// - /// the client identifier of the player - /// A list of instances (if more than one are assigned) - public List GetPlayerNetworkObjects(ulong clientId) - { - if (m_PlayerObjectsTable.ContainsKey(clientId)) - { - return m_PlayerObjectsTable[clientId]; - } - return null; - } - - /// - /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. - /// - /// the client identifier of the player - /// The player object with a given clientId or null if one does not exist - public NetworkObject GetPlayerNetworkObject(ulong clientId) - { - if (!NetworkManager.DistributedAuthorityMode) - { - if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId) - { - throw new NotServerException("Only the server can find player objects from other clients."); - } - if (TryGetNetworkClient(clientId, out NetworkClient networkClient)) - { - return networkClient.PlayerObject; - } - } - else - { - if (m_PlayerObjectsTable.ContainsKey(clientId)) - { - return m_PlayerObjectsTable[clientId].First(); - } - } - - return null; - } - - /// - /// Helper function to get a network client for a clientId from the NetworkManager. - /// On the server this will check the list. - /// On a non-server this will check the only. - /// - /// The clientId for which to try getting the NetworkClient for. - /// The found NetworkClient. Null if no client was found. - /// True if a NetworkClient with a matching id was found else false. - private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient) - { - if (NetworkManager.IsServer) - { - return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); - } - - if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId) - { - networkClient = NetworkManager.LocalClient; - return true; - } - - networkClient = null; - return false; - } - - /// - /// Not used. - /// - /// not used - /// not used - protected virtual void InternalOnOwnershipChanged(ulong perviousOwner, ulong newOwner) - { - - } - - internal void RemoveOwnership(NetworkObject networkObject) - { - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.ShutdownInProgress) - { - Debug.LogError($"Removing ownership is invalid in Distributed Authority Mode. Use {nameof(ChangeOwnership)} instead."); - return; - } - ChangeOwnership(networkObject, NetworkManager.ServerClientId, true); - } - - private Dictionary m_LastChangeInOwnership = new Dictionary(); - private const int k_MaximumTickOwnershipChangeMultiplier = 6; - - internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false) - { - if (clientId == networkObject.OwnerClientId) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)"); - } - return; - } - - // For client-server: - // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes, - // then notify the user they could potentially lose state updates if developer logging is enabled. - if (NetworkManager.LogLevel == LogLevel.Developer && !NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup) - { - for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) - { - if (networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0) - { - NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss."); - break; - } - } - } - - if (NetworkManager.DistributedAuthorityMode) - { - // Ensure only the session owner can change ownership (i.e. acquire) and that the session owner is not trying to assign a non-session owner client - // ownership of a NetworkObject with SessionOwner permissions. - if (networkObject.IsOwnershipSessionOwner && (!NetworkManager.LocalClient.IsSessionOwner || clientId != NetworkManager.CurrentSessionOwner)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Session Owner Only] You cannot change ownership of a {nameof(NetworkObject)} that has the {NetworkObject.OwnershipStatus.SessionOwner} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.SessionOwnerOnly); - return; - } - - // If are not authorized and this is not an approved ownership change, then check to see if we can change ownership - if (!isAuthorized && !isRequestApproval) - { - if (networkObject.IsOwnershipLocked) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Locked] You cannot change ownership while a {nameof(NetworkObject)} is locked!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.Locked); - return; - } - if (networkObject.IsRequestInProgress) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Request Pending] You cannot change ownership while a {nameof(NetworkObject)} has a pending ownership request!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestInProgress); - return; - } - if (networkObject.IsOwnershipRequestRequired) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Request Required] You cannot change ownership directly if a {nameof(NetworkObject)} has the {NetworkObject.OwnershipStatus.RequestRequired} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired); - return; - } - if (!networkObject.IsOwnershipTransferable) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Not transferrable] You cannot change ownership of a {nameof(NetworkObject)} that does not have the {NetworkObject.OwnershipStatus.Transferable} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.NotTransferrable); - return; - } - } - } - else if (!isAuthorized) - { - throw new NotServerException("Only the server can change ownership"); - } - - if (!networkObject.IsSpawned) - { - throw new SpawnStateException("Object is not spawned"); - } - - if (networkObject.OwnerClientId == clientId && networkObject.PreviousOwnerId == clientId) - { - if (NetworkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarningServer($"[Already Owner] Unnecessary ownership change for {networkObject.name} as it is already the owned by client-{clientId}"); - } - return; - } - - if (!networkObject.Observers.Contains(clientId)) - { - if (NetworkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarningServer($"[Invalid Owner] Cannot send Ownership change as client-{clientId} cannot see {networkObject.name}! Use {nameof(NetworkObject.NetworkShow)} first."); - } - return; - } - - // Save the previous owner to work around a bit of a nasty issue with assuring NetworkVariables are serialized when ownership changes - var originalPreviousOwnerId = networkObject.PreviousOwnerId; - var originalOwner = networkObject.OwnerClientId; - - // Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates - networkObject.PreviousOwnerId = networkObject.OwnerClientId; - - // Assign the new owner - networkObject.OwnerClientId = clientId; - - // Always notify locally on the server when ownership is lost - networkObject.InvokeBehaviourOnLostOwnership(); - - // Authority adds entries for all client ownership - UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); - - // Always notify locally on the server when a new owner is assigned - networkObject.InvokeBehaviourOnGainedOwnership(); - - // If we are the original owner, then we want to synchronize owner read & write NetworkVariables. - if (originalOwner == NetworkManager.LocalClientId) - { - networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId); - } - - var size = 0; - if (NetworkManager.DistributedAuthorityMode) - { - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId, - DistributedAuthorityMode = NetworkManager.DistributedAuthorityMode, - RequestApproved = isRequestApproval, - OwnershipIsChanging = true, - RequestClientId = networkObject.PreviousOwnerId, - OwnershipFlags = (ushort)networkObject.Ownership, - }; - // If we are connected to the CMB service or not the DAHost (i.e. pure DA-Clients only) - if (NetworkManager.CMBServiceConnection || !NetworkManager.DAHost) - { - // Always update the network properties in distributed authority mode for the client gaining ownership - for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) - { - networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties(); - } - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(NetworkManager.LocalClientId, networkObject, size); - } - else // We are the DAHost so broadcast the ownership change - { - foreach (var client in NetworkManager.ConnectedClients) - { - if (client.Value.ClientId == NetworkManager.ServerClientId) - { - continue; - } - if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) - { - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); - } - } - } - } - else // Normal Client-Server mode - { - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId, - }; - foreach (var client in NetworkManager.ConnectedClients) - { - if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) - { - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); - } - } - } - - // After we have sent the change ownership message to all client observers, invoke the ownership changed notification. - /// !!Important!! - /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership - /// change can be sent from NetworkBehaviours that override the - networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); - - // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. - if (!NetworkManager.DistributedAuthorityMode) - { - if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId)) - { - m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f); - } - var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; - m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier); - } - } - - internal bool HasPrefab(NetworkObject.SceneObject sceneObject) - { - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) - { - if (NetworkManager.PrefabHandler.ContainsHandler(sceneObject.Hash)) - { - return true; - } - if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.TryGetValue(sceneObject.Hash, out var networkPrefab)) - { - switch (networkPrefab.Override) - { - default: - case NetworkPrefabOverride.None: - return networkPrefab.Prefab != null; - case NetworkPrefabOverride.Hash: - case NetworkPrefabOverride.Prefab: - return networkPrefab.OverridingTargetPrefab != null; - } - } - - return false; - } - var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(sceneObject.Hash, sceneObject.NetworkSceneHandle); - return networkObject != null; - } - - internal enum InstantiateAndSpawnErrorTypes - { - NetworkPrefabNull, - NotAuthority, - InvokedWhenShuttingDown, - NotRegisteredNetworkPrefab, - NetworkManagerNull, - NoActiveSession, - } - - internal static readonly Dictionary InstantiateAndSpawnErrors = new Dictionary( - new KeyValuePair[]{ - new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkPrefabNull, $"The {nameof(NetworkObject)} prefab parameter was null!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NotAuthority, $"Only the server has authority to {nameof(InstantiateAndSpawn)}!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown, $"Invoking {nameof(InstantiateAndSpawn)} while shutting down! Calls to {nameof(InstantiateAndSpawn)} will be ignored."), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab, $"The {nameof(NetworkObject)} parameter is not a registered network prefab. Did you forget to register it or are you trying to instantiate and spawn an instance of a network prefab?"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkManagerNull, $"The {nameof(NetworkManager)} parameter was null!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NoActiveSession, "You can only invoke this method when you are connected to an existing/in-progress network session!") - }); - - /// - /// Use this method to easily instantiate and spawn an instance of a network prefab. - /// InstantiateAndSpawn will: - /// - Find any override associated with the prefab - /// - If there is no override, then the current prefab type is used. - /// - Create an instance of the prefab (or its override). - /// - Spawn the prefab instance - /// - /// The of the pefab asset. - /// The owner of the instance (defaults to server). - /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). - /// Whether the instance is a player object or not (default is false). - /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and - /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). - /// The starting poisiton of the instance. - /// The starting rotation of the instance. - /// The newly instantiated and spawned prefab instance. - public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - { - if (networkPrefab == null) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NetworkPrefabNull]); - return null; - } - - ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; - // We only need to check for authority when running in client-server mode - if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); - return null; - } - - if (NetworkManager.ShutdownInProgress) - { - Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); - return null; - } - - // Verify it is actually a valid prefab - if (!NetworkManager.NetworkConfig.Prefabs.Contains(networkPrefab.gameObject)) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); - return null; - } - - return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); - } - - /// - /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! - /// - internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - { - var networkObject = networkPrefab; - // - Host and clients always instantiate the override if one exists. - // - Server instantiates the original prefab unless: - // -- forceOverride is set to true =or= - // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. - // - Distributed authority mode always spawns the override if one exists. - if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) - { - networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation); - } - else // Under this case, server instantiate the prefab passed in. - { - networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation); - } - - if (networkObject == null) - { - Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); - return null; - } - networkObject.IsPlayerObject = isPlayerObject; - networkObject.transform.position = position; - networkObject.transform.rotation = rotation; - // If spawning as a player, then invoke SpawnAsPlayerObject - if (isPlayerObject) - { - networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene); - } - else // Otherwise just spawn with ownership - { - networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); - } - return networkObject; - } - - /// - /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the - /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. - /// - internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) - { - NetworkObject networkObject = null; - // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class - if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) - { - // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default); - networkObject.NetworkManagerOwner = NetworkManager; - } - else - { - // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash - var networkPrefabReference = (GameObject)null; - var inScenePlacedWithNoSceneManagement = !NetworkManager.NetworkConfig.EnableSceneManagement && isScenePlaced; - - if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) - { - var networkPrefab = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash]; - - switch (networkPrefab.Override) - { - default: - case NetworkPrefabOverride.None: - networkPrefabReference = networkPrefab.Prefab; - break; - case NetworkPrefabOverride.Hash: - case NetworkPrefabOverride.Prefab: - { - // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the - // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically - // but might want to use the same source network prefab as an in-scene placed NetworkObject. - // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically - // spawning them so the original prefab placed is preserved and this is not needed) - if (inScenePlacedWithNoSceneManagement) - { - networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; - } - else - { - networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; - } - break; - } - } - } - - // If not, then there is an issue (user possibly didn't register the prefab properly?) - if (networkPrefabReference == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); - } - } - else - { - // Create prefab instance while applying any pre-assigned position and rotation values - networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation); - } - } - return networkObject; - } - - /// - /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients - /// the instance. - /// !!! Should only be invoked by unless used by an integration test !!! - /// - /// - /// should be the base prefab value and not the - /// overrided value. - /// (Can be used for integration testing) - /// - /// prefab to instantiate - /// of the base prefab instance - /// conditional position in place of the network prefab's default position - /// conditional rotation in place of the network prefab's default rotation - /// the instance of the - internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) - { - var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); - networkObject.transform.position = position ?? networkObject.transform.position; - networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; - networkObject.NetworkManagerOwner = NetworkManager; - networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; - return networkObject; - } - - /// - /// Creates a local NetowrkObject to be spawned. - /// - /// - /// For most cases this is client-side only, with the exception of when the server - /// is spawning a player. - /// - internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject) - { - NetworkObject networkObject = null; - var globalObjectIdHash = sceneObject.Hash; - var position = sceneObject.HasTransform ? sceneObject.Transform.Position : default; - var rotation = sceneObject.HasTransform ? sceneObject.Transform.Rotation : default; - var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default; - var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default; - var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays; - - // If scene management is disabled or the NetworkObject was dynamically spawned - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) - { - networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject); - } - else // Get the in-scene placed NetworkObject - { - networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); - if (networkObject == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); - } - } - - // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so - // NetworkBehaviours will have their OnNetworkSpawn method invoked - if (networkObject != null && !networkObject.gameObject.activeInHierarchy) - { - networkObject.gameObject.SetActive(true); - } - } - - if (networkObject != null) - { - networkObject.DestroyWithScene = sceneObject.DestroyWithScene; - networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; - networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner; - networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags; - - var nonNetworkObjectParent = false; - // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) - // This is a special case scenario where a late joining client has joined and loaded one or - // more scenes that contain nested in-scene placed NetworkObject children yet the server's - // synchronization information does not indicate the NetworkObject in question has a parent =or= - // the parent has changed. - // For this we will want to remove the parent before spawning and setting the transform values based - // on several possible scenarios. - if (sceneObject.IsSceneObject && networkObject.transform.parent != null) - { - var parentNetworkObject = networkObject.transform.parent.GetComponent(); - - // special case to handle being parented under a GameObject with no NetworkObject - nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent; - - // If the in-scene placed NetworkObject has a parent NetworkObject... - if (parentNetworkObject) - { - // Then remove the parent only if: - // - The authority says we don't have a parent (but locally we do). - // - The auhtority says we have a parent but either of the two are true: - // -- It isn't the same parent. - // -- It was parented using world position stays. - if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet - && (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays))) - { - // If parenting without notifications then we are temporarily removing the parent to set the transform - // values before reparenting under the current parent. - networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent); - } - } - } - - // Set the transform only if the sceneObject includes transform information. - if (sceneObject.HasTransform) - { - // If world position stays is true or we have auto object parent synchronization disabled - // then we want to apply the position and rotation values world space relative - if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) - { - networkObject.transform.position = position; - networkObject.transform.rotation = rotation; - } - else - { - networkObject.transform.localPosition = position; - networkObject.transform.localRotation = rotation; - } - - // SPECIAL CASE: - // Since players are created uniquely we don't apply scale because - // the ConnectionApprovalResponse does not currently provide the - // ability to specify scale. So, we just use the default scale of - // the network prefab used to represent the player. - // Note: not doing this would set the player's scale to zero since - // that is the default value of Vector3. - if (!sceneObject.IsPlayerObject) - { - // Since scale is always applied to local space scale, we do the transform - // space logic during serialization such that it works out whether AutoObjectParentSync - // is enabled or not (see NetworkObject.SceneObject) - networkObject.transform.localScale = scale; - } - } - - if (sceneObject.HasParent) - { - // Go ahead and set network parenting properties, if the latest parent is not set then pass in null - // (we always want to set worldPositionStays) - ulong? parentId = null; - if (sceneObject.IsLatestParentSet) - { - parentId = parentNetworkId; - } - networkObject.SetNetworkParenting(parentId, worldPositionStays); - } - - // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL - // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. - if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) - { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); - } - } - return networkObject; - } - - /// - /// Invoked from: - /// - ConnectionManager after instantiating a player prefab when running in client-server. - /// - NetworkObject when spawning a newly instantiated NetworkObject for the first time. - /// - NetworkSceneManager after a server/session-owner has loaded a scene to locally spawn the newly instantiated in-scene placed NetworkObjects. - /// - NetworkSpawnManager when spawning any already loaded in-scene placed NetworkObjects (client-server or session owner). - /// - /// Client-Server: - /// Server is the only instance that invokes this method. - /// - /// Distributed Authority: - /// DAHost client and standard DA clients invoke this method. - /// - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) - { - if (networkObject == null) - { - throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); - } - - if (networkObject.IsSpawned) - { - Debug.LogError($"{networkObject.name} is already spawned!"); - return; - } - - if (!sceneObject) - { - var networkObjectChildren = networkObject.GetComponentsInChildren(); - if (networkObjectChildren.Length > 1) - { - Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); - } - } - // Invoke NetworkBehaviour.OnPreSpawn methods - networkObject.InvokeBehaviourNetworkPreSpawn(); - - // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning - // For now, this is the best place I could find to add all connected clients as observers for newly - // instantiated and spawned NetworkObjects on the authoritative side. - if (NetworkManager.DistributedAuthorityMode) - { - if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) - { - networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; - networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; - } - - // Always add the owner/authority even if SpawnWithObservers is false - // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) - if (!networkObject.SpawnWithObservers) - { - networkObject.Observers.Add(ownerClientId); - } - else - { - foreach (var clientId in NetworkManager.ConnectedClientsIds) - { - // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) - { - continue; - } - networkObject.Observers.Add(clientId); - } - - // Sanity check to make sure the owner is always included - // Itentionally checking as opposed to just assigning in order to generate notification. - if (!networkObject.Observers.Contains(ownerClientId)) - { - Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); - networkObject.Observers.Add(ownerClientId); - } - } - } - SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); - - // Invoke NetworkBehaviour.OnPostSpawn methods - networkObject.InvokeBehaviourNetworkPostSpawn(); - } - - /// - /// This is only invoked to instantiate a serialized NetworkObject via - /// - /// - /// - /// IMPORTANT: Pre spawn methods need to be invoked from within . - /// - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene) - { - if (networkObject == null) - { - throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); - } - - if (networkObject.IsSpawned) - { - throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); - } - - // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) - SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene); - - // It is ok to invoke NetworkBehaviour.OnPostSpawn methods - networkObject.InvokeBehaviourNetworkPostSpawn(); - } - - private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) - { - if (SpawnedObjects.ContainsKey(networkId)) - { - Debug.LogWarning($"[{NetworkManager.name}] Trying to spawn {networkObject.name} with a {nameof(NetworkObject.NetworkObjectId)} of {networkId} but it is already in the spawned list!"); - return; - } - - networkObject.IsSpawned = true; - networkObject.IsSceneObject = sceneObject; - - // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects - // Note: Always check SceneOriginHandle directly at this specific location. - if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0) - { - networkObject.SceneOrigin = networkObject.gameObject.scene; - } - - // For integration testing, this makes sure that the appropriate NetworkManager is assigned to - // the NetworkObject since it uses the NetworkManager.Singleton when not set - if (networkObject.NetworkManagerOwner != NetworkManager) - { - networkObject.NetworkManagerOwner = NetworkManager; - } - - networkObject.NetworkObjectId = networkId; - - networkObject.DestroyWithScene = sceneObject || destroyWithScene; - - networkObject.IsPlayerObject = playerObject; - - networkObject.OwnerClientId = ownerClientId; - - // When spawned, previous owner is always the first assigned owner - networkObject.PreviousOwnerId = ownerClientId; - - // If this the player and the client is the owner, then lock ownership by default - if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) - { - networkObject.SetOwnershipLock(); - } - SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); - SpawnedObjectsList.Add(networkObject); - - // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, - // then add all connected clients as observers - if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) - { - // If running as a server only, then make sure to always add the server's client identifier - if (!NetworkManager.IsHost) - { - networkObject.Observers.Add(NetworkManager.LocalClientId); - } - - // Add client observers - for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) - { - // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) - { - continue; - } - networkObject.Observers.Add(NetworkManager.ConnectedClientsIds[i]); - } - } - - networkObject.ApplyNetworkParenting(); - NetworkObject.CheckOrphanChildren(); - - AddNetworkObjectToSceneChangedUpdates(networkObject); - - networkObject.InvokeBehaviourNetworkSpawn(); - - NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId); - - // propagate the IsSceneObject setting to child NetworkObjects - var children = networkObject.GetComponentsInChildren(); - foreach (var childObject in children) - { - // Do not propagate the in-scene object setting if a child was dynamically spawned. - if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value) - { - continue; - } - childObject.IsSceneObject = sceneObject; - } - - // Only dynamically spawned NetworkObjects are allowed - if (!sceneObject) - { - networkObject.SubscribeToActiveSceneForSynch(); - } - - if (networkObject.IsPlayerObject) - { - UpdateNetworkClientPlayer(networkObject); - } - - // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set - // then assign this to the PrefabGlobalObjectIdHash - if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) - { - networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; - } - } - - internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); - - // Pre-allocating to avoid the initial constructor hit - internal Stack CleanUpDisposedObjects = new Stack(); - - internal void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject) - { - if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && - !NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) - { - if (networkObject.UpdateForSceneChanges()) - { - NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject); - } - } - } - - internal void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject) - { - if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && - NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) - { - NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId); - } - } - - internal unsafe void UpdateNetworkObjectSceneChanges() - { - foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) - { - // If it fails the first update then don't add for updates - if (!entry.Value.UpdateForSceneChanges()) - { - CleanUpDisposedObjects.Push(entry.Key); - } - } - - // Clean up any NetworkObjects that no longer exist (destroyed before they should be or the like) - while (CleanUpDisposedObjects.Count > 0) - { - NetworkObjectsToSynchronizeSceneChanges.Remove(CleanUpDisposedObjects.Pop()); - } - } - - internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) - { - // If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message. - var updateObservers = NetworkManager.DistributedAuthorityMode && networkObject.SpawnWithObservers; - - // Only skip if distributed authority mode is not enabled - if (clientId == NetworkManager.ServerClientId && !NetworkManager.DistributedAuthorityMode) - { - return; - } - - var message = new CreateObjectMessage - { - ObjectInfo = networkObject.GetMessageSceneObject(clientId, NetworkManager.DistributedAuthorityMode), - IncludesSerializedObject = true, - UpdateObservers = NetworkManager.DistributedAuthorityMode, - ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, - }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - } - - /// - /// Only used to update object visibility/observers. - /// ** Clients are the only instances that use this method ** - /// - internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject networkObject) - { - if (!NetworkManager.DistributedAuthorityMode) - { - throw new Exception("[SendSpawnCallForObserverUpdate] Invoking a distributed authority only method when distributed authority is not enabled!"); - } - - var message = new CreateObjectMessage - { - ObjectInfo = networkObject.GetMessageSceneObject(), - ObserverIds = networkObject.Observers.ToArray(), - NewObserverIds = newObservers.ToArray(), - IncludesSerializedObject = true, - UpdateObservers = true, - UpdateNewObservers = true, - }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); - foreach (var clientId in newObservers) - { - // TODO: We might want to track observer update sent as well? - NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - } - } - - internal ulong? GetSpawnParentId(NetworkObject networkObject) - { - NetworkObject parentNetworkObject = null; - - if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) - { - parentNetworkObject = networkObject.transform.parent.GetComponent(); - } - - if (parentNetworkObject == null) - { - return null; - } - - return parentNetworkObject.NetworkObjectId; - } - - internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false, bool playerDisconnect = false) - { - if (!networkObject.IsSpawned) - { - NetworkLog.LogErrorServer("Object is not spawned!"); - return; - } - - if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) - { - NetworkLog.LogErrorServer("Only server can despawn objects"); - return; - } - - if (NetworkManager.DistributedAuthorityMode && networkObject.OwnerClientId != NetworkManager.LocalClientId) - { - if (!NetworkManager.DAHost || NetworkManager.DAHost && !playerDisconnect) - { - NetworkLog.LogErrorServer($"In distributed authority mode, only the owner of the NetworkObject can despawn it! Local Client is ({NetworkManager.LocalClientId}) while the owner is ({networkObject.OwnerClientId})"); - return; - } - } - OnDespawnObject(networkObject, destroyObject, playerDisconnect); - } - - // Makes scene objects ready to be reused - internal void ServerResetShudownStateForSceneObjects() - { + /// + /// Class that handles object spawning + /// + public class NetworkSpawnManager + { + // Stores the objects that need to be shown at end-of-frame + internal Dictionary> ObjectsToShowToClient = new Dictionary>(); + + internal Dictionary> ClientsToShowObject = new Dictionary>(); + + /// + /// The currently spawned objects + /// + public readonly Dictionary SpawnedObjects = new Dictionary(); + + /// + /// A list of the spawned objects + /// + public readonly HashSet SpawnedObjectsList = new HashSet(); + + /// + /// Use to get all NetworkObjects owned by a client + /// Ownership to Objects Table Format: + /// [ClientId][NetworkObjectId][NetworkObject] + /// Server: Keeps track of all clients' ownership + /// Client: Keeps track of only its ownership + /// + public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); + + /// + /// Object to Ownership Table: + /// [NetworkObjectId][ClientId] + /// Used internally to find the client Id that currently owns + /// the NetworkObject + /// + private Dictionary m_ObjectToOwnershipTable = new Dictionary(); + + /// + /// In distributed authority mode, a list of known spawned player NetworkObject instance is maintained by each client. + /// + public IReadOnlyList PlayerObjects => m_PlayerObjects; + // Since NetworkSpawnManager is destroyed when NetworkManager shuts down, it will always be an empty list for each new network session. + // DANGO-TODO: We need to add something like a ConnectionStateMessage that is sent by either the DAHost or CMBService to each client when a client + // is connected and synchronized or when a cient disconnects (but the player objects list we should keep as it is useful to have). + private List m_PlayerObjects = new List(); + + private Dictionary> m_PlayerObjectsTable = new Dictionary>(); + + /// + /// Returns the connect client identifiers + /// + /// connected client identifier list + public List GetConnectedPlayers() + { + return m_PlayerObjectsTable.Keys.ToList(); + } + + /// + /// Adds a player object and updates all other players' observers list + /// + private void AddPlayerObject(NetworkObject playerObject) + { + if (!playerObject.IsPlayerObject) + { + if (NetworkManager.LogLevel == LogLevel.Normal) + { + NetworkLog.LogError($"Attempting to register a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); + return; + } + } + + foreach (var player in m_PlayerObjects) + { + // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer. + if (player.SpawnWithObservers) + { + player.Observers.Add(playerObject.OwnerClientId); + } + + // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object. + if (playerObject.SpawnWithObservers) + { + playerObject.Observers.Add(player.OwnerClientId); + } + } + + // Only if spawn with observers is set or we are using a distributed authority network topology and this is the client's player should we add + // the owner as an observer. + if (playerObject.SpawnWithObservers || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == playerObject.OwnerClientId)) + { + playerObject.Observers.Add(playerObject.OwnerClientId); + } + + m_PlayerObjects.Add(playerObject); + if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) + { + m_PlayerObjectsTable.Add(playerObject.OwnerClientId, new List()); + } + m_PlayerObjectsTable[playerObject.OwnerClientId].Add(playerObject); + } + + internal void UpdateNetworkClientPlayer(NetworkObject playerObject) + { + // If the player's client does not already have a NetworkClient entry + if (!NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId)) + { + // Add the player's client + NetworkManager.ConnectionManager.AddClient(playerObject.OwnerClientId); + } + var playerNetworkClient = NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId]; + + // If a client changes their player object, then we should adjust for the client's new player + if (playerNetworkClient.PlayerObject != null && m_PlayerObjects.Contains(playerNetworkClient.PlayerObject)) + { + // Just remove the previous player object but keep the assigned observers of the NetworkObject + RemovePlayerObject(playerNetworkClient.PlayerObject); + } + + // Now update the associated NetworkClient's player object + NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].AssignPlayerObject(ref playerObject); + AddPlayerObject(playerObject); + } + + /// + /// Removes a player object and updates all other players' observers list + /// + private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObject = false) + { + if (!playerObject.IsPlayerObject) + { + if (NetworkManager.LogLevel == LogLevel.Normal) + { + NetworkLog.LogError($"Attempting to deregister a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); + return; + } + } + playerObject.IsPlayerObject = false; + m_PlayerObjects.Remove(playerObject); + if (m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) + { + m_PlayerObjectsTable[playerObject.OwnerClientId].Remove(playerObject); + if (m_PlayerObjectsTable[playerObject.OwnerClientId].Count == 0) + { + m_PlayerObjectsTable.Remove(playerObject.OwnerClientId); + } + } + + if (NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId) && destroyingObject) + { + NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null; + } + } + + internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId) + { + if (!ObjectsToShowToClient.ContainsKey(clientId)) + { + ObjectsToShowToClient.Add(clientId, new List()); + } + ObjectsToShowToClient[clientId].Add(networkObject); + if (NetworkManager.DistributedAuthorityMode) + { + if (!ClientsToShowObject.ContainsKey(networkObject)) + { + ClientsToShowObject.Add(networkObject, new List()); + } + ClientsToShowObject[networkObject].Add(clientId); + } + } + + // returns whether any matching objects would have become visible and were returned to hidden state + internal bool RemoveObjectFromShowingTo(NetworkObject networkObject, ulong clientId) + { + if (NetworkManager.DistributedAuthorityMode) + { + if (ClientsToShowObject.ContainsKey(networkObject)) + { + ClientsToShowObject[networkObject].Remove(clientId); + if (ClientsToShowObject[networkObject].Count == 0) + { + ClientsToShowObject.Remove(networkObject); + } + } + } + var ret = false; + if (!ObjectsToShowToClient.ContainsKey(clientId)) + { + return false; + } + + // probably overkill, but deals with multiple entries + while (ObjectsToShowToClient[clientId].Contains(networkObject)) + { + Debug.LogWarning( + "Object was shown and hidden from the same client in the same Network frame. As a result, the client will _not_ receive a NetworkSpawn"); + ObjectsToShowToClient[clientId].Remove(networkObject); + ret = true; + } + + if (ret) + { + networkObject.Observers.Remove(clientId); + } + + return ret; + } + + /// + /// Used to update a NetworkObject's ownership + /// + internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false) + { + var previousOwner = newOwner; + + // Use internal lookup table to see if the NetworkObject has a previous owner + if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId)) + { + // Keep track of the previous owner's ClientId + previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId]; + + // We are either despawning (remove) or changing ownership (assign) + if (isRemoving) + { + m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId); + } + else + { + // If we already had this owner in our table then just exit + if (NetworkManager.DistributedAuthorityMode && previousOwner == newOwner) + { + return; + } + m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner; + } + } + else + { + // Otherwise, just add a new lookup entry + m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner); + } + + // Check to see if we had a previous owner + if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner)) + { + // Before updating the previous owner, assure this entry exists + if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Remove the previous owner's entry + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + + // If we are removing the entry (i.e. despawning or client lost ownership) + if (isRemoving) + { + return; + } + } + else + { + // Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen + throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?"); + } + } + + // If the owner doesn't have an entry then create one + if (!OwnershipToObjectsTable.ContainsKey(newOwner)) + { + OwnershipToObjectsTable.Add(newOwner, new Dictionary()); + } + + // Sanity check to make sure we don't already have this entry (we shouldn't) + if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Add the new ownership entry + OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject); + } + else if (isRemoving) + { + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + } + else if (NetworkManager.LogLevel == LogLevel.Developer && previousOwner == newOwner) + { + NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!"); + } + } + + /// + /// Returns an array of all NetworkObjects that belong to a client. + /// + /// the client's id + /// returns an array of the s owned by the client + public NetworkObject[] GetClientOwnedObjects(ulong clientId) + { + if (!OwnershipToObjectsTable.ContainsKey(clientId)) + { + OwnershipToObjectsTable.Add(clientId, new Dictionary()); + } + return OwnershipToObjectsTable[clientId].Values.ToArray(); + } + + /// + /// Gets the NetworkManager associated with this SpawnManager. + /// + public NetworkManager NetworkManager { get; } + + internal readonly Queue ReleasedNetworkObjectIds = new Queue(); + private ulong m_NetworkObjectIdCounter; + + // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations + private List m_TargetClientIds = new List(); + + internal ulong GetNetworkObjectId() + { + if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) + { + return ReleasedNetworkObjectIds.Dequeue().NetworkId; + } + + m_NetworkObjectIdCounter++; + + // DANGO-TODO: Need a more robust solution here. + return m_NetworkObjectIdCounter + (NetworkManager.LocalClientId * 10000); + } + + /// + /// Returns the local player object or null if one does not exist + /// + /// The local player object or null if one does not exist + public NetworkObject GetLocalPlayerObject() + { + return GetPlayerNetworkObject(NetworkManager.LocalClientId); + } + + /// + /// Returns all instances assigned to the client identifier + /// + /// the client identifier of the player + /// A list of instances (if more than one are assigned) + public List GetPlayerNetworkObjects(ulong clientId) + { + if (m_PlayerObjectsTable.ContainsKey(clientId)) + { + return m_PlayerObjectsTable[clientId]; + } + return null; + } + + /// + /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. + /// + /// the client identifier of the player + /// The player object with a given clientId or null if one does not exist + public NetworkObject GetPlayerNetworkObject(ulong clientId) + { + if (!NetworkManager.DistributedAuthorityMode) + { + if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId) + { + throw new NotServerException("Only the server can find player objects from other clients."); + } + if (TryGetNetworkClient(clientId, out NetworkClient networkClient)) + { + return networkClient.PlayerObject; + } + } + else + { + if (m_PlayerObjectsTable.ContainsKey(clientId)) + { + return m_PlayerObjectsTable[clientId].First(); + } + } + + return null; + } + + /// + /// Helper function to get a network client for a clientId from the NetworkManager. + /// On the server this will check the list. + /// On a non-server this will check the only. + /// + /// The clientId for which to try getting the NetworkClient for. + /// The found NetworkClient. Null if no client was found. + /// True if a NetworkClient with a matching id was found else false. + private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient) + { + if (NetworkManager.IsServer) + { + return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); + } + + if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId) + { + networkClient = NetworkManager.LocalClient; + return true; + } + + networkClient = null; + return false; + } + + /// + /// Not used. + /// + /// not used + /// not used + protected virtual void InternalOnOwnershipChanged(ulong perviousOwner, ulong newOwner) + { + + } + + internal void RemoveOwnership(NetworkObject networkObject) + { + if (NetworkManager.DistributedAuthorityMode && !NetworkManager.ShutdownInProgress) + { + Debug.LogError($"Removing ownership is invalid in Distributed Authority Mode. Use {nameof(ChangeOwnership)} instead."); + return; + } + ChangeOwnership(networkObject, NetworkManager.ServerClientId, true); + } + + private Dictionary m_LastChangeInOwnership = new Dictionary(); + private const int k_MaximumTickOwnershipChangeMultiplier = 6; + + internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false) + { + if (clientId == networkObject.OwnerClientId) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)"); + } + return; + } + + // For client-server: + // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes, + // then notify the user they could potentially lose state updates if developer logging is enabled. + if (NetworkManager.LogLevel == LogLevel.Developer && !NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup) + { + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + if (networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0) + { + NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss."); + break; + } + } + } + + if (NetworkManager.DistributedAuthorityMode) + { + // Ensure only the session owner can change ownership (i.e. acquire) and that the session owner is not trying to assign a non-session owner client + // ownership of a NetworkObject with SessionOwner permissions. + if (networkObject.IsOwnershipSessionOwner && (!NetworkManager.LocalClient.IsSessionOwner || clientId != NetworkManager.CurrentSessionOwner)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Session Owner Only] You cannot change ownership of a {nameof(NetworkObject)} that has the {NetworkObject.OwnershipStatus.SessionOwner} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.SessionOwnerOnly); + return; + } + + // If are not authorized and this is not an approved ownership change, then check to see if we can change ownership + if (!isAuthorized && !isRequestApproval) + { + if (networkObject.IsOwnershipLocked) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Locked] You cannot change ownership while a {nameof(NetworkObject)} is locked!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.Locked); + return; + } + if (networkObject.IsRequestInProgress) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Request Pending] You cannot change ownership while a {nameof(NetworkObject)} has a pending ownership request!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestInProgress); + return; + } + if (networkObject.IsOwnershipRequestRequired) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Request Required] You cannot change ownership directly if a {nameof(NetworkObject)} has the {NetworkObject.OwnershipStatus.RequestRequired} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired); + return; + } + if (!networkObject.IsOwnershipTransferable) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Not transferrable] You cannot change ownership of a {nameof(NetworkObject)} that does not have the {NetworkObject.OwnershipStatus.Transferable} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.NotTransferrable); + return; + } + } + } + else if (!isAuthorized) + { + throw new NotServerException("Only the server can change ownership"); + } + + if (!networkObject.IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (networkObject.OwnerClientId == clientId && networkObject.PreviousOwnerId == clientId) + { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarningServer($"[Already Owner] Unnecessary ownership change for {networkObject.name} as it is already the owned by client-{clientId}"); + } + return; + } + + if (!networkObject.Observers.Contains(clientId)) + { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarningServer($"[Invalid Owner] Cannot send Ownership change as client-{clientId} cannot see {networkObject.name}! Use {nameof(NetworkObject.NetworkShow)} first."); + } + return; + } + + // Save the previous owner to work around a bit of a nasty issue with assuring NetworkVariables are serialized when ownership changes + var originalPreviousOwnerId = networkObject.PreviousOwnerId; + var originalOwner = networkObject.OwnerClientId; + + // Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates + networkObject.PreviousOwnerId = networkObject.OwnerClientId; + + // Assign the new owner + networkObject.OwnerClientId = clientId; + + // Always notify locally on the server when ownership is lost + networkObject.InvokeBehaviourOnLostOwnership(); + + // Authority adds entries for all client ownership + UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); + + // Always notify locally on the server when a new owner is assigned + networkObject.InvokeBehaviourOnGainedOwnership(); + + // If we are the original owner, then we want to synchronize owner read & write NetworkVariables. + if (originalOwner == NetworkManager.LocalClientId) + { + networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId); + } + + var size = 0; + if (NetworkManager.DistributedAuthorityMode) + { + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId, + DistributedAuthorityMode = NetworkManager.DistributedAuthorityMode, + RequestApproved = isRequestApproval, + OwnershipIsChanging = true, + RequestClientId = networkObject.PreviousOwnerId, + OwnershipFlags = (ushort)networkObject.Ownership, + }; + // If we are connected to the CMB service or not the DAHost (i.e. pure DA-Clients only) + if (NetworkManager.CMBServiceConnection || !NetworkManager.DAHost) + { + // Always update the network properties in distributed authority mode for the client gaining ownership + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties(); + } + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(NetworkManager.LocalClientId, networkObject, size); + } + else // We are the DAHost so broadcast the ownership change + { + foreach (var client in NetworkManager.ConnectedClients) + { + if (client.Value.ClientId == NetworkManager.ServerClientId) + { + continue; + } + if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + } + } + else // Normal Client-Server mode + { + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId, + }; + foreach (var client in NetworkManager.ConnectedClients) + { + if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + } + + // After we have sent the change ownership message to all client observers, invoke the ownership changed notification. + /// !!Important!! + /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership + /// change can be sent from NetworkBehaviours that override the + networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); + + // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. + if (!NetworkManager.DistributedAuthorityMode) + { + if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId)) + { + m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f); + } + var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; + m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier); + } + } + + internal bool HasPrefab(NetworkObject.SceneObject sceneObject) + { + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) + { + if (NetworkManager.PrefabHandler.ContainsHandler(sceneObject.Hash)) + { + return true; + } + if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.TryGetValue(sceneObject.Hash, out var networkPrefab)) + { + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + return networkPrefab.Prefab != null; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + return networkPrefab.OverridingTargetPrefab != null; + } + } + + return false; + } + var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(sceneObject.Hash, sceneObject.NetworkSceneHandle); + return networkObject != null; + } + + internal enum InstantiateAndSpawnErrorTypes + { + NetworkPrefabNull, + NotAuthority, + InvokedWhenShuttingDown, + NotRegisteredNetworkPrefab, + NetworkManagerNull, + NoActiveSession, + } + + internal static readonly Dictionary InstantiateAndSpawnErrors = new Dictionary( + new KeyValuePair[]{ + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkPrefabNull, $"The {nameof(NetworkObject)} prefab parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotAuthority, $"Only the server has authority to {nameof(InstantiateAndSpawn)}!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown, $"Invoking {nameof(InstantiateAndSpawn)} while shutting down! Calls to {nameof(InstantiateAndSpawn)} will be ignored."), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab, $"The {nameof(NetworkObject)} parameter is not a registered network prefab. Did you forget to register it or are you trying to instantiate and spawn an instance of a network prefab?"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkManagerNull, $"The {nameof(NetworkManager)} parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NoActiveSession, "You can only invoke this method when you are connected to an existing/in-progress network session!") + }); + + /// + /// Use this method to easily instantiate and spawn an instance of a network prefab. + /// InstantiateAndSpawn will: + /// - Find any override associated with the prefab + /// - If there is no override, then the current prefab type is used. + /// - Create an instance of the prefab (or its override). + /// - Spawn the prefab instance + /// + /// The of the pefab asset. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default, byte[] customSpawnData = null) + { + if (networkPrefab == null) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NetworkPrefabNull]); + return null; + } + + ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; + // We only need to check for authority when running in client-server mode + if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); + return null; + } + + if (NetworkManager.ShutdownInProgress) + { + Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); + return null; + } + + // Verify it is actually a valid prefab + if (!NetworkManager.NetworkConfig.Prefabs.Contains(networkPrefab.gameObject)) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); + return null; + } + + return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation, customSpawnData); + } + //public NetworkObject InstantiateAndSpawnExtended(int handlerId, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + //{ + // ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; + // // We only need to check for authority when running in client-server mode + // if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) + // { + // Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); + // return null; + // } + + // if (NetworkManager.ShutdownInProgress) + // { + // Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); + // return null; + // } + + // return InstantiateAndSpawnNoParameterChecksExtended(handlerId, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); + //} + + /// + /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! + /// + internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default, byte[] customSpanData = null) + { + var networkObject = networkPrefab; + // - Host and clients always instantiate the override if one exists. + // - Server instantiates the original prefab unless: + // -- forceOverride is set to true =or= + // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. + // - Distributed authority mode always spawns the override if one exists. + if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) + { + networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation, customSpawnData: customSpanData); + } + else // Under this case, server instantiate the prefab passed in. + { + networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation); + } + + if (networkObject == null) + { + Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); + return null; + } + networkObject.IsPlayerObject = isPlayerObject; + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + // If spawning as a player, then invoke SpawnAsPlayerObject + if (isPlayerObject) + { + networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene, customSpanData); + } + else // Otherwise just spawn with ownership + { + networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene, customSpanData); + } + return networkObject; + } + //internal NetworkObject InstantiateAndSpawnNoParameterChecksExtended(int handlerId, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + //{ + // var networkObject = InstantiateNetworkPrefabExtended(handlerId, position, rotation); + // // - Host and clients always instantiate the override if one exists. + // // - Server instantiates the original prefab unless: + // // -- forceOverride is set to true =or= + // // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. + // // - Distributed authority mode always spawns the override if one exists. + // networkObject = GetNetworkObjectToSpawnExtended(handlerId, ownerClientId, position, rotation); + + // if (networkObject == null) + // { + // Debug.LogError($"Failed to instantiate and spawn Extended for Handler with id: {handlerId}!"); + // return null; + // } + // networkObject.IsPlayerObject = isPlayerObject; + // networkObject.transform.position = position; + // networkObject.transform.rotation = rotation; + + // networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); + + // return networkObject; + //} + + /// + /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the + /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. + /// + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false, byte[] customSpawnData = null) + { + NetworkObject networkObject = null; + // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class + if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) + { + // Let the handler spawn the NetworkObject + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default, customSpawnData); + networkObject.NetworkManagerOwner = NetworkManager; + } + else + { + // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash + var networkPrefabReference = (GameObject)null; + var inScenePlacedWithNoSceneManagement = !NetworkManager.NetworkConfig.EnableSceneManagement && isScenePlaced; + + if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) + { + var networkPrefab = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash]; + + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + networkPrefabReference = networkPrefab.Prefab; + break; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + { + // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the + // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically + // but might want to use the same source network prefab as an in-scene placed NetworkObject. + // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically + // spawning them so the original prefab placed is preserved and this is not needed) + if (inScenePlacedWithNoSceneManagement) + { + networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; + } + else + { + networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; + } + break; + } + } + } + + // If not, then there is an issue (user possibly didn't register the prefab properly?) + if (networkPrefabReference == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); + } + } + else + { + // Create prefab instance while applying any pre-assigned position and rotation values + networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation); + } + } + return networkObject; + } + + //internal NetworkObject GetNetworkObjectToSpawnExtended(int handlerId, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) + //{ + // NetworkObject networkObject = null; + // // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class + // if (customHandlers.ContainsKey(handlerId)) + // { + // // Let the handler spawn the NetworkObject + // networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawnExtended(handlerId, ownerId, position ?? default, rotation ?? default); + // networkObject.NetworkManagerOwner = NetworkManager; + // } + // else + // { + // throw new Exception($"No handler registered with id: {handlerId}"); + // } + // return networkObject; + //} + + /// + /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients + /// the instance. + /// !!! Should only be invoked by unless used by an integration test !!! + /// + /// + /// should be the base prefab value and not the + /// overrided value. + /// (Can be used for integration testing) + /// + /// prefab to instantiate + /// of the base prefab instance + /// conditional position in place of the network prefab's default position + /// conditional rotation in place of the network prefab's default rotation + /// the instance of the + internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) + { + var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); + networkObject.transform.position = position ?? networkObject.transform.position; + networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; + networkObject.NetworkManagerOwner = NetworkManager; + networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; + return networkObject; + } + public static Dictionary customHandlers = new(); + public static Dictionary customPrefabInstanceToHandlers = new(); + //internal NetworkObject InstantiateNetworkPrefabExtended(int handlerId, Vector3? position, Quaternion? rotation) + //{ + + // //TODO: ACCESS NETWORK PREFAB HANDLER + + // var networkObject = customHandlers[handlerId].Instantiate(0, Vector3.zero, Quaternion.identity); + // networkObject.transform.position = position ?? networkObject.transform.position; + // networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; + // networkObject.NetworkManagerOwner = NetworkManager; + // //networkObject.PrefabGlobalObjectIdHash = 0; + // return networkObject; + //} + + /// + /// Creates a local NetowrkObject to be spawned. + /// + /// + /// For most cases this is client-side only, with the exception of when the server + /// is spawning a player. + /// + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject, byte[] customSpawnData) + { + NetworkObject networkObject = null; + var globalObjectIdHash = sceneObject.Hash; + var position = sceneObject.HasTransform ? sceneObject.Transform.Position : default; + var rotation = sceneObject.HasTransform ? sceneObject.Transform.Rotation : default; + var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default; + var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default; + var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays; + + // If scene management is disabled or the NetworkObject was dynamically spawned + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) + { + networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject, customSpawnData); + } + else // Get the in-scene placed NetworkObject + { + networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); + } + } + + // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so + // NetworkBehaviours will have their OnNetworkSpawn method invoked + if (networkObject != null && !networkObject.gameObject.activeInHierarchy) + { + networkObject.gameObject.SetActive(true); + } + } + + if (networkObject != null) + { + networkObject.DestroyWithScene = sceneObject.DestroyWithScene; + networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner; + networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags; + + var nonNetworkObjectParent = false; + // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) + // This is a special case scenario where a late joining client has joined and loaded one or + // more scenes that contain nested in-scene placed NetworkObject children yet the server's + // synchronization information does not indicate the NetworkObject in question has a parent =or= + // the parent has changed. + // For this we will want to remove the parent before spawning and setting the transform values based + // on several possible scenarios. + if (sceneObject.IsSceneObject && networkObject.transform.parent != null) + { + var parentNetworkObject = networkObject.transform.parent.GetComponent(); + + // special case to handle being parented under a GameObject with no NetworkObject + nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent; + + // If the in-scene placed NetworkObject has a parent NetworkObject... + if (parentNetworkObject) + { + // Then remove the parent only if: + // - The authority says we don't have a parent (but locally we do). + // - The auhtority says we have a parent but either of the two are true: + // -- It isn't the same parent. + // -- It was parented using world position stays. + if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet + && (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays))) + { + // If parenting without notifications then we are temporarily removing the parent to set the transform + // values before reparenting under the current parent. + networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent); + } + } + } + + // Set the transform only if the sceneObject includes transform information. + if (sceneObject.HasTransform) + { + // If world position stays is true or we have auto object parent synchronization disabled + // then we want to apply the position and rotation values world space relative + if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) + { + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + } + else + { + networkObject.transform.localPosition = position; + networkObject.transform.localRotation = rotation; + } + + // SPECIAL CASE: + // Since players are created uniquely we don't apply scale because + // the ConnectionApprovalResponse does not currently provide the + // ability to specify scale. So, we just use the default scale of + // the network prefab used to represent the player. + // Note: not doing this would set the player's scale to zero since + // that is the default value of Vector3. + if (!sceneObject.IsPlayerObject) + { + // Since scale is always applied to local space scale, we do the transform + // space logic during serialization such that it works out whether AutoObjectParentSync + // is enabled or not (see NetworkObject.SceneObject) + networkObject.transform.localScale = scale; + } + } + + if (sceneObject.HasParent) + { + // Go ahead and set network parenting properties, if the latest parent is not set then pass in null + // (we always want to set worldPositionStays) + ulong? parentId = null; + if (sceneObject.IsLatestParentSet) + { + parentId = parentNetworkId; + } + networkObject.SetNetworkParenting(parentId, worldPositionStays); + } + + // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL + // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. + if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } + } + return networkObject; + } + + /// + /// Invoked from: + /// - ConnectionManager after instantiating a player prefab when running in client-server. + /// - NetworkObject when spawning a newly instantiated NetworkObject for the first time. + /// - NetworkSceneManager after a server/session-owner has loaded a scene to locally spawn the newly instantiated in-scene placed NetworkObjects. + /// - NetworkSpawnManager when spawning any already loaded in-scene placed NetworkObjects (client-server or session owner). + /// + /// Client-Server: + /// Server is the only instance that invokes this method. + /// + /// Distributed Authority: + /// DAHost client and standard DA clients invoke this method. + /// + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + Debug.LogError($"{networkObject.name} is already spawned!"); + return; + } + + if (!sceneObject) + { + var networkObjectChildren = networkObject.GetComponentsInChildren(); + if (networkObjectChildren.Length > 1) + { + Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); + } + } + // Invoke NetworkBehaviour.OnPreSpawn methods + networkObject.InvokeBehaviourNetworkPreSpawn(); + + // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning + // For now, this is the best place I could find to add all connected clients as observers for newly + // instantiated and spawned NetworkObjects on the authoritative side. + if (NetworkManager.DistributedAuthorityMode) + { + if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) + { + networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; + networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; + } + + // Always add the owner/authority even if SpawnWithObservers is false + // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) + if (!networkObject.SpawnWithObservers) + { + networkObject.Observers.Add(ownerClientId); + } + else + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration + if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) + { + continue; + } + networkObject.Observers.Add(clientId); + } + + // Sanity check to make sure the owner is always included + // Itentionally checking as opposed to just assigning in order to generate notification. + if (!networkObject.Observers.Contains(ownerClientId)) + { + Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); + networkObject.Observers.Add(ownerClientId); + } + } + } + SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + + // Invoke NetworkBehaviour.OnPostSpawn methods + networkObject.InvokeBehaviourNetworkPostSpawn(); + } + //internal void SpawnNetworkObjectLocallyExtended(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + //{ + // if (networkObject == null) + // { + // throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + // } + + // if (networkObject.IsSpawned) + // { + // Debug.LogError($"{networkObject.name} is already spawned!"); + // return; + // } + + // if (!sceneObject) + // { + // var networkObjectChildren = networkObject.GetComponentsInChildren(); + // if (networkObjectChildren.Length > 1) + // { + // Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); + // } + // } + // // Invoke NetworkBehaviour.OnPreSpawn methods + // networkObject.InvokeBehaviourNetworkPreSpawn(); + + // // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning + // // For now, this is the best place I could find to add all connected clients as observers for newly + // // instantiated and spawned NetworkObjects on the authoritative side. + // if (NetworkManager.DistributedAuthorityMode) + // { + // if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) + // { + // networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; + // networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; + // } + + // // Always add the owner/authority even if SpawnWithObservers is false + // // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) + // if (!networkObject.SpawnWithObservers) + // { + // networkObject.Observers.Add(ownerClientId); + // } + // else + // { + // foreach (var clientId in NetworkManager.ConnectedClientsIds) + // { + // // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration + // if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) + // { + // continue; + // } + // networkObject.Observers.Add(clientId); + // } + + // // Sanity check to make sure the owner is always included + // // Itentionally checking as opposed to just assigning in order to generate notification. + // if (!networkObject.Observers.Contains(ownerClientId)) + // { + // Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); + // networkObject.Observers.Add(ownerClientId); + // } + // } + // } + // SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + + // // Invoke NetworkBehaviour.OnPostSpawn methods + // networkObject.InvokeBehaviourNetworkPostSpawn(); + //} + + /// + /// This is only invoked to instantiate a serialized NetworkObject via + /// + /// + /// + /// IMPORTANT: Pre spawn methods need to be invoked from within . + /// + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); + } + + // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) + SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene); + + // It is ok to invoke NetworkBehaviour.OnPostSpawn methods + networkObject.InvokeBehaviourNetworkPostSpawn(); + } + + private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + { + if (SpawnedObjects.ContainsKey(networkId)) + { + Debug.LogWarning($"[{NetworkManager.name}] Trying to spawn {networkObject.name} with a {nameof(NetworkObject.NetworkObjectId)} of {networkId} but it is already in the spawned list!"); + return; + } + + networkObject.IsSpawned = true; + networkObject.IsSceneObject = sceneObject; + + // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects + // Note: Always check SceneOriginHandle directly at this specific location. + if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0) + { + networkObject.SceneOrigin = networkObject.gameObject.scene; + } + + // For integration testing, this makes sure that the appropriate NetworkManager is assigned to + // the NetworkObject since it uses the NetworkManager.Singleton when not set + if (networkObject.NetworkManagerOwner != NetworkManager) + { + networkObject.NetworkManagerOwner = NetworkManager; + } + + networkObject.NetworkObjectId = networkId; + + networkObject.DestroyWithScene = sceneObject || destroyWithScene; + + networkObject.IsPlayerObject = playerObject; + + networkObject.OwnerClientId = ownerClientId; + + // When spawned, previous owner is always the first assigned owner + networkObject.PreviousOwnerId = ownerClientId; + + // If this the player and the client is the owner, then lock ownership by default + if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) + { + networkObject.SetOwnershipLock(); + } + SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); + SpawnedObjectsList.Add(networkObject); + + // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, + // then add all connected clients as observers + if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) + { + // If running as a server only, then make sure to always add the server's client identifier + if (!NetworkManager.IsHost) + { + networkObject.Observers.Add(NetworkManager.LocalClientId); + } + + // Add client observers + for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) + { + // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. + if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) + { + continue; + } + networkObject.Observers.Add(NetworkManager.ConnectedClientsIds[i]); + } + } + + networkObject.ApplyNetworkParenting(); + NetworkObject.CheckOrphanChildren(); + + AddNetworkObjectToSceneChangedUpdates(networkObject); + + networkObject.InvokeBehaviourNetworkSpawn(); + + NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId); + + // propagate the IsSceneObject setting to child NetworkObjects + var children = networkObject.GetComponentsInChildren(); + foreach (var childObject in children) + { + // Do not propagate the in-scene object setting if a child was dynamically spawned. + if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value) + { + continue; + } + childObject.IsSceneObject = sceneObject; + } + + // Only dynamically spawned NetworkObjects are allowed + if (!sceneObject) + { + networkObject.SubscribeToActiveSceneForSynch(); + } + + if (networkObject.IsPlayerObject) + { + UpdateNetworkClientPlayer(networkObject); + } + + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) + { + networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; + } + } + + internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); + + // Pre-allocating to avoid the initial constructor hit + internal Stack CleanUpDisposedObjects = new Stack(); + + internal void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject) + { + if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && + !NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) + { + if (networkObject.UpdateForSceneChanges()) + { + NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject); + } + } + } + + internal void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject) + { + if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && + NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) + { + NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId); + } + } + + internal unsafe void UpdateNetworkObjectSceneChanges() + { + foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) + { + // If it fails the first update then don't add for updates + if (!entry.Value.UpdateForSceneChanges()) + { + CleanUpDisposedObjects.Push(entry.Key); + } + } + + // Clean up any NetworkObjects that no longer exist (destroyed before they should be or the like) + while (CleanUpDisposedObjects.Count > 0) + { + NetworkObjectsToSynchronizeSceneChanges.Remove(CleanUpDisposedObjects.Pop()); + } + } + + internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject, byte[] customSpawnData) + { + // If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message. + var updateObservers = NetworkManager.DistributedAuthorityMode && networkObject.SpawnWithObservers; + + // Only skip if distributed authority mode is not enabled + if (clientId == NetworkManager.ServerClientId && !NetworkManager.DistributedAuthorityMode) + { + return; + } + + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(clientId, NetworkManager.DistributedAuthorityMode), + IncludesSerializedObject = true, + UpdateObservers = NetworkManager.DistributedAuthorityMode, + ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, + CustomSpawnData = customSpawnData, + HasCustomSpawnData = customSpawnData != null && customSpawnData.Length > 0, + }; + Debug.Log("customSpawnData:2 " + (customSpawnData?.Length ?? 0).ToString()); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); + Debug.Log("s: " + size); + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + } + + /// + /// Only used to update object visibility/observers. + /// ** Clients are the only instances that use this method ** + /// + internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject networkObject) + { + if (!NetworkManager.DistributedAuthorityMode) + { + throw new Exception("[SendSpawnCallForObserverUpdate] Invoking a distributed authority only method when distributed authority is not enabled!"); + } + + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(), + ObserverIds = networkObject.Observers.ToArray(), + NewObserverIds = newObservers.ToArray(), + IncludesSerializedObject = true, + UpdateObservers = true, + UpdateNewObservers = true, + }; + Debug.Log("customSpawnData:2 SendSpawnCallForObserverUpdate" + message.CustomSpawnData.Length); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); + foreach (var clientId in newObservers) + { + // TODO: We might want to track observer update sent as well? + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + } + } + + internal ulong? GetSpawnParentId(NetworkObject networkObject) + { + NetworkObject parentNetworkObject = null; + + if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) + { + parentNetworkObject = networkObject.transform.parent.GetComponent(); + } + + if (parentNetworkObject == null) + { + return null; + } + + return parentNetworkObject.NetworkObjectId; + } + + internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false, bool playerDisconnect = false) + { + if (!networkObject.IsSpawned) + { + NetworkLog.LogErrorServer("Object is not spawned!"); + return; + } + + if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) + { + NetworkLog.LogErrorServer("Only server can despawn objects"); + return; + } + + if (NetworkManager.DistributedAuthorityMode && networkObject.OwnerClientId != NetworkManager.LocalClientId) + { + if (!NetworkManager.DAHost || NetworkManager.DAHost && !playerDisconnect) + { + NetworkLog.LogErrorServer($"In distributed authority mode, only the owner of the NetworkObject can despawn it! Local Client is ({NetworkManager.LocalClientId}) while the owner is ({networkObject.OwnerClientId})"); + return; + } + } + OnDespawnObject(networkObject, destroyObject, playerDisconnect); + } + + // Makes scene objects ready to be reused + internal void ServerResetShudownStateForSceneObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); #else var networkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); #endif - foreach (var sobj in networkObjects) - { - sobj.IsSpawned = false; - sobj.DestroyWithScene = false; - sobj.IsSceneObject = null; - } - } - - /// - /// Gets called only by NetworkSceneManager.SwitchScene - /// - internal void ServerDestroySpawnedSceneObjects() - { - // This Allocation is "OK" for now because this code only executes when a new scene is switched to - // We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove - // objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur. - var spawnedObjects = SpawnedObjectsList.ToList(); - - foreach (var sobj in spawnedObjects) - { - if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) - { - SpawnedObjectsList.Remove(sobj); - UnityEngine.Object.Destroy(sobj.gameObject); - } - } - } - - internal void DespawnAndDestroyNetworkObjects() - { + foreach (var sobj in networkObjects) + { + sobj.IsSpawned = false; + sobj.DestroyWithScene = false; + sobj.IsSceneObject = null; + } + } + + /// + /// Gets called only by NetworkSceneManager.SwitchScene + /// + internal void ServerDestroySpawnedSceneObjects() + { + // This Allocation is "OK" for now because this code only executes when a new scene is switched to + // We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove + // objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur. + var spawnedObjects = SpawnedObjectsList.ToList(); + + foreach (var sobj in spawnedObjects) + { + if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) + { + SpawnedObjectsList.Remove(sobj); + UnityEngine.Object.Destroy(sobj.gameObject); + } + } + } + + internal void DespawnAndDestroyNetworkObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) - { - OnDespawnObject(networkObjects[i], false); - // Leave destruction up to the handler - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - } - else - { - // If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene - // is unloaded. Otherwise, despawn and destroy it. - var shouldDestroy = !(networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value)); - - // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed - if (shouldDestroy) - { - // Check to see if there are any in-scene placed children that are marked to be destroyed with the scene - var childrenObjects = networkObjects[i].GetComponentsInChildren(); - foreach (var childObject in childrenObjects) - { - if (childObject == networkObjects[i]) - { - continue; - } - - // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) - // and set its parent to root - if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) - { - childObject.TryRemoveParent(childObject.WorldPositionStays()); - } - } - } - - // If spawned, then despawn and potentially destroy. - if (networkObjects[i].IsSpawned) - { - OnDespawnObject(networkObjects[i], shouldDestroy); - } - else // Otherwise, if we are not spawned and we should destroy...then destroy. - if (shouldDestroy) - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); - } - } - } - } - } - - internal void DestroySceneObjects() - { + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + OnDespawnObject(networkObjects[i], false); + // Leave destruction up to the handler + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + } + else + { + // If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene + // is unloaded. Otherwise, despawn and destroy it. + var shouldDestroy = !(networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value)); + + // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed + if (shouldDestroy) + { + // Check to see if there are any in-scene placed children that are marked to be destroyed with the scene + var childrenObjects = networkObjects[i].GetComponentsInChildren(); + foreach (var childObject in childrenObjects) + { + if (childObject == networkObjects[i]) + { + continue; + } + + // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) + // and set its parent to root + if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) + { + childObject.TryRemoveParent(childObject.WorldPositionStays()); + } + } + } + + // If spawned, then despawn and potentially destroy. + if (networkObjects[i].IsSpawned) + { + OnDespawnObject(networkObjects[i], shouldDestroy); + } + else // Otherwise, if we are not spawned and we should destroy...then destroy. + if (shouldDestroy) + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void DestroySceneObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) - { - if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) - { - // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. - OnDespawnObject(networkObjects[i], false); - } - else // If not spawned, then just invoke the handler - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - } - } - else - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); - } - } - } - } - } - - internal void ServerSpawnSceneObjectsOnStartSweep() - { + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) + { + // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. + OnDespawnObject(networkObjects[i], false); + } + else // If not spawned, then just invoke the handler + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + } + } + else + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void ServerSpawnSceneObjectsOnStartSweep() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - var networkObjectsToSpawn = new List(); - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - // This used to be two loops. - // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. - // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. - if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) - { - var ownerId = networkObjects[i].OwnerClientId; - if (NetworkManager.DistributedAuthorityMode) - { - ownerId = NetworkManager.LocalClientId; - } - - SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); - networkObjectsToSpawn.Add(networkObjects[i]); - } - } - } - - // Since we are spawing in-scene placed NetworkObjects for already loaded scenes, - // we need to add any in-scene placed NetworkObject to our tracking table - var clearFirst = true; - foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded) - { - NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst); - clearFirst = false; - } - - // Notify all in-scene placed NetworkObjects have been spawned - foreach (var networkObject in networkObjectsToSpawn) - { - networkObject.InternalInSceneNetworkObjectsSpawned(); - } - networkObjectsToSpawn.Clear(); - } - - /// - /// Called when destroying an object after receiving a . - /// Processes logic for how to destroy objects on the non-authority client. - /// - internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject) - { - if (networkObject.HasAuthority) - { - NetworkLog.LogError($"OnDespawnNonAuthorityObject called on object {networkObject.NetworkObjectId} when is current client {NetworkManager.LocalClientId} has authority on this object."); - } - - // On the non-authority, never destroy the game object when InScenePlaced, otherwise always destroy on non-authority side - OnDespawnObject(networkObject, networkObject.IsSceneObject == false); - } - - internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject, bool modeDestroy = false) - { - if (!NetworkManager) - { - return; - } - - // We have to do this check first as subsequent checks assume we can access NetworkObjectId. - if (!networkObject) - { - Debug.LogWarning($"Trying to destroy network object but it is null"); - return; - } - - // Removal of spawned object - if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId)) - { - if (!NetworkManager.ShutdownInProgress) - { - Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!"); - } - return; - } - - // If we are shutting down the NetworkManager, then ignore resetting the parent - // and only attempt to remove the child's parent on the server-side - var distributedAuthority = NetworkManager.DistributedAuthorityMode; - if (!NetworkManager.ShutdownInProgress && (NetworkManager.IsServer || distributedAuthority)) - { - // Get all child NetworkObjects - var objectsToRemoveParent = networkObject.GetComponentsInChildren(); - - // Move child NetworkObjects to the root when parent NetworkObject is destroyed - foreach (var spawnedNetObj in objectsToRemoveParent) - { - if (spawnedNetObj == networkObject) - { - continue; - } - var latestParent = spawnedNetObj.GetNetworkParenting(); - // Only deparent the first generation children of the NetworkObject being spawned. - // Ignore any nested children under first generation children. - if (latestParent.HasValue && latestParent.Value != networkObject.NetworkObjectId) - { - continue; - } - // For mixed authority hierarchies, if the parent is despawned then any removal of children - // is considered "authority approved". If we don't have authority over the object and we are - // in distributed authority mode, then set the AuthorityAppliedParenting flag. - spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !spawnedNetObj.HasAuthority; - - // Try to remove the parent using the cached WorldPositionStays value - // Note: WorldPositionStays will still default to true if this was an - // in-scene placed NetworkObject and parenting was predefined in the - // scene via the editor. - if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays()) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed"); - } - } - else - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) - { - NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); - } - } - } - - networkObject.InvokeBehaviourNetworkDespawn(); - - if (NetworkManager != null && ((NetworkManager.IsServer && (!distributedAuthority || - (distributedAuthority && modeDestroy))) || - (distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId))) - { - if (NetworkManager.NetworkConfig.RecycleNetworkIds) - { - ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() - { - NetworkId = networkObject.NetworkObjectId, - ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime - }); - } - m_TargetClientIds.Clear(); - - // If clients are not allowed to spawn locally then go ahead and send the despawn message or if we are in distributed authority mode, we are the server, we own this NetworkObject - // send the despawn message, and as long as we have any remaining clients, then notify of the object being destroy. - if (NetworkManager.IsServer && NetworkManager.ConnectedClientsList.Count > 0 && (!distributedAuthority || - (NetworkManager.DAHost && distributedAuthority && - (networkObject.OwnerClientId == NetworkManager.LocalClientId || modeDestroy)))) - { - // We keep only the client for which the object is visible - // as the other clients have them already despawned - foreach (var clientId in NetworkManager.ConnectedClientsIds) - { - if ((distributedAuthority && clientId == networkObject.OwnerClientId) || clientId == NetworkManager.LocalClientId) - { - continue; - } - if (networkObject.IsNetworkVisibleTo(clientId)) - { - m_TargetClientIds.Add(clientId); - } - } - } - else // DANGO-TODO: If we are not the server, distributed authority mode is enabled, and we are the owner then inform the DAHost to despawn the NetworkObject - if (!NetworkManager.IsServer && distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId) - { - // DANGO-TODO: If a shutdown is not in progress or a shutdown is in progress and we can destroy with the owner then notify the DAHost - if (!NetworkManager.ShutdownInProgress || (NetworkManager.ShutdownInProgress && !networkObject.DontDestroyWithOwner)) - { - m_TargetClientIds.Add(NetworkManager.ServerClientId); - } - } - - if (m_TargetClientIds.Count > 0 && !NetworkManager.ShutdownInProgress) - { - var message = new DestroyObjectMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - DeferredDespawnTick = networkObject.DeferredDespawnTick, - DestroyGameObject = destroyGameObject, - IsTargetedDestroy = false, - IsDistributedAuthority = distributedAuthority, - }; - foreach (var clientId in m_TargetClientIds) - { - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, networkObject, size); - } - } - } - - networkObject.IsSpawned = false; - networkObject.DeferredDespawnTick = 0; - - if (SpawnedObjects.Remove(networkObject.NetworkObjectId)) - { - SpawnedObjectsList.Remove(networkObject); - } - - if (networkObject.IsPlayerObject) - { - RemovePlayerObject(networkObject, destroyGameObject); - } - - // Always clear out the observers list when despawned - networkObject.Observers.Clear(); - - var gobj = networkObject.gameObject; - if (destroyGameObject && gobj != null) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); - } - else - { - UnityEngine.Object.Destroy(gobj); - } - } - } - - /// - /// Updates all spawned for the specified newly connected client - /// Note: if the clientId is the server then it is observable to all spawned 's - /// - /// - /// This method is to only to be used for newly connected clients in order to update the observers list for - /// each NetworkObject instance. - /// - internal void UpdateObservedNetworkObjects(ulong clientId) - { - foreach (var sobj in SpawnedObjectsList) - { - // If the NetworkObject has no visibility check then prepare to add this client as an observer - if (sobj.CheckObjectVisibility == null) - { - // If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server/host/DAHost - if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId)) - { - sobj.Observers.Add(clientId); - } - } - else - { - // CheckObject visibility overrides SpawnWithObservers under this condition - if (sobj.CheckObjectVisibility(clientId)) - { - sobj.Observers.Add(clientId); - } - else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false - { - sobj.Observers.Remove(clientId); - } - } - } - } - - /// - /// See - /// - internal void HandleNetworkObjectShow() - { - // In distributed authority mode, we send a single message that is broadcasted to all clients - // that will be shown the object (i.e. 1 message to service that then broadcasts that to the - // targeted clients). When using a DAHost, we skip this and send like we do in client-server - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) - { - foreach (var entry in ClientsToShowObject) - { - if (entry.Key != null && entry.Key.IsSpawned) - { - try - { - SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); - } - catch (Exception ex) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogException(ex); - } - } - } - } - ClientsToShowObject.Clear(); - ObjectsToShowToClient.Clear(); - return; - } - - // Server or Host handling of NetworkObjects to show - foreach (var client in ObjectsToShowToClient) - { - ulong clientId = client.Key; - foreach (var networkObject in client.Value) - { - if (networkObject != null && networkObject.IsSpawned) - { - try - { - SendSpawnCallForObject(clientId, networkObject); - } - catch (Exception ex) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogException(ex); - } - } - } - } - } - ObjectsToShowToClient.Clear(); - } - - internal NetworkSpawnManager(NetworkManager networkManager) - { - NetworkManager = networkManager; - } - - /// - /// Finalizer that ensures proper cleanup of spawn manager resources - /// - ~NetworkSpawnManager() - { - Shutdown(); - } - - internal void Shutdown() - { - NetworkObjectsToSynchronizeSceneChanges.Clear(); - CleanUpDisposedObjects.Clear(); - } - - /// - /// DANGO-TODO: Until we have the CMB Server end-to-end with all features verified working via integration tests, - /// I am keeping this debug toggle available. (NSS) - /// - internal bool EnableDistributeLogging = false; - - /// - /// Fills the first table passed in with the current distribution of prefab types relative to their owners - /// Fills the second table passed in with the total number of spawned objects of that particular type. - /// The second table allows us to calculate how many objects per client there should be in order to determine - /// how many of that type should be distributed. - /// - /// the table to populate - /// the total number of the specific object type to distribute - internal void GetObjectDistribution(ulong clientId, ref Dictionary>> objectByTypeAndOwner, ref Dictionary objectTypeCount) - { - // DANGO-TODO-MVP: Remove this once the service handles object distribution - var onlyIncludeOwnedObjects = NetworkManager.CMBServiceConnection; - - foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList) - { - if (networkObject.IsOwnershipSessionOwner) - { - continue; - } - - // We first check for a parent and then if the parent is a NetworkObject - if (networkObject.transform.parent != null) - { - // If the parent is a NetworkObject, has the same owner, and this NetworkObject has the distributable =or= transferable permission - // then skip this child (NetworkObjects are always parented directly under other NetworkObjects) - // (later we determine if all children with the same owner will be transferred together as a group) - var parentNetworkObject = networkObject.transform.parent.GetComponent(); - if (parentNetworkObject != null && parentNetworkObject.OwnerClientId == networkObject.OwnerClientId - && (networkObject.IsOwnershipDistributable || networkObject.IsOwnershipTransferable)) - { - continue; - } - } - - // At this point we only allow things marked with the distributable permission and is not locked to be distributed - if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked) - { - // Don't include anything that is not visible to the new client - if (!networkObject.Observers.Contains(clientId)) - { - continue; - } - - // We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance - // since all in-scene placed instances use unique GlobalObjectIdHash values. - var globalOjectIdHash = networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value ? networkObject.InScenePlacedSourceGlobalObjectIdHash : networkObject.GlobalObjectIdHash; - - if (!objectTypeCount.ContainsKey(globalOjectIdHash)) - { - objectTypeCount.Add(globalOjectIdHash, 0); - } - objectTypeCount[globalOjectIdHash] += 1; - - // DANGO-TODO-MVP: Remove this once the service handles object distribution - if (onlyIncludeOwnedObjects && !networkObject.IsOwner) - { - continue; - } - - - // Divide up by prefab type (GlobalObjectIdHash) to get a better distribution of object types - if (!objectByTypeAndOwner.ContainsKey(globalOjectIdHash)) - { - objectByTypeAndOwner.Add(globalOjectIdHash, new Dictionary>()); - } - - // Sub-divide each type by owner - if (!objectByTypeAndOwner[globalOjectIdHash].ContainsKey(networkObject.OwnerClientId)) - { - objectByTypeAndOwner[globalOjectIdHash].Add(networkObject.OwnerClientId, new List()); - } - - // Add to the client's spawned object list - objectByTypeAndOwner[globalOjectIdHash][networkObject.OwnerClientId].Add(networkObject); - } - } - } - - /// - /// Distributes an even portion of spawned NetworkObjects to the given client. - /// This is called as part of the client connection process to ensure that all clients have a fair share of the spawned NetworkObjects. - /// DANGO-TODO: This will be handled by the CMB Service in the future. - /// - /// Client to distribute NetworkObjects to - internal void DistributeNetworkObjects(ulong clientId) - { - if (!NetworkManager.DistributedAuthorityMode) - { - return; - } - - if (NetworkManager.SessionConfig.ServiceSideDistribution) - { - return; - } - - // DA-NGO CMB SERVICE NOTES: - // The most basic object distribution should be broken up into a table of spawned object types - // where each type contains a list of each client's owned objects of that type that can be - // distributed. - // The table format: - // [GlobalObjectIdHashValue][ClientId][List of Owned Objects] - var distributedNetworkObjects = new Dictionary>>(); - - // DA-NGO CMB SERVICE NOTES: - // This is optional, but I found it easier to get the total count of spawned objects for each prefab - // type contained in the previous table in order to be able to calculate the targeted object distribution - // count of that type per client. - var objectTypeCount = new Dictionary(); - - // Get all spawned objects by type and then by client owner that are spawned and can be distributed - GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); - - var clientCount = NetworkManager.ConnectedClientsIds.Count; - - // Cycle through each prefab type - foreach (var objectTypeEntry in distributedNetworkObjects) - { - // Calculate the number of objects that should be distributed amongst the clients - var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key]; - var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount); - var floorValue = (int)Math.Floor(objPerClientF); - var fractional = objPerClientF - floorValue; - var objPerClient = 0; - if (fractional >= 0.556f) - { - objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount)); - } - else - { - objPerClient = floorValue; - } - - // If the object per client count is zero, then move to the next type. - if (objPerClient <= 0) - { - continue; - } - - // Evenly distribute this object type amongst the clients - foreach (var ownerList in objectTypeEntry.Value) - { - if (ownerList.Value.Count <= 1) - { - continue; - } - - var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1); - var distributed = 0; - // For now when we have more players then distributed NetworkObjects that - // a specific client owns, just assign half of the NetworkObjects to the new client - var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1); - if (EnableDistributeLogging) - { - Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}"); - } - - for (int i = 0; i < ownerList.Value.Count; i++) - { - if ((i % offsetCount) == 0) - { - var children = ownerList.Value[i].GetComponentsInChildren(); - // Since the ownerList.Value[i] has to be distributable, then transfer all child NetworkObjects - // with the same owner clientId and are marked as distributable also to the same client to keep - // the owned distributable parent with the owned distributable children - foreach (var child in children) - { - // Ignore any child that does not have the same owner, that is already owned by the currently targeted client, or that doesn't have the targeted client as an observer - if (child == ownerList.Value[i] || child.OwnerClientId != ownerList.Value[i].OwnerClientId || child.OwnerClientId == clientId || !child.Observers.Contains(clientId)) - { - continue; - } - if (!child.IsOwnershipDistributable || !child.IsOwnershipTransferable) - { - NetworkLog.LogWarning($"Sibling {child.name} of root parent {ownerList.Value[i].name} is neither transferable or distributable! Object distribution skipped and could lead to a potentially un-owned or owner-mismatched {nameof(NetworkObject)}!"); - continue; - } - // Transfer ownership of all distributable =or= transferable children with the same owner to the same client to preserve the sibling ownership tree. - ChangeOwnership(child, clientId, true); - // Note: We don't increment the distributed count for these children as they are skipped when getting the object distribution - } - // Finally, transfer ownership of the root parent - ChangeOwnership(ownerList.Value[i], clientId, true); - if (EnableDistributeLogging) - { - Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}"); - } - distributed++; - } - if (distributed == maxDistributeCount) - { - break; - } - } - } - } - - // If EnableDistributeLogging is enabled, log the object type distribution counts per client - if (EnableDistributeLogging) - { - var builder = new StringBuilder(); - distributedNetworkObjects.Clear(); - objectTypeCount.Clear(); - GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); - builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)"); - // Cycle through each prefab type - foreach (var objectTypeEntry in distributedNetworkObjects) - { - builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]"); - builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:"); - // Evenly distribute this type amongst clients - foreach (var ownerList in objectTypeEntry.Value) - { - builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}"); - } - } - Debug.Log(builder.ToString()); - } - } - - internal struct DeferredDespawnObject - { - public int TickToDespawn; - public bool HasDeferredDespawnCheck; - public ulong NetworkObjectId; - } - - internal List DeferredDespawnObjects = new List(); - - /// - /// Adds a deferred despawn entry to be processed - /// - /// associated NetworkObject - /// when to despawn the NetworkObject - /// if true, user script is to be invoked to determine when to despawn - internal void DeferDespawnNetworkObject(ulong networkObjectId, int tickToDespawn, bool hasDeferredDespawnCheck) - { - var deferredDespawnObject = new DeferredDespawnObject() - { - TickToDespawn = tickToDespawn, - HasDeferredDespawnCheck = hasDeferredDespawnCheck, - NetworkObjectId = networkObjectId, - }; - DeferredDespawnObjects.Add(deferredDespawnObject); - } - - /// - /// Processes any deferred despawn entries - /// - internal void DeferredDespawnUpdate(NetworkTime serverTime) - { - // Exit early if there is nothing to process - if (DeferredDespawnObjects.Count == 0) - { - return; - } - var currentTick = serverTime.Tick; - var deferredDespawnCount = DeferredDespawnObjects.Count; - // Loop forward and to process user callbacks and update despawn ticks - for (int i = 0; i < deferredDespawnCount; i++) - { - var deferredObjectEntry = DeferredDespawnObjects[i]; - if (!deferredObjectEntry.HasDeferredDespawnCheck) - { - continue; - } - - if (!SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) - { - continue; - } - - // Double check to make sure user did not remove the callback - if (networkObject.OnDeferredDespawnComplete != null) - { - // If the user callback returns true, then we despawn it this tick - if (networkObject.OnDeferredDespawnComplete.Invoke()) - { - deferredObjectEntry.TickToDespawn = currentTick; - } - else - { - // If the user callback does not verify the NetworkObject can be despawned, - // continue setting this value in the event user script adjusts it. - deferredObjectEntry.TickToDespawn = networkObject.DeferredDespawnTick; - } - } - else - { - // If it was removed, then in case it is being left to defer naturally exclude it - // from the next query. - deferredObjectEntry.HasDeferredDespawnCheck = false; - } - } - - // Parse backwards so we can remove objects as we parse through them - for (int i = deferredDespawnCount - 1; i >= 0; i--) - { - var deferredObjectEntry = DeferredDespawnObjects[i]; - if (deferredObjectEntry.TickToDespawn >= currentTick) - { - continue; - } - - if (SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) - { - // Local instance despawns the instance - OnDespawnNonAuthorityObject(networkObject); - } - - DeferredDespawnObjects.RemoveAt(i); - } - } - - internal void NotifyNetworkObjectsSynchronized() - { - // Users could spawn NetworkObjects during these notifications. - // Create a separate list from the hashset to avoid list modification errors. - var spawnedObjects = SpawnedObjectsList.ToList(); - foreach (var networkObject in spawnedObjects) - { - networkObject.InternalNetworkSessionSynchronized(); - } - } - - /// - /// Distributed Authority Only - /// Should be invoked on non-session owner clients when a newly joined client is finished - /// synchronizing in order to "show" (spawn) anything that might be currently hidden from - /// the session owner. - /// - /// - /// Replacement is: SynchronizeObjectsToNewlyJoinedClient - /// - internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId) - { - if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); - return; - } - - if (!NetworkManager.DistributedAuthorityMode) - { - Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); - return; - } - - if (NetworkManager.LocalClient.IsSessionOwner) - { - Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!"); - return; - } - var localClientId = NetworkManager.LocalClient.ClientId; - var sessionOwnerId = NetworkManager.CurrentSessionOwner; - foreach (var networkObject in SpawnedObjectsList) - { - if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId)) - { - if (networkObject.Observers.Contains(newClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner - Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!"); - } - // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added - // code to handle this edge case works. - networkObject.Observers.Remove(newClientId); - } - networkObject.NetworkShow(newClientId); - } - } - } - - internal void SynchronizeObjectsToNewlyJoinedClient(ulong newClientId) - { - if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); - return; - } - - if (!NetworkManager.DistributedAuthorityMode) - { - Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); - return; - } - - if (NetworkManager.NetworkConfig.EnableSceneManagement) - { - Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when scene management is disabled!"); - return; - } - - var localClientId = NetworkManager.LocalClient.ClientId; - foreach (var networkObject in SpawnedObjectsList) - { - if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId) - { - if (networkObject.Observers.Contains(newClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - // Temporary tracking to make sure we are not showing something already visibile (should never be the case for this) - Debug.LogWarning($"[{nameof(SynchronizeObjectsToNewlyJoinedClient)}][{networkObject.name}] New client as already an observer!"); - } - // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added - // code to handle this edge case works. - networkObject.Observers.Remove(newClientId); - } - networkObject.NetworkShow(newClientId); - } - } - } - } + var networkObjectsToSpawn = new List(); + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + // This used to be two loops. + // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. + // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. + if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) + { + var ownerId = networkObjects[i].OwnerClientId; + if (NetworkManager.DistributedAuthorityMode) + { + ownerId = NetworkManager.LocalClientId; + } + + SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); + networkObjectsToSpawn.Add(networkObjects[i]); + } + } + } + + // Since we are spawing in-scene placed NetworkObjects for already loaded scenes, + // we need to add any in-scene placed NetworkObject to our tracking table + var clearFirst = true; + foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded) + { + NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst); + clearFirst = false; + } + + // Notify all in-scene placed NetworkObjects have been spawned + foreach (var networkObject in networkObjectsToSpawn) + { + networkObject.InternalInSceneNetworkObjectsSpawned(); + } + networkObjectsToSpawn.Clear(); + } + + /// + /// Called when destroying an object after receiving a . + /// Processes logic for how to destroy objects on the non-authority client. + /// + internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject) + { + if (networkObject.HasAuthority) + { + NetworkLog.LogError($"OnDespawnNonAuthorityObject called on object {networkObject.NetworkObjectId} when is current client {NetworkManager.LocalClientId} has authority on this object."); + } + + // On the non-authority, never destroy the game object when InScenePlaced, otherwise always destroy on non-authority side + OnDespawnObject(networkObject, networkObject.IsSceneObject == false); + } + + internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject, bool modeDestroy = false) + { + if (!NetworkManager) + { + return; + } + + // We have to do this check first as subsequent checks assume we can access NetworkObjectId. + if (!networkObject) + { + Debug.LogWarning($"Trying to destroy network object but it is null"); + return; + } + + // Removal of spawned object + if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId)) + { + if (!NetworkManager.ShutdownInProgress) + { + Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!"); + } + return; + } + + // If we are shutting down the NetworkManager, then ignore resetting the parent + // and only attempt to remove the child's parent on the server-side + var distributedAuthority = NetworkManager.DistributedAuthorityMode; + if (!NetworkManager.ShutdownInProgress && (NetworkManager.IsServer || distributedAuthority)) + { + // Get all child NetworkObjects + var objectsToRemoveParent = networkObject.GetComponentsInChildren(); + + // Move child NetworkObjects to the root when parent NetworkObject is destroyed + foreach (var spawnedNetObj in objectsToRemoveParent) + { + if (spawnedNetObj == networkObject) + { + continue; + } + var latestParent = spawnedNetObj.GetNetworkParenting(); + // Only deparent the first generation children of the NetworkObject being spawned. + // Ignore any nested children under first generation children. + if (latestParent.HasValue && latestParent.Value != networkObject.NetworkObjectId) + { + continue; + } + // For mixed authority hierarchies, if the parent is despawned then any removal of children + // is considered "authority approved". If we don't have authority over the object and we are + // in distributed authority mode, then set the AuthorityAppliedParenting flag. + spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !spawnedNetObj.HasAuthority; + + // Try to remove the parent using the cached WorldPositionStays value + // Note: WorldPositionStays will still default to true if this was an + // in-scene placed NetworkObject and parenting was predefined in the + // scene via the editor. + if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays()) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed"); + } + } + else + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + } + } + } + + networkObject.InvokeBehaviourNetworkDespawn(); + + if (NetworkManager != null && ((NetworkManager.IsServer && (!distributedAuthority || + (distributedAuthority && modeDestroy))) || + (distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId))) + { + if (NetworkManager.NetworkConfig.RecycleNetworkIds) + { + ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() + { + NetworkId = networkObject.NetworkObjectId, + ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime + }); + } + m_TargetClientIds.Clear(); + + // If clients are not allowed to spawn locally then go ahead and send the despawn message or if we are in distributed authority mode, we are the server, we own this NetworkObject + // send the despawn message, and as long as we have any remaining clients, then notify of the object being destroy. + if (NetworkManager.IsServer && NetworkManager.ConnectedClientsList.Count > 0 && (!distributedAuthority || + (NetworkManager.DAHost && distributedAuthority && + (networkObject.OwnerClientId == NetworkManager.LocalClientId || modeDestroy)))) + { + // We keep only the client for which the object is visible + // as the other clients have them already despawned + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if ((distributedAuthority && clientId == networkObject.OwnerClientId) || clientId == NetworkManager.LocalClientId) + { + continue; + } + if (networkObject.IsNetworkVisibleTo(clientId)) + { + m_TargetClientIds.Add(clientId); + } + } + } + else // DANGO-TODO: If we are not the server, distributed authority mode is enabled, and we are the owner then inform the DAHost to despawn the NetworkObject + if (!NetworkManager.IsServer && distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId) + { + // DANGO-TODO: If a shutdown is not in progress or a shutdown is in progress and we can destroy with the owner then notify the DAHost + if (!NetworkManager.ShutdownInProgress || (NetworkManager.ShutdownInProgress && !networkObject.DontDestroyWithOwner)) + { + m_TargetClientIds.Add(NetworkManager.ServerClientId); + } + } + + if (m_TargetClientIds.Count > 0 && !NetworkManager.ShutdownInProgress) + { + var message = new DestroyObjectMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + DeferredDespawnTick = networkObject.DeferredDespawnTick, + DestroyGameObject = destroyGameObject, + IsTargetedDestroy = false, + IsDistributedAuthority = distributedAuthority, + }; + foreach (var clientId in m_TargetClientIds) + { + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, networkObject, size); + } + } + } + + networkObject.IsSpawned = false; + networkObject.DeferredDespawnTick = 0; + + if (SpawnedObjects.Remove(networkObject.NetworkObjectId)) + { + SpawnedObjectsList.Remove(networkObject); + } + + if (networkObject.IsPlayerObject) + { + RemovePlayerObject(networkObject, destroyGameObject); + } + + // Always clear out the observers list when despawned + networkObject.Observers.Clear(); + + var gobj = networkObject.gameObject; + if (destroyGameObject && gobj != null) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); + } + else + { + UnityEngine.Object.Destroy(gobj); + } + } + } + + /// + /// Updates all spawned for the specified newly connected client + /// Note: if the clientId is the server then it is observable to all spawned 's + /// + /// + /// This method is to only to be used for newly connected clients in order to update the observers list for + /// each NetworkObject instance. + /// + internal void UpdateObservedNetworkObjects(ulong clientId) + { + foreach (var sobj in SpawnedObjectsList) + { + // If the NetworkObject has no visibility check then prepare to add this client as an observer + if (sobj.CheckObjectVisibility == null) + { + // If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server/host/DAHost + if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId)) + { + sobj.Observers.Add(clientId); + } + } + else + { + // CheckObject visibility overrides SpawnWithObservers under this condition + if (sobj.CheckObjectVisibility(clientId)) + { + sobj.Observers.Add(clientId); + } + else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false + { + sobj.Observers.Remove(clientId); + } + } + } + } + + /// + /// See + /// + internal void HandleNetworkObjectShow() + { + // In distributed authority mode, we send a single message that is broadcasted to all clients + // that will be shown the object (i.e. 1 message to service that then broadcasts that to the + // targeted clients). When using a DAHost, we skip this and send like we do in client-server + if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) + { + foreach (var entry in ClientsToShowObject) + { + if (entry.Key != null && entry.Key.IsSpawned) + { + try + { + SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } + } + ClientsToShowObject.Clear(); + ObjectsToShowToClient.Clear(); + return; + } + + // Server or Host handling of NetworkObjects to show + foreach (var client in ObjectsToShowToClient) + { + ulong clientId = client.Key; + foreach (var networkObject in client.Value) + { + if (networkObject != null && networkObject.IsSpawned) + { + try + { + SendSpawnCallForObject(clientId, networkObject, null); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } + } + } + ObjectsToShowToClient.Clear(); + } + + internal NetworkSpawnManager(NetworkManager networkManager) + { + NetworkManager = networkManager; + } + + /// + /// Finalizer that ensures proper cleanup of spawn manager resources + /// + ~NetworkSpawnManager() + { + Shutdown(); + } + + internal void Shutdown() + { + NetworkObjectsToSynchronizeSceneChanges.Clear(); + CleanUpDisposedObjects.Clear(); + } + + /// + /// DANGO-TODO: Until we have the CMB Server end-to-end with all features verified working via integration tests, + /// I am keeping this debug toggle available. (NSS) + /// + internal bool EnableDistributeLogging = false; + + /// + /// Fills the first table passed in with the current distribution of prefab types relative to their owners + /// Fills the second table passed in with the total number of spawned objects of that particular type. + /// The second table allows us to calculate how many objects per client there should be in order to determine + /// how many of that type should be distributed. + /// + /// the table to populate + /// the total number of the specific object type to distribute + internal void GetObjectDistribution(ulong clientId, ref Dictionary>> objectByTypeAndOwner, ref Dictionary objectTypeCount) + { + // DANGO-TODO-MVP: Remove this once the service handles object distribution + var onlyIncludeOwnedObjects = NetworkManager.CMBServiceConnection; + + foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList) + { + if (networkObject.IsOwnershipSessionOwner) + { + continue; + } + + // We first check for a parent and then if the parent is a NetworkObject + if (networkObject.transform.parent != null) + { + // If the parent is a NetworkObject, has the same owner, and this NetworkObject has the distributable =or= transferable permission + // then skip this child (NetworkObjects are always parented directly under other NetworkObjects) + // (later we determine if all children with the same owner will be transferred together as a group) + var parentNetworkObject = networkObject.transform.parent.GetComponent(); + if (parentNetworkObject != null && parentNetworkObject.OwnerClientId == networkObject.OwnerClientId + && (networkObject.IsOwnershipDistributable || networkObject.IsOwnershipTransferable)) + { + continue; + } + } + + // At this point we only allow things marked with the distributable permission and is not locked to be distributed + if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked) + { + // Don't include anything that is not visible to the new client + if (!networkObject.Observers.Contains(clientId)) + { + continue; + } + + // We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance + // since all in-scene placed instances use unique GlobalObjectIdHash values. + var globalOjectIdHash = networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value ? networkObject.InScenePlacedSourceGlobalObjectIdHash : networkObject.GlobalObjectIdHash; + + if (!objectTypeCount.ContainsKey(globalOjectIdHash)) + { + objectTypeCount.Add(globalOjectIdHash, 0); + } + objectTypeCount[globalOjectIdHash] += 1; + + // DANGO-TODO-MVP: Remove this once the service handles object distribution + if (onlyIncludeOwnedObjects && !networkObject.IsOwner) + { + continue; + } + + + // Divide up by prefab type (GlobalObjectIdHash) to get a better distribution of object types + if (!objectByTypeAndOwner.ContainsKey(globalOjectIdHash)) + { + objectByTypeAndOwner.Add(globalOjectIdHash, new Dictionary>()); + } + + // Sub-divide each type by owner + if (!objectByTypeAndOwner[globalOjectIdHash].ContainsKey(networkObject.OwnerClientId)) + { + objectByTypeAndOwner[globalOjectIdHash].Add(networkObject.OwnerClientId, new List()); + } + + // Add to the client's spawned object list + objectByTypeAndOwner[globalOjectIdHash][networkObject.OwnerClientId].Add(networkObject); + } + } + } + + /// + /// Distributes an even portion of spawned NetworkObjects to the given client. + /// This is called as part of the client connection process to ensure that all clients have a fair share of the spawned NetworkObjects. + /// DANGO-TODO: This will be handled by the CMB Service in the future. + /// + /// Client to distribute NetworkObjects to + internal void DistributeNetworkObjects(ulong clientId) + { + if (!NetworkManager.DistributedAuthorityMode) + { + return; + } + + if (NetworkManager.SessionConfig.ServiceSideDistribution) + { + return; + } + + // DA-NGO CMB SERVICE NOTES: + // The most basic object distribution should be broken up into a table of spawned object types + // where each type contains a list of each client's owned objects of that type that can be + // distributed. + // The table format: + // [GlobalObjectIdHashValue][ClientId][List of Owned Objects] + var distributedNetworkObjects = new Dictionary>>(); + + // DA-NGO CMB SERVICE NOTES: + // This is optional, but I found it easier to get the total count of spawned objects for each prefab + // type contained in the previous table in order to be able to calculate the targeted object distribution + // count of that type per client. + var objectTypeCount = new Dictionary(); + + // Get all spawned objects by type and then by client owner that are spawned and can be distributed + GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); + + var clientCount = NetworkManager.ConnectedClientsIds.Count; + + // Cycle through each prefab type + foreach (var objectTypeEntry in distributedNetworkObjects) + { + // Calculate the number of objects that should be distributed amongst the clients + var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key]; + var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount); + var floorValue = (int)Math.Floor(objPerClientF); + var fractional = objPerClientF - floorValue; + var objPerClient = 0; + if (fractional >= 0.556f) + { + objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount)); + } + else + { + objPerClient = floorValue; + } + + // If the object per client count is zero, then move to the next type. + if (objPerClient <= 0) + { + continue; + } + + // Evenly distribute this object type amongst the clients + foreach (var ownerList in objectTypeEntry.Value) + { + if (ownerList.Value.Count <= 1) + { + continue; + } + + var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1); + var distributed = 0; + // For now when we have more players then distributed NetworkObjects that + // a specific client owns, just assign half of the NetworkObjects to the new client + var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1); + if (EnableDistributeLogging) + { + Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}"); + } + + for (int i = 0; i < ownerList.Value.Count; i++) + { + if ((i % offsetCount) == 0) + { + var children = ownerList.Value[i].GetComponentsInChildren(); + // Since the ownerList.Value[i] has to be distributable, then transfer all child NetworkObjects + // with the same owner clientId and are marked as distributable also to the same client to keep + // the owned distributable parent with the owned distributable children + foreach (var child in children) + { + // Ignore any child that does not have the same owner, that is already owned by the currently targeted client, or that doesn't have the targeted client as an observer + if (child == ownerList.Value[i] || child.OwnerClientId != ownerList.Value[i].OwnerClientId || child.OwnerClientId == clientId || !child.Observers.Contains(clientId)) + { + continue; + } + if (!child.IsOwnershipDistributable || !child.IsOwnershipTransferable) + { + NetworkLog.LogWarning($"Sibling {child.name} of root parent {ownerList.Value[i].name} is neither transferable or distributable! Object distribution skipped and could lead to a potentially un-owned or owner-mismatched {nameof(NetworkObject)}!"); + continue; + } + // Transfer ownership of all distributable =or= transferable children with the same owner to the same client to preserve the sibling ownership tree. + ChangeOwnership(child, clientId, true); + // Note: We don't increment the distributed count for these children as they are skipped when getting the object distribution + } + // Finally, transfer ownership of the root parent + ChangeOwnership(ownerList.Value[i], clientId, true); + if (EnableDistributeLogging) + { + Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}"); + } + distributed++; + } + if (distributed == maxDistributeCount) + { + break; + } + } + } + } + + // If EnableDistributeLogging is enabled, log the object type distribution counts per client + if (EnableDistributeLogging) + { + var builder = new StringBuilder(); + distributedNetworkObjects.Clear(); + objectTypeCount.Clear(); + GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); + builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)"); + // Cycle through each prefab type + foreach (var objectTypeEntry in distributedNetworkObjects) + { + builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]"); + builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:"); + // Evenly distribute this type amongst clients + foreach (var ownerList in objectTypeEntry.Value) + { + builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}"); + } + } + Debug.Log(builder.ToString()); + } + } + + internal struct DeferredDespawnObject + { + public int TickToDespawn; + public bool HasDeferredDespawnCheck; + public ulong NetworkObjectId; + } + + internal List DeferredDespawnObjects = new List(); + + /// + /// Adds a deferred despawn entry to be processed + /// + /// associated NetworkObject + /// when to despawn the NetworkObject + /// if true, user script is to be invoked to determine when to despawn + internal void DeferDespawnNetworkObject(ulong networkObjectId, int tickToDespawn, bool hasDeferredDespawnCheck) + { + var deferredDespawnObject = new DeferredDespawnObject() + { + TickToDespawn = tickToDespawn, + HasDeferredDespawnCheck = hasDeferredDespawnCheck, + NetworkObjectId = networkObjectId, + }; + DeferredDespawnObjects.Add(deferredDespawnObject); + } + + /// + /// Processes any deferred despawn entries + /// + internal void DeferredDespawnUpdate(NetworkTime serverTime) + { + // Exit early if there is nothing to process + if (DeferredDespawnObjects.Count == 0) + { + return; + } + var currentTick = serverTime.Tick; + var deferredDespawnCount = DeferredDespawnObjects.Count; + // Loop forward and to process user callbacks and update despawn ticks + for (int i = 0; i < deferredDespawnCount; i++) + { + var deferredObjectEntry = DeferredDespawnObjects[i]; + if (!deferredObjectEntry.HasDeferredDespawnCheck) + { + continue; + } + + if (!SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) + { + continue; + } + + // Double check to make sure user did not remove the callback + if (networkObject.OnDeferredDespawnComplete != null) + { + // If the user callback returns true, then we despawn it this tick + if (networkObject.OnDeferredDespawnComplete.Invoke()) + { + deferredObjectEntry.TickToDespawn = currentTick; + } + else + { + // If the user callback does not verify the NetworkObject can be despawned, + // continue setting this value in the event user script adjusts it. + deferredObjectEntry.TickToDespawn = networkObject.DeferredDespawnTick; + } + } + else + { + // If it was removed, then in case it is being left to defer naturally exclude it + // from the next query. + deferredObjectEntry.HasDeferredDespawnCheck = false; + } + } + + // Parse backwards so we can remove objects as we parse through them + for (int i = deferredDespawnCount - 1; i >= 0; i--) + { + var deferredObjectEntry = DeferredDespawnObjects[i]; + if (deferredObjectEntry.TickToDespawn >= currentTick) + { + continue; + } + + if (SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) + { + // Local instance despawns the instance + OnDespawnNonAuthorityObject(networkObject); + } + + DeferredDespawnObjects.RemoveAt(i); + } + } + + internal void NotifyNetworkObjectsSynchronized() + { + // Users could spawn NetworkObjects during these notifications. + // Create a separate list from the hashset to avoid list modification errors. + var spawnedObjects = SpawnedObjectsList.ToList(); + foreach (var networkObject in spawnedObjects) + { + networkObject.InternalNetworkSessionSynchronized(); + } + } + + /// + /// Distributed Authority Only + /// Should be invoked on non-session owner clients when a newly joined client is finished + /// synchronizing in order to "show" (spawn) anything that might be currently hidden from + /// the session owner. + /// + /// + /// Replacement is: SynchronizeObjectsToNewlyJoinedClient + /// + internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId) + { + if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); + return; + } + + if (!NetworkManager.DistributedAuthorityMode) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); + return; + } + + if (NetworkManager.LocalClient.IsSessionOwner) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!"); + return; + } + var localClientId = NetworkManager.LocalClient.ClientId; + var sessionOwnerId = NetworkManager.CurrentSessionOwner; + foreach (var networkObject in SpawnedObjectsList) + { + if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId)) + { + if (networkObject.Observers.Contains(newClientId)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner + Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!"); + } + // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added + // code to handle this edge case works. + networkObject.Observers.Remove(newClientId); + } + networkObject.NetworkShow(newClientId); + } + } + } + + internal void SynchronizeObjectsToNewlyJoinedClient(ulong newClientId) + { + if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); + return; + } + + if (!NetworkManager.DistributedAuthorityMode) + { + Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); + return; + } + + if (NetworkManager.NetworkConfig.EnableSceneManagement) + { + Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when scene management is disabled!"); + return; + } + + var localClientId = NetworkManager.LocalClient.ClientId; + foreach (var networkObject in SpawnedObjectsList) + { + if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId) + { + if (networkObject.Observers.Contains(newClientId)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + // Temporary tracking to make sure we are not showing something already visibile (should never be the case for this) + Debug.LogWarning($"[{nameof(SynchronizeObjectsToNewlyJoinedClient)}][{networkObject.name}] New client as already an observer!"); + } + // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added + // code to handle this edge case works. + networkObject.Observers.Remove(newClientId); + } + networkObject.NetworkShow(newClientId); + } + } + } + } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs index c66c843abf..29db0d01bb 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs @@ -110,7 +110,7 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) //Test result of registering via GameObject reference Assert.True(gameObjectRegistered); - var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -135,7 +135,7 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(2.0f, 1.0f, 5.0f); prefabRotation = new Quaternion(4.0f, 1.5f, 5.4f, 5.1f); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -160,7 +160,7 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(6.0f, 4.0f, 1.0f); prefabRotation = new Quaternion(3f, 2f, 4f, 1f); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); //Test that something was instantiated Assert.NotNull(spawnedObject); From 9c18c02fb5bffc645854962b25e6e52d3b40be93 Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 23 Apr 2025 19:28:04 +0200 Subject: [PATCH 3/7] Removed work in progress --- .../Runtime/Core/NetworkObject.cs | 92 ----------- .../Runtime/Spawning/NetworkPrefabHandler.cs | 24 --- .../Runtime/Spawning/NetworkSpawnManager.cs | 147 +----------------- 3 files changed, 4 insertions(+), 259 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index c99f1d15a1..470e7d7e28 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1843,94 +1843,6 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla NetworkLog.LogWarningServer($"Ran into unknown conditional check during spawn when determining distributed authority mode or not"); } } - //internal void SpawnInternalExtended(bool destroyWithScene, ulong ownerClientId, bool playerObject, byte[] customSpawnData) - //{ - // if (NetworkManagerOwner == null) - // { - // NetworkManagerOwner = NetworkManager.Singleton; - // } - // if (!NetworkManager.IsListening) - // { - // throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects"); - // } - - // if ((!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) || (NetworkManager.DistributedAuthorityMode && !NetworkManager.LocalClient.IsSessionOwner && NetworkManager.LocalClientId != ownerClientId)) - // { - // if (NetworkManager.DistributedAuthorityMode) - // { - // throw new NotServerException($"When distributed authority mode is enabled, you can only spawn NetworkObjects that belong to the local instance! Local instance id {NetworkManager.LocalClientId} is not the same as the assigned owner id: {ownerClientId}!"); - // } - // else - // { - // throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); - // } - // } - - // if (NetworkManager.DistributedAuthorityMode) - // { - // if (NetworkManager.LocalClient == null || !NetworkManager.IsConnectedClient || !NetworkManager.ConnectionManager.LocalClient.IsApproved) - // { - // Debug.LogError($"Cannot spawn {name} until the client is fully connected to the session!"); - // return; - // } - // if (NetworkManager.NetworkConfig.EnableSceneManagement) - // { - // if (!NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(gameObject.scene.handle)) - // { - // // Most likely this issue is due to an integration test - // if (NetworkManager.LogLevel <= LogLevel.Developer) - // { - // NetworkLog.LogWarning($"Failed to find scene handle {gameObject.scene.handle} for {gameObject.name}!"); - // } - // // Just use the existing handle - // NetworkSceneHandle = gameObject.scene.handle; - // } - // else - // { - // NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[gameObject.scene.handle]; - // } - // } - // if (DontDestroyWithOwner && !IsOwnershipDistributable) - // { - // //Ownership |= OwnershipStatus.Distributable; - // // DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set - // if (NetworkManager.LogLevel == LogLevel.Developer) - // { - // NetworkLog.LogWarning("DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set. For now, if the NetworkObject does not destroy with the owner it will set ownership to SessionOwner."); - // } - // } - // } - - // NetworkManager.SpawnManager.SpawnNetworkObjectLocallyExtended(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); - - // if ((NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost) || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer)) - // { - // for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) - // { - // if (NetworkManager.ConnectedClientsList[i].ClientId == NetworkManager.ServerClientId) - // { - // continue; - // } - // if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) - // { - // NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this, customSpawnData); - // } - // } - // } - // else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) - // { - // // If spawning with observers or if not spawning with observers but the observer count is greater than 1 (i.e. owner/authority creating), - // // then we want to send a spawn notification. - // if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) - // { - // NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this, customSpawnData); - // } - // } - // else - // { - // NetworkLog.LogWarningServer($"Ran into unknown conditional check during spawn when determining distributed authority mode or not"); - // } - //} /// /// This invokes . @@ -2025,10 +1937,6 @@ public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false, by { SpawnInternal(destroyWithScene, clientId, false, customSpawnData); } - //public void SpawnWithOwnershipExtended(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) - //{ - // SpawnInternalExtended(destroyWithScene, clientId, false, customSpawnData); - //} /// /// Spawns a across the network and makes it the player object for the given client diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index 88410e55d9..9b190045d3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -276,30 +276,6 @@ internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulo return null; } - - - internal NetworkObject HandleNetworkPrefabSpawnExtended(int handlerId, ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] customSpawnData) - { - if (NetworkSpawnManager.customHandlers.TryGetValue(handlerId, out var prefabInstanceHandler)) - { - if (customSpawnData != null && prefabInstanceHandler is INetworkCustomSpawnDataReceiver receiver) - receiver.OnCustomSpawnDataReceived(customSpawnData); - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); - - //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) - //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. - if (networkObjectInstance != null && !NetworkSpawnManager.customPrefabInstanceToHandlers.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) - { - NetworkSpawnManager.customPrefabInstanceToHandlers.Add(networkObjectInstance.GlobalObjectIdHash, handlerId); - } - - return networkObjectInstance; - } - - return null; - } - - /// /// Will invoke the implementation's Destroy method diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 28d1afbf5d..c9eb831e9f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -734,25 +734,7 @@ public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong owne return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation, customSpawnData); } - //public NetworkObject InstantiateAndSpawnExtended(int handlerId, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - //{ - // ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; - // // We only need to check for authority when running in client-server mode - // if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) - // { - // Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); - // return null; - // } - - // if (NetworkManager.ShutdownInProgress) - // { - // Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); - // return null; - // } - - // return InstantiateAndSpawnNoParameterChecksExtended(handlerId, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); - //} - + /// /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! /// @@ -792,30 +774,7 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ } return networkObject; } - //internal NetworkObject InstantiateAndSpawnNoParameterChecksExtended(int handlerId, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - //{ - // var networkObject = InstantiateNetworkPrefabExtended(handlerId, position, rotation); - // // - Host and clients always instantiate the override if one exists. - // // - Server instantiates the original prefab unless: - // // -- forceOverride is set to true =or= - // // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. - // // - Distributed authority mode always spawns the override if one exists. - // networkObject = GetNetworkObjectToSpawnExtended(handlerId, ownerClientId, position, rotation); - - // if (networkObject == null) - // { - // Debug.LogError($"Failed to instantiate and spawn Extended for Handler with id: {handlerId}!"); - // return null; - // } - // networkObject.IsPlayerObject = isPlayerObject; - // networkObject.transform.position = position; - // networkObject.transform.rotation = rotation; - - // networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); - - // return networkObject; - //} - + /// /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. @@ -884,23 +843,6 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow return networkObject; } - //internal NetworkObject GetNetworkObjectToSpawnExtended(int handlerId, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) - //{ - // NetworkObject networkObject = null; - // // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class - // if (customHandlers.ContainsKey(handlerId)) - // { - // // Let the handler spawn the NetworkObject - // networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawnExtended(handlerId, ownerId, position ?? default, rotation ?? default); - // networkObject.NetworkManagerOwner = NetworkManager; - // } - // else - // { - // throw new Exception($"No handler registered with id: {handlerId}"); - // } - // return networkObject; - //} - /// /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients /// the instance. @@ -925,21 +867,7 @@ internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint p networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; return networkObject; } - public static Dictionary customHandlers = new(); - public static Dictionary customPrefabInstanceToHandlers = new(); - //internal NetworkObject InstantiateNetworkPrefabExtended(int handlerId, Vector3? position, Quaternion? rotation) - //{ - - // //TODO: ACCESS NETWORK PREFAB HANDLER - - // var networkObject = customHandlers[handlerId].Instantiate(0, Vector3.zero, Quaternion.identity); - // networkObject.transform.position = position ?? networkObject.transform.position; - // networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; - // networkObject.NetworkManagerOwner = NetworkManager; - // //networkObject.PrefabGlobalObjectIdHash = 0; - // return networkObject; - //} - + /// /// Creates a local NetowrkObject to be spawned. /// @@ -1155,74 +1083,7 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong netwo // Invoke NetworkBehaviour.OnPostSpawn methods networkObject.InvokeBehaviourNetworkPostSpawn(); } - //internal void SpawnNetworkObjectLocallyExtended(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) - //{ - // if (networkObject == null) - // { - // throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); - // } - - // if (networkObject.IsSpawned) - // { - // Debug.LogError($"{networkObject.name} is already spawned!"); - // return; - // } - - // if (!sceneObject) - // { - // var networkObjectChildren = networkObject.GetComponentsInChildren(); - // if (networkObjectChildren.Length > 1) - // { - // Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); - // } - // } - // // Invoke NetworkBehaviour.OnPreSpawn methods - // networkObject.InvokeBehaviourNetworkPreSpawn(); - - // // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning - // // For now, this is the best place I could find to add all connected clients as observers for newly - // // instantiated and spawned NetworkObjects on the authoritative side. - // if (NetworkManager.DistributedAuthorityMode) - // { - // if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) - // { - // networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; - // networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; - // } - - // // Always add the owner/authority even if SpawnWithObservers is false - // // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) - // if (!networkObject.SpawnWithObservers) - // { - // networkObject.Observers.Add(ownerClientId); - // } - // else - // { - // foreach (var clientId in NetworkManager.ConnectedClientsIds) - // { - // // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration - // if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) - // { - // continue; - // } - // networkObject.Observers.Add(clientId); - // } - - // // Sanity check to make sure the owner is always included - // // Itentionally checking as opposed to just assigning in order to generate notification. - // if (!networkObject.Observers.Contains(ownerClientId)) - // { - // Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); - // networkObject.Observers.Add(ownerClientId); - // } - // } - // } - // SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); - - // // Invoke NetworkBehaviour.OnPostSpawn methods - // networkObject.InvokeBehaviourNetworkPostSpawn(); - //} - + /// /// This is only invoked to instantiate a serialized NetworkObject via /// From 62acdb1789ca307ad644bf1cda75e32f7d2b095b Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 23 Apr 2025 19:57:50 +0200 Subject: [PATCH 4/7] Update package.json --- com.unity.netcode.gameobjects/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index 8f572051d0..1f09e52ae8 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.netcode.gameobjects", - "displayName": "Netcode for GameObjects_Extended", - "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer. This fork extends the spawn system for easy spawn management over network", + "displayName": "Netcode for GameObjects", + "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", "version": "2.3.2", "unity": "6000.0", "dependencies": { From 1943a2ad9bf1d15a7372617b77c744119f2b43dc Mon Sep 17 00:00:00 2001 From: Extrys Date: Sat, 26 Apr 2025 22:21:15 +0200 Subject: [PATCH 5/7] Symmetrical approach working --- .../Connection/NetworkConnectionManager.cs | 12 ++- .../Runtime/Core/NetworkObject.cs | 98 ++++++++++++++++--- .../Messages/ConnectionApprovedMessage.cs | 2 + .../Messaging/Messages/CreateObjectMessage.cs | 19 ++-- .../SceneManagement/NetworkSceneManager.cs | 3 +- .../INetworkCustomSpawnDataReceiver.cs | 4 +- .../INetworkCustomSpawnDataReceiver.cs.meta | 2 + .../Runtime/Spawning/NetworkPrefabHandler.cs | 39 ++++++-- .../Runtime/Spawning/NetworkSpawnManager.cs | 40 +++++--- .../Prefabs/NetworkPrefabHandlerTests.cs | 12 ++- 10 files changed, 171 insertions(+), 60 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 115bc2cbf8..b8d5a5f9bb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -797,8 +797,10 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne // Server-side spawning (only if there is a prefab hash or player prefab provided) if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) { - var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position ?? null, response.Rotation ?? null) - : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null); + var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0,Allocator.Temp))); + Debug.Log("SENDING BUFFFER WRITERRR FOR PLAYER SPAWN"); + var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, ref bufferReaderSerializer, response.Position ?? null, response.Rotation ?? null) + : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, ref bufferReaderSerializer, response.Position ?? null, response.Rotation ?? null); // Spawn the player NetworkObject locally NetworkManager.SpawnManager.SpawnNetworkObjectLocally( @@ -942,7 +944,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne /// /// Client-Side Spawning in distributed authority mode uses this to spawn the player. /// - internal void CreateAndSpawnPlayer(ulong ownerId, byte[] customSpawnData = null) + internal void CreateAndSpawnPlayer(ulong ownerId) { if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide) { @@ -950,7 +952,9 @@ internal void CreateAndSpawnPlayer(ulong ownerId, byte[] customSpawnData = null) if (playerPrefab != null) { var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash; - var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation, customSpawnData:customSpawnData); + var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Allocator.Temp))); + Debug.Log("SENDING BUFFFER WRITERRR FOR PLAYER SPAWN 2"); + var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, ref bufferReaderSerializer, playerPrefab.transform.position, playerPrefab.transform.rotation); networkObject.IsSceneObject = false; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 470e7d7e28..069fd04509 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Unity.Collections; using Unity.Netcode.Components; #if UNITY_EDITOR using UnityEditor; @@ -36,6 +37,8 @@ public sealed class NetworkObject : MonoBehaviour /// internal uint PrefabGlobalObjectIdHash; + internal FastBufferReader preInstanceData; + /// /// This is the source prefab of an in-scene placed NetworkObject. This is not set for in-scene /// placd NetworkObjects that are not prefab instances, dynamically spawned prefab instances, @@ -1753,7 +1756,7 @@ private void OnDestroy() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject, byte[] customSpawnData) + internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (NetworkManagerOwner == null) { @@ -1823,8 +1826,8 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) { - Debug.Log("customSpawnData: " + (customSpawnData?.Length ?? 0).ToString()); - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this, customSpawnData); + Debug.Log("Spawn internal 1, object name " + gameObject.name); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); } } } @@ -1834,8 +1837,8 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla // then we want to send a spawn notification. if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) { - Debug.Log("customSpawnData: " + customSpawnData.Length); - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this, customSpawnData); + Debug.Log("Spawn internal 2, object name " + gameObject.name); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); } } else @@ -1922,10 +1925,10 @@ public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ow /// Spawns this across the network. Can only be called from the Server /// /// Should the object be destroyed when the scene is changed - public void Spawn(bool destroyWithScene = false, byte[] customSpawnData = null) + public void Spawn(bool destroyWithScene = false) { var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; - SpawnInternal(destroyWithScene, clientId, false, customSpawnData); + SpawnInternal(destroyWithScene, clientId, false); } /// @@ -1933,9 +1936,9 @@ public void Spawn(bool destroyWithScene = false, byte[] customSpawnData = null) /// /// The clientId to own the object /// Should the object be destroyed when the scene is changed - public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) + public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) { - SpawnInternal(destroyWithScene, clientId, false, customSpawnData); + SpawnInternal(destroyWithScene, clientId, false); } /// @@ -1943,9 +1946,9 @@ public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false, by /// /// The clientId who's player object this is /// Should the object be destroyed when the scene is changed - public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false, byte[] customSpawnData = null) + public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) { - SpawnInternal(destroyWithScene, clientId, true, customSpawnData); + SpawnInternal(destroyWithScene, clientId, true); } /// @@ -2814,6 +2817,7 @@ internal struct SceneObject public ulong NetworkObjectId; public ulong OwnerClientId; public ushort OwnershipFlags; + public FastBufferReader preInstanceData; public bool IsPlayerObject { @@ -2884,6 +2888,13 @@ public bool SpawnWithObservers set => ByteUtility.SetBit(ref m_BitField, 10, value); } + public bool HasPreInstantiateData + { + get => ByteUtility.GetBit(m_BitField, 11); + set => ByteUtility.SetBit(ref m_BitField, 11, value); + } + + // When handling the initial synchronization of NetworkObjects, // this will be populated with the known observers. public ulong[] Observers; @@ -2924,6 +2935,8 @@ public void Serialize(FastBufferWriter writer) BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, OwnerClientId); + + if (HasParent) { BytePacker.WriteValueBitPacked(writer, ParentObjectId); @@ -2950,17 +2963,34 @@ public void Serialize(FastBufferWriter writer) var writeSize = 0; writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += FastBufferWriter.GetWriteSize(); + if (HasPreInstantiateData) + { + writeSize += FastBufferWriter.GetWriteSize(); + writeSize += preInstanceData.Length; + } + if (!writer.TryBeginWrite(writeSize)) { throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); } + + if (HasPreInstantiateData) + { + writer.WriteValueSafe(preInstanceData.Length); + Debug.Log($"PreInstanceData Length Serialize: {preInstanceData.Length}"); + unsafe + { + writer.WriteBytes(preInstanceData.GetUnsafePtr(), preInstanceData.Length); + } + } if (HasTransform) { writer.WriteValue(Transform); } + // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. if (OwnerObject.NetworkManager.DistributedAuthorityMode) @@ -2972,6 +3002,8 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } + + // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); @@ -2984,6 +3016,8 @@ public void Deserialize(FastBufferReader reader) ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId); ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId); + + if (HasParent) { ByteUnpacker.ReadValueBitPacked(reader, out ParentObjectId); @@ -3016,21 +3050,49 @@ public void Deserialize(FastBufferReader reader) readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += FastBufferWriter.GetWriteSize(); + int preInstanceDataSize = 0; + if (HasPreInstantiateData) + { + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize())) + { + throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer (preInstanceData size)"); + } + + reader.ReadValueSafe(out preInstanceDataSize); + readSize += FastBufferWriter.GetWriteSize(); + readSize += preInstanceDataSize; + } + + // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) { throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } + if (HasPreInstantiateData) + { + Debug.Log($"PreInstanceData Length des: {preInstanceDataSize}"); + unsafe + { + preInstanceData = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, preInstanceDataSize); + reader.Seek(reader.Position + preInstanceDataSize); + } + } + if (HasTransform) { reader.ReadValue(out Transform); } + + // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. reader.ReadValue(out NetworkSceneHandle); - } + + + } } internal void PostNetworkVariableWrite(bool forced = false) @@ -3150,8 +3212,10 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager NetworkSceneHandle = NetworkSceneHandle, Hash = CheckForGlobalObjectIdHashOverride(), OwnerObject = this, - TargetClientId = targetClientId - }; + TargetClientId = targetClientId, + HasPreInstantiateData = preInstanceData.IsInitialized, + preInstanceData = preInstanceData + }; // Handle Parenting if (!AlwaysReplicateAsRoot && obj.HasParent) @@ -3215,6 +3279,7 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager /// The deserialized NetworkObject or null if deserialization failed internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false, byte[] customSpawnData = null) { + Debug.Log($"AddSceneObjects, it comes with a NetworkVariable reader, maybe i can send the data over there?. (also called for SceneObjects)"); //Attempt to create a local NetworkObject var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, customSpawnData); @@ -3245,8 +3310,9 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf // in order to be able to determine which NetworkVariables the client will be allowed to read. networkObject.OwnerClientId = sceneObject.OwnerClientId; - // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours - networkObject.InvokeBehaviourNetworkPreSpawn(); + networkObject.preInstanceData = sceneObject.preInstanceData; + // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours + networkObject.InvokeBehaviourNetworkPreSpawn(); // Synchronize NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index ab46a4465f..5fbc3c598a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Unity.Collections; +using UnityEngine; namespace Unity.Netcode { @@ -245,6 +246,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { + Debug.Log("Handling"); var networkManager = (NetworkManager)context.SystemOwner; if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index 33053fcaa7..f8b74b34f2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; +using UnityEngine; namespace Unity.Netcode { @@ -130,6 +131,7 @@ public void Serialize(FastBufferWriter writer, int targetVersion) } } + Debug.Log($"[{k_Name}][Serialize] {nameof(IncludesSerializedObject)}: {IncludesSerializedObject}, {nameof(UpdateObservers)}: {UpdateObservers}, {nameof(UpdateNewObservers)}: {UpdateNewObservers} preinstance data initialized? {ObjectInfo.preInstanceData.IsInitialized}"); if (IncludesSerializedObject) { ObjectInfo.Serialize(writer); @@ -189,6 +191,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int } } + Debug.Log($"[{k_Name}][Deserialize] {nameof(IncludesSerializedObject)}: {IncludesSerializedObject}, {nameof(UpdateObservers)}: {UpdateObservers}, {nameof(UpdateNewObservers)}: {UpdateNewObservers}"); if (IncludesSerializedObject) { ObjectInfo.Deserialize(reader); @@ -214,7 +217,8 @@ public void Handle(ref NetworkContext context) // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing if (networkManager.SceneManager.ShouldDeferCreateObject()) { - networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds, CustomSpawnData); + Debug.Log($"[{k_Name}][Defer] Deferring object creation for {NetworkObjectId} until scene synchronization is complete."); + networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); } else { @@ -225,7 +229,8 @@ public void Handle(ref NetworkContext context) NetworkObjectId = NetworkObjectId, }; } - CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds, CustomSpawnData); + Debug.Log($"[{k_Name}][Handle] Creating object {ObjectInfo.NetworkObjectId} for {context.SenderId} preinstance data initialized? {ObjectInfo.preInstanceData.IsInitialized}"); + CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); } } @@ -239,18 +244,18 @@ internal static void CreateObject(ref NetworkManager networkManager, ref Network var sceneObject = deferredObjectCreation.SceneObject; var networkVariableData = deferredObjectCreation.FastBufferReader; var customSpawnData = deferredObjectCreation.CustomSpawnData; - CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds, customSpawnData); + CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds, byte[] customSpawnData) + internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds) { var networkObject = (NetworkObject)null; try { if (!networkManager.DistributedAuthorityMode) { - networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, customSpawnData: customSpawnData); //TODO: Metadata could go into sceneObject structure, as its used more widely + networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager); //TODO: Metadata could go into sceneObject structure, as its used more widely } else { @@ -313,8 +318,6 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende ObserverIds = hasObserverIdList ? observerIds : null, NetworkObjectId = networkObject.NetworkObjectId, IncludesSerializedObject = true, - CustomSpawnData = customSpawnData, - HasCustomSpawnData = customSpawnData != null && customSpawnData.Length > 0, }; foreach (var clientId in clientList) { @@ -333,7 +336,7 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende // observers list to that client's instance. createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); - networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject, customSpawnData); + networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index a0ac3a7199..2ca00ea87b 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -3163,7 +3163,7 @@ internal struct DeferredObjectCreation internal int DeferredObjectCreationCount; // The added clientIds is specific to DAHost when session ownership changes and a normal client is controlling scene loading - internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader, ulong[] observerIds, ulong[] newObserverIds, byte[] customSpawnData) + internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader, ulong[] observerIds, ulong[] newObserverIds) { var deferredObjectCreationEntry = new DeferredObjectCreation() { @@ -3172,7 +3172,6 @@ internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject. ObserverIds = observerIds, NewObserverIds = newObserverIds, SceneObject = sceneObject, - CustomSpawnData = customSpawnData, }; unsafe diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs index b59a89e146..1a60509c45 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs @@ -1,7 +1,7 @@ namespace Unity.Netcode { - public interface INetworkCustomSpawnDataReceiver + public interface INetworkCustomSpawnDataSynchronizer { /// /// Called on the client side after receiving custom spawn metadata during the instantiation process. @@ -13,6 +13,6 @@ public interface INetworkCustomSpawnDataReceiver /// allowing you to cache or prepare information needed during instantiation. /// /// The metadata buffer sent from the server during the spawn message. - void OnCustomSpawnDataReceived(byte[] customSpawnData); + void OnSynchronize(ref BufferSerializer serializer) where T : IReaderWriter; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs.meta b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs.meta new file mode 100644 index 0000000000..3895ba444c --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9bf5119a47f8d3247aaa4cd13c1ee96b \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index 9b190045d3..0ce9f920e6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Unity.Collections; using UnityEngine; namespace Unity.Netcode @@ -256,17 +257,37 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) /// /// /// - internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] customSpawnData) - { + internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, ref BufferSerializer preInstanceDataSerializer, Vector3 position, Quaternion rotation) where T : IReaderWriter + { if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) { - if(customSpawnData != null && prefabInstanceHandler is INetworkCustomSpawnDataReceiver receiver) - receiver.OnCustomSpawnDataReceived(customSpawnData); - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); - - //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) - //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. - if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + if(prefabInstanceHandler is INetworkCustomSpawnDataSynchronizer synchronizer) + { + Debug.Log($"The sinchronizer will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); + synchronizer.OnSynchronize(ref preInstanceDataSerializer); + } + var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); + if (networkObjectInstance != null) + { + if (preInstanceDataSerializer.IsReader) + { + Debug.Log($"The reader will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); + networkObjectInstance.preInstanceData = preInstanceDataSerializer.GetFastBufferReader(); + } + else + { + Debug.Log($"The writer will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); + // Si es writer, creamos un Reader desde el Writer + unsafe + { + var writer = preInstanceDataSerializer.GetFastBufferWriter(); + networkObjectInstance.preInstanceData = new FastBufferReader(writer.GetUnsafePtr(), Allocator.Persistent, writer.Length); + } + } + } + //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) + //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. + if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) { m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index c9eb831e9f..f6d8ef022b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -703,7 +703,7 @@ internal enum InstantiateAndSpawnErrorTypes /// The starting poisiton of the instance. /// The starting rotation of the instance. /// The newly instantiated and spawned prefab instance. - public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default, byte[] customSpawnData = null) + public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { if (networkPrefab == null) { @@ -732,13 +732,13 @@ public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong owne return null; } - return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation, customSpawnData); + return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); } - + /// /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! /// - internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default, byte[] customSpanData = null) + internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { var networkObject = networkPrefab; // - Host and clients always instantiate the override if one exists. @@ -748,7 +748,11 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ // - Distributed authority mode always spawns the override if one exists. if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) { - networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation, customSpawnData: customSpanData); + Debug.Assert(networkPrefab.GlobalObjectIdHash != 0, $"The {nameof(NetworkObject)} prefab passed in does not have a valid {nameof(NetworkObject.GlobalObjectIdHash)} value!"); + Debug.Log("SENDING WRITERR, EN TEORIA AQUI SE PASA EL WRITTER PARA ESCRIBIERLO Y LUEGO HAY QUE SACARLE EL READER Y PASARLO POR UN MENSAJE"); + var bufferWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(20,Collections.Allocator.Temp))); + networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, ref bufferWriter, position, rotation); + } else // Under this case, server instantiate the prefab passed in. { @@ -766,11 +770,11 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ // If spawning as a player, then invoke SpawnAsPlayerObject if (isPlayerObject) { - networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene, customSpanData); + networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene); } else // Otherwise just spawn with ownership { - networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene, customSpanData); + networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); } return networkObject; } @@ -779,14 +783,15 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. /// - internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false, byte[] customSpawnData = null) + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, ref BufferSerializer preInstanceDataSerializer, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) where T : IReaderWriter { + NetworkObject networkObject = null; // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default, customSpawnData); + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, ref preInstanceDataSerializer, position ?? default, rotation ?? default); networkObject.NetworkManagerOwner = NetworkManager; } else @@ -888,10 +893,15 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO // If scene management is disabled or the NetworkObject was dynamically spawned if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) { - networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject, customSpawnData); + Debug.Log("Here is needed to check the handler has or not that custom data (DYNAMIC SPAWN)"); + Debug.Log("SENDING READER"); + Debug.Log($"SceneObject preInstanceData: {sceneObject.preInstanceData.IsInitialized.ToString()}"); + var bufferWriter = new BufferSerializer(new BufferSerializerReader(sceneObject.preInstanceData)); + networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, ref bufferWriter, position, rotation, sceneObject.IsSceneObject); } else // Get the in-scene placed NetworkObject { + Debug.Log("Here is needed to check the handler has or not that custom data (SCENE PLACED)"); networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); if (networkObject == null) { @@ -1260,7 +1270,8 @@ internal unsafe void UpdateNetworkObjectSceneChanges() } } - internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject, byte[] customSpawnData) + ///AAA + internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) { // If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message. var updateObservers = NetworkManager.DistributedAuthorityMode && networkObject.SpawnWithObservers; @@ -1277,12 +1288,9 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject IncludesSerializedObject = true, UpdateObservers = NetworkManager.DistributedAuthorityMode, ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, - CustomSpawnData = customSpawnData, - HasCustomSpawnData = customSpawnData != null && customSpawnData.Length > 0, }; - Debug.Log("customSpawnData:2 " + (customSpawnData?.Length ?? 0).ToString()); + Debug.Log($"Sending spawn call for object, parameters: ClientID {clientId} networkObject{networkObject} object has preInstanceData {networkObject.preInstanceData.IsInitialized.ToString()}"); var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); - Debug.Log("s: " + size); NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); } @@ -1791,7 +1799,7 @@ internal void HandleNetworkObjectShow() { try { - SendSpawnCallForObject(clientId, networkObject, null); + SendSpawnCallForObject(clientId, networkObject); } catch (Exception ex) { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs index 29db0d01bb..d93b68f20d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs @@ -110,7 +110,9 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) //Test result of registering via GameObject reference Assert.True(gameObjectRegistered); - var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); + var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + Debug.Log("SENDING Writter BUFFFER Tests"); + var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref bufferReaderSerializer, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -135,7 +137,9 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(2.0f, 1.0f, 5.0f); prefabRotation = new Quaternion(4.0f, 1.5f, 5.4f, 5.1f); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); + bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + Debug.Log("SENDING Writter BUFFFER Tests"); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0,ref bufferReaderSerializer, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -160,7 +164,9 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(6.0f, 4.0f, 1.0f); prefabRotation = new Quaternion(3f, 2f, 4f, 1f); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation, null); + bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + Debug.Log("SENDING Writter BUFFFER Tests"); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref bufferReaderSerializer, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); From e1350172e6567a7957c36fee0337de056c37d1f9 Mon Sep 17 00:00:00 2001 From: Extrys Date: Sun, 27 Apr 2025 01:52:43 +0200 Subject: [PATCH 6/7] Clean diff and symetrical approach working for new InstantiationPayload --- .../Connection/NetworkConnectionManager.cs | 12 +- .../Runtime/Core/NetworkObject.cs | 172 +- .../Runtime/Messaging/CustomMessageManager.cs | 16 +- .../Messages/ConnectionApprovedMessage.cs | 2 - .../Messaging/Messages/CreateObjectMessage.cs | 653 ++- .../SceneManagement/NetworkSceneManager.cs | 3 +- .../INetworkCustomSpawnDataReceiver.cs | 28 +- .../Runtime/Spawning/NetworkPrefabHandler.cs | 112 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 4450 ++++++++--------- .../Prefabs/NetworkPrefabHandlerTests.cs | 15 +- 10 files changed, 2690 insertions(+), 2773 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index b8d5a5f9bb..59e2164dd3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -797,10 +797,9 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne // Server-side spawning (only if there is a prefab hash or player prefab provided) if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) { - var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0,Allocator.Temp))); - Debug.Log("SENDING BUFFFER WRITERRR FOR PLAYER SPAWN"); - var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, ref bufferReaderSerializer, response.Position ?? null, response.Rotation ?? null) - : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, ref bufferReaderSerializer, response.Position ?? null, response.Rotation ?? null); + var instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0,Allocator.Temp))); + var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, ref instantiationPayloadWriter, response.Position ?? null, response.Rotation ?? null) + : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, ref instantiationPayloadWriter, response.Position ?? null, response.Rotation ?? null); // Spawn the player NetworkObject locally NetworkManager.SpawnManager.SpawnNetworkObjectLocally( @@ -952,9 +951,8 @@ internal void CreateAndSpawnPlayer(ulong ownerId) if (playerPrefab != null) { var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash; - var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Allocator.Temp))); - Debug.Log("SENDING BUFFFER WRITERRR FOR PLAYER SPAWN 2"); - var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, ref bufferReaderSerializer, playerPrefab.transform.position, playerPrefab.transform.rotation); + var instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Allocator.Temp))); + var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, ref instantiationPayloadWriter, playerPrefab.transform.position, playerPrefab.transform.rotation); networkObject.IsSceneObject = false; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 069fd04509..26d03b87ee 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -37,8 +37,6 @@ public sealed class NetworkObject : MonoBehaviour /// internal uint PrefabGlobalObjectIdHash; - internal FastBufferReader preInstanceData; - /// /// This is the source prefab of an in-scene placed NetworkObject. This is not set for in-scene /// placd NetworkObjects that are not prefab instances, dynamically spawned prefab instances, @@ -48,6 +46,13 @@ public sealed class NetworkObject : MonoBehaviour [SerializeField] internal uint InScenePlacedSourceGlobalObjectIdHash; + /// + /// Metadata sent during the instantiation process. + /// Retrieved in INetworkCustomSpawnDataSynchronizer before instantiation, + /// and available to INetworkPrefabInstanceHandler.Instantiate() for custom handling by user code. + /// + internal FastBufferReader InstantiationPayload; + /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 /// @@ -1826,8 +1831,7 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) { - Debug.Log("Spawn internal 1, object name " + gameObject.name); - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); } } } @@ -1837,8 +1841,7 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla // then we want to send a spawn notification. if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1) { - Debug.Log("Spawn internal 2, object name " + gameObject.name); - NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); } } else @@ -1847,20 +1850,20 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } } - /// - /// This invokes . - /// - /// The NetworkPrefab to instantiate and spawn. - /// The local instance of the NetworkManager connected to an session in progress. - /// The owner of the instance (defaults to server). - /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). - /// Whether the instance is a player object or not (default is false). - /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and - /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). - /// The starting poisiton of the instance. - /// The starting rotation of the instance. - /// The newly instantiated and spawned prefab instance. - public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + /// + /// This invokes . + /// + /// The NetworkPrefab to instantiate and spawn. + /// The local instance of the NetworkManager connected to an session in progress. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { var networkObject = networkPrefab.GetComponent(); if (networkObject == null) @@ -1941,12 +1944,12 @@ public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) SpawnInternal(destroyWithScene, clientId, false); } - /// - /// Spawns a across the network and makes it the player object for the given client - /// - /// The clientId who's player object this is - /// Should the object be destroyed when the scene is changed - public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) + /// + /// Spawns a across the network and makes it the player object for the given client + /// + /// The clientId who's player object this is + /// Should the object be destroyed when the scene is changed + public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) { SpawnInternal(destroyWithScene, clientId, true); } @@ -2817,7 +2820,7 @@ internal struct SceneObject public ulong NetworkObjectId; public ulong OwnerClientId; public ushort OwnershipFlags; - public FastBufferReader preInstanceData; + public FastBufferReader InstantiationPayload; public bool IsPlayerObject { @@ -2888,12 +2891,11 @@ public bool SpawnWithObservers set => ByteUtility.SetBit(ref m_BitField, 10, value); } - public bool HasPreInstantiateData - { - get => ByteUtility.GetBit(m_BitField, 11); - set => ByteUtility.SetBit(ref m_BitField, 11, value); - } - + public bool HasInstantiationPayload + { + get => ByteUtility.GetBit(m_BitField, 11); + set => ByteUtility.SetBit(ref m_BitField, 11, value); + } // When handling the initial synchronization of NetworkObjects, // this will be populated with the known observers. @@ -2935,8 +2937,6 @@ public void Serialize(FastBufferWriter writer) BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, OwnerClientId); - - if (HasParent) { BytePacker.WriteValueBitPacked(writer, ParentObjectId); @@ -2963,34 +2963,31 @@ public void Serialize(FastBufferWriter writer) var writeSize = 0; writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += FastBufferWriter.GetWriteSize(); - if (HasPreInstantiateData) - { - writeSize += FastBufferWriter.GetWriteSize(); - writeSize += preInstanceData.Length; - } - + if (HasInstantiationPayload) + { + writeSize += FastBufferWriter.GetWriteSize(); + writeSize += InstantiationPayload.Length; + } if (!writer.TryBeginWrite(writeSize)) { throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); } - - if (HasPreInstantiateData) - { - writer.WriteValueSafe(preInstanceData.Length); - Debug.Log($"PreInstanceData Length Serialize: {preInstanceData.Length}"); - unsafe - { - writer.WriteBytes(preInstanceData.GetUnsafePtr(), preInstanceData.Length); - } - } + + if (HasInstantiationPayload) + { + writer.WriteValueSafe(InstantiationPayload.Length); + unsafe + { + writer.WriteBytes(InstantiationPayload.GetUnsafePtr(), InstantiationPayload.Length); + } + } if (HasTransform) { writer.WriteValue(Transform); } - // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. if (OwnerObject.NetworkManager.DistributedAuthorityMode) @@ -3002,8 +2999,6 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } - - // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); @@ -3016,8 +3011,6 @@ public void Deserialize(FastBufferReader reader) ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId); ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId); - - if (HasParent) { ByteUnpacker.ReadValueBitPacked(reader, out ParentObjectId); @@ -3050,49 +3043,43 @@ public void Deserialize(FastBufferReader reader) readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += FastBufferWriter.GetWriteSize(); - int preInstanceDataSize = 0; - if (HasPreInstantiateData) - { - if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize())) - { - throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer (preInstanceData size)"); - } - - reader.ReadValueSafe(out preInstanceDataSize); - readSize += FastBufferWriter.GetWriteSize(); - readSize += preInstanceDataSize; - } + int preInstanceDataSize = 0; + if (HasInstantiationPayload) + { + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize())) + { + throw new OverflowException($"Could not deserialize SceneObject: Reading past the end of the buffer ({nameof(InstantiationPayload)} size)"); + } + reader.ReadValueSafe(out preInstanceDataSize); + readSize += FastBufferWriter.GetWriteSize(); + readSize += preInstanceDataSize; + } // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) { throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } - if (HasPreInstantiateData) - { - Debug.Log($"PreInstanceData Length des: {preInstanceDataSize}"); - unsafe - { - preInstanceData = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, preInstanceDataSize); - reader.Seek(reader.Position + preInstanceDataSize); - } - } + if (HasInstantiationPayload) + { + unsafe + { + InstantiationPayload = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, preInstanceDataSize); + reader.Seek(reader.Position + preInstanceDataSize); + } + } if (HasTransform) { reader.ReadValue(out Transform); } - - // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. reader.ReadValue(out NetworkSceneHandle); - - - } + } } internal void PostNetworkVariableWrite(bool forced = false) @@ -3213,9 +3200,9 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager Hash = CheckForGlobalObjectIdHashOverride(), OwnerObject = this, TargetClientId = targetClientId, - HasPreInstantiateData = preInstanceData.IsInitialized, - preInstanceData = preInstanceData - }; + HasInstantiationPayload = InstantiationPayload.IsInitialized, + InstantiationPayload = InstantiationPayload + }; // Handle Parenting if (!AlwaysReplicateAsRoot && obj.HasParent) @@ -3277,11 +3264,10 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager /// NetworkManager instance /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed - internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false, byte[] customSpawnData = null) + internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) { - Debug.Log($"AddSceneObjects, it comes with a NetworkVariable reader, maybe i can send the data over there?. (also called for SceneObjects)"); //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, customSpawnData); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); if (networkObject == null) { @@ -3310,9 +3296,13 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf // in order to be able to determine which NetworkVariables the client will be allowed to read. networkObject.OwnerClientId = sceneObject.OwnerClientId; - networkObject.preInstanceData = sceneObject.preInstanceData; - // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours - networkObject.InvokeBehaviourNetworkPreSpawn(); + // Even though the Instantiation Payload is typically consumed during the spawn message handling phase, + // we still assign it here to preserve the original spawn metadata for potential inspection, diagnostics, + // or in case future systems want to access it directly without relying on synchronization messages. + networkObject.InstantiationPayload = sceneObject.InstantiationPayload; + + // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours + networkObject.InvokeBehaviourNetworkPreSpawn(); // Synchronize NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs index 15e30205f8..815e38d35c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs @@ -180,18 +180,14 @@ internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader read // We dont know what size to use. Try every (more collision prone) if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) { - // handler can remove itself, cache the name for metrics - var messageName = m_MessageHandlerNameLookup32[hash]; messageHandler32(sender, reader); - m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount); } if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64)) { - // handler can remove itself, cache the name for metrics - var messageName = m_MessageHandlerNameLookup64[hash]; messageHandler64(sender, reader); - m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); } } else @@ -202,19 +198,15 @@ internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader read case HashSize.VarIntFourBytes: if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) { - // handler can remove itself, cache the name for metrics - var messageName = m_MessageHandlerNameLookup32[hash]; messageHandler32(sender, reader); - m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount); } break; case HashSize.VarIntEightBytes: if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64)) { - // handler can remove itself, cache the name for metrics - var messageName = m_MessageHandlerNameLookup64[hash]; messageHandler64(sender, reader); - m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); } break; } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 5fbc3c598a..ab46a4465f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Unity.Collections; -using UnityEngine; namespace Unity.Netcode { @@ -246,7 +245,6 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { - Debug.Log("Handling"); var networkManager = (NetworkManager)context.SystemOwner; if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index f8b74b34f2..02f4263e9d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -1,354 +1,311 @@ using System.Linq; using System.Runtime.CompilerServices; -using UnityEngine; namespace Unity.Netcode { - internal struct CreateObjectMessage : INetworkMessage - { - public int Version => 0; - - private const string k_Name = "CreateObjectMessage"; - - public NetworkObject.SceneObject ObjectInfo; - private FastBufferReader m_ReceivedNetworkVariableData; - public byte[] CustomSpawnData; - - // DA - NGO CMB SERVICE NOTES: - // The ObserverIds and ExistingObserverIds will only be populated if k_UpdateObservers is set - // ObserverIds is the full list of observers (see below) - internal ulong[] ObserverIds; - - // While this does consume a bit more bandwidth, this is only sent by the authority/owner - // and can be used to determine which clients should receive the ObjectInfo serialized data. - // All other already existing observers just need to receive the NewObserverIds and the - // NetworkObjectId - internal ulong[] NewObserverIds; - - // If !IncludesSerializedObject then the NetworkObjectId will be serialized. - // This happens when we are just sending an update to the observers list - // to clients that already have the NetworkObject spawned - internal ulong NetworkObjectId; - - private const byte k_IncludesSerializedObject = 0x01; - private const byte k_UpdateObservers = 0x02; - private const byte k_UpdateNewObservers = 0x04; - private const byte k_HasCreationMetadata = 0x08; - - - private byte m_CreateObjectMessageTypeFlags; - - internal bool IncludesSerializedObject - { - get - { - return GetFlag(k_IncludesSerializedObject); - } - - set - { - SetFlag(value, k_IncludesSerializedObject); - } - } - - internal bool UpdateObservers - { - get - { - return GetFlag(k_UpdateObservers); - } - - set - { - SetFlag(value, k_UpdateObservers); - } - } - - internal bool UpdateNewObservers - { - get - { - return GetFlag(k_UpdateNewObservers); - } - - set - { - SetFlag(value, k_UpdateNewObservers); - } - } - - internal bool HasCustomSpawnData - { - get - { - return GetFlag(k_HasCreationMetadata); - } - set - { - SetFlag(value, k_HasCreationMetadata); - } - } - - private bool GetFlag(int flag) - { - return (m_CreateObjectMessageTypeFlags & flag) != 0; - } - - private void SetFlag(bool set, byte flag) - { - if (set) { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags | flag); } - else { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags & ~flag); } - } - - public void Serialize(FastBufferWriter writer, int targetVersion) - { - writer.WriteValueSafe(m_CreateObjectMessageTypeFlags); - - if (HasCustomSpawnData) - { - BytePacker.WriteValuePacked(writer, CustomSpawnData.Length); - foreach (var mdByte in CustomSpawnData) - { - BytePacker.WriteValuePacked(writer, mdByte); - } - } - - if (UpdateObservers) - { - BytePacker.WriteValuePacked(writer, ObserverIds.Length); - foreach (var clientId in ObserverIds) - { - BytePacker.WriteValuePacked(writer, clientId); - } - } - - if (UpdateNewObservers) - { - BytePacker.WriteValuePacked(writer, NewObserverIds.Length); - foreach (var clientId in NewObserverIds) - { - BytePacker.WriteValuePacked(writer, clientId); - } - } - - Debug.Log($"[{k_Name}][Serialize] {nameof(IncludesSerializedObject)}: {IncludesSerializedObject}, {nameof(UpdateObservers)}: {UpdateObservers}, {nameof(UpdateNewObservers)}: {UpdateNewObservers} preinstance data initialized? {ObjectInfo.preInstanceData.IsInitialized}"); - if (IncludesSerializedObject) - { - ObjectInfo.Serialize(writer); - } - else - { - BytePacker.WriteValuePacked(writer, NetworkObjectId); - } - } - - public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) - { - var networkManager = (NetworkManager)context.SystemOwner; - if (!networkManager.IsClient) - { - return false; - } - - reader.ReadValueSafe(out m_CreateObjectMessageTypeFlags); - - if (HasCustomSpawnData) - { - var length = 0; - ByteUnpacker.ReadValuePacked(reader, out length); - CustomSpawnData = new byte[length]; - var clientId = (byte)0; - for (int i = 0; i < length; i++) - { - ByteUnpacker.ReadValuePacked(reader, out clientId); - CustomSpawnData[i] = clientId; - } - } - - if (UpdateObservers) - { - var length = 0; - ByteUnpacker.ReadValuePacked(reader, out length); - ObserverIds = new ulong[length]; - var clientId = (ulong)0; - for (int i = 0; i < length; i++) - { - ByteUnpacker.ReadValuePacked(reader, out clientId); - ObserverIds[i] = clientId; - } - } - - if (UpdateNewObservers) - { - var length = 0; - ByteUnpacker.ReadValuePacked(reader, out length); - NewObserverIds = new ulong[length]; - var clientId = (ulong)0; - for (int i = 0; i < length; i++) - { - ByteUnpacker.ReadValuePacked(reader, out clientId); - NewObserverIds[i] = clientId; - } - } - - Debug.Log($"[{k_Name}][Deserialize] {nameof(IncludesSerializedObject)}: {IncludesSerializedObject}, {nameof(UpdateObservers)}: {UpdateObservers}, {nameof(UpdateNewObservers)}: {UpdateNewObservers}"); - if (IncludesSerializedObject) - { - ObjectInfo.Deserialize(reader); - } - else - { - ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); - } - - if (!networkManager.NetworkConfig.ForceSamePrefabs && !networkManager.SpawnManager.HasPrefab(ObjectInfo)) - { - networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, ObjectInfo.Hash, reader, ref context, k_Name); - return false; - } - m_ReceivedNetworkVariableData = reader; - - return true; - } - - public void Handle(ref NetworkContext context) - { - var networkManager = (NetworkManager)context.SystemOwner; - // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing - if (networkManager.SceneManager.ShouldDeferCreateObject()) - { - Debug.Log($"[{k_Name}][Defer] Deferring object creation for {NetworkObjectId} until scene synchronization is complete."); - networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); - } - else - { - if (networkManager.DistributedAuthorityMode && !IncludesSerializedObject && UpdateObservers) - { - ObjectInfo = new NetworkObject.SceneObject() - { - NetworkObjectId = NetworkObjectId, - }; - } - Debug.Log($"[{k_Name}][Handle] Creating object {ObjectInfo.NetworkObjectId} for {context.SenderId} preinstance data initialized? {ObjectInfo.preInstanceData.IsInitialized}"); - CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void CreateObject(ref NetworkManager networkManager, ref NetworkSceneManager.DeferredObjectCreation deferredObjectCreation) - { - var senderId = deferredObjectCreation.SenderId; - var observerIds = deferredObjectCreation.ObserverIds; - var newObserverIds = deferredObjectCreation.NewObserverIds; - var messageSize = deferredObjectCreation.MessageSize; - var sceneObject = deferredObjectCreation.SceneObject; - var networkVariableData = deferredObjectCreation.FastBufferReader; - var customSpawnData = deferredObjectCreation.CustomSpawnData; - CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds) - { - var networkObject = (NetworkObject)null; - try - { - if (!networkManager.DistributedAuthorityMode) - { - networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager); //TODO: Metadata could go into sceneObject structure, as its used more widely - } - else - { - var hasObserverIdList = observerIds != null && observerIds.Length > 0; - var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; - // Depending upon visibility of the NetworkObject and the client in question, it could be that - // this client already has visibility of this NetworkObject - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(sceneObject.NetworkObjectId)) - { - // If so, then just get the local instance - networkObject = networkManager.SpawnManager.SpawnedObjects[sceneObject.NetworkObjectId]; - - // This should not happen, logging error just in case - if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) - { - NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {sceneObject.NetworkObjectId}!"); - } - else // Trap to make sure the owner is not receiving any messages it sent - if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) - { - NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{sceneObject.NetworkObjectId}!"); - } - } - else - { - networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, true); - } - - // DA - NGO CMB SERVICE NOTES: - // It is possible for two clients to connect at the exact same time which, due to client-side spawning, can cause each client - // to miss their spawns. For now, all player NetworkObject spawns will always be visible to all known connected clients - var clientList = hasObserverIdList && !networkObject.IsPlayerObject ? observerIds : networkManager.ConnectedClientsIds; - - // Update the observers for this instance - foreach (var clientId in clientList) - { - networkObject.Observers.Add(clientId); - } - - // Mock CMB Service and forward to all clients - if (networkManager.DAHost) - { - // DA - NGO CMB SERVICE NOTES: - // (*** See above notes fist ***) - // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively - // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, - // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each - // player's AOI. - if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) - { - // For same-frame newly spawned players that might not be aware of all other players, update the player's observer - // list. - observerIds = clientList.ToArray(); - } - - var createObjectMessage = new CreateObjectMessage() - { - ObjectInfo = sceneObject, - m_ReceivedNetworkVariableData = networkVariableData, - ObserverIds = hasObserverIdList ? observerIds : null, - NetworkObjectId = networkObject.NetworkObjectId, - IncludesSerializedObject = true, - }; - foreach (var clientId in clientList) - { - // DA - NGO CMB SERVICE NOTES: - // If the authority did not specify the list of clients and the client is not an observer or we are the owner/originator - // or we are the DAHost, then we skip sending the message. - if ((!hasObserverIdList && (!networkObject.Observers.Contains(clientId)) || - clientId == networkObject.OwnerClientId || clientId == NetworkManager.ServerClientId)) - { - continue; - } - - // DA - NGO CMB SERVICE NOTES: - // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. - // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated - // observers list to that client's instance. - createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); - - networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); - } - } - } - if (networkObject != null) - { - networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); - } - } - catch (System.Exception ex) - { - UnityEngine.Debug.LogException(ex); - } - } - } + internal struct CreateObjectMessage : INetworkMessage + { + public int Version => 0; + + private const string k_Name = "CreateObjectMessage"; + + public NetworkObject.SceneObject ObjectInfo; + private FastBufferReader m_ReceivedNetworkVariableData; + + // DA - NGO CMB SERVICE NOTES: + // The ObserverIds and ExistingObserverIds will only be populated if k_UpdateObservers is set + // ObserverIds is the full list of observers (see below) + internal ulong[] ObserverIds; + + // While this does consume a bit more bandwidth, this is only sent by the authority/owner + // and can be used to determine which clients should receive the ObjectInfo serialized data. + // All other already existing observers just need to receive the NewObserverIds and the + // NetworkObjectId + internal ulong[] NewObserverIds; + + // If !IncludesSerializedObject then the NetworkObjectId will be serialized. + // This happens when we are just sending an update to the observers list + // to clients that already have the NetworkObject spawned + internal ulong NetworkObjectId; + + private const byte k_IncludesSerializedObject = 0x01; + private const byte k_UpdateObservers = 0x02; + private const byte k_UpdateNewObservers = 0x04; + + + private byte m_CreateObjectMessageTypeFlags; + + internal bool IncludesSerializedObject + { + get + { + return GetFlag(k_IncludesSerializedObject); + } + + set + { + SetFlag(value, k_IncludesSerializedObject); + } + } + + internal bool UpdateObservers + { + get + { + return GetFlag(k_UpdateObservers); + } + + set + { + SetFlag(value, k_UpdateObservers); + } + } + + internal bool UpdateNewObservers + { + get + { + return GetFlag(k_UpdateNewObservers); + } + + set + { + SetFlag(value, k_UpdateNewObservers); + } + } + + private bool GetFlag(int flag) + { + return (m_CreateObjectMessageTypeFlags & flag) != 0; + } + + private void SetFlag(bool set, byte flag) + { + if (set) { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags | flag); } + else { m_CreateObjectMessageTypeFlags = (byte)(m_CreateObjectMessageTypeFlags & ~flag); } + } + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + writer.WriteValueSafe(m_CreateObjectMessageTypeFlags); + + if (UpdateObservers) + { + BytePacker.WriteValuePacked(writer, ObserverIds.Length); + foreach (var clientId in ObserverIds) + { + BytePacker.WriteValuePacked(writer, clientId); + } + } + + if (UpdateNewObservers) + { + BytePacker.WriteValuePacked(writer, NewObserverIds.Length); + foreach (var clientId in NewObserverIds) + { + BytePacker.WriteValuePacked(writer, clientId); + } + } + + if (IncludesSerializedObject) + { + ObjectInfo.Serialize(writer); + } + else + { + BytePacker.WriteValuePacked(writer, NetworkObjectId); + } + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return false; + } + + reader.ReadValueSafe(out m_CreateObjectMessageTypeFlags); + if (UpdateObservers) + { + var length = 0; + ByteUnpacker.ReadValuePacked(reader, out length); + ObserverIds = new ulong[length]; + var clientId = (ulong)0; + for (int i = 0; i < length; i++) + { + ByteUnpacker.ReadValuePacked(reader, out clientId); + ObserverIds[i] = clientId; + } + } + + if (UpdateNewObservers) + { + var length = 0; + ByteUnpacker.ReadValuePacked(reader, out length); + NewObserverIds = new ulong[length]; + var clientId = (ulong)0; + for (int i = 0; i < length; i++) + { + ByteUnpacker.ReadValuePacked(reader, out clientId); + NewObserverIds[i] = clientId; + } + } + + if (IncludesSerializedObject) + { + ObjectInfo.Deserialize(reader); + } + else + { + ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); + } + + if (!networkManager.NetworkConfig.ForceSamePrefabs && !networkManager.SpawnManager.HasPrefab(ObjectInfo)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, ObjectInfo.Hash, reader, ref context, k_Name); + return false; + } + m_ReceivedNetworkVariableData = reader; + + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing + if (networkManager.SceneManager.ShouldDeferCreateObject()) + { + networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); + } + else + { + if (networkManager.DistributedAuthorityMode && !IncludesSerializedObject && UpdateObservers) + { + ObjectInfo = new NetworkObject.SceneObject() + { + NetworkObjectId = NetworkObjectId, + }; + } + CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData, ObserverIds, NewObserverIds); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateObject(ref NetworkManager networkManager, ref NetworkSceneManager.DeferredObjectCreation deferredObjectCreation) + { + var senderId = deferredObjectCreation.SenderId; + var observerIds = deferredObjectCreation.ObserverIds; + var newObserverIds = deferredObjectCreation.NewObserverIds; + var messageSize = deferredObjectCreation.MessageSize; + var sceneObject = deferredObjectCreation.SceneObject; + var networkVariableData = deferredObjectCreation.FastBufferReader; + CreateObject(ref networkManager, senderId, messageSize, sceneObject, networkVariableData, observerIds, newObserverIds); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData, ulong[] observerIds, ulong[] newObserverIds) + { + var networkObject = (NetworkObject)null; + try + { + if (!networkManager.DistributedAuthorityMode) + { + networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager); + } + else + { + var hasObserverIdList = observerIds != null && observerIds.Length > 0; + var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; + // Depending upon visibility of the NetworkObject and the client in question, it could be that + // this client already has visibility of this NetworkObject + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(sceneObject.NetworkObjectId)) + { + // If so, then just get the local instance + networkObject = networkManager.SpawnManager.SpawnedObjects[sceneObject.NetworkObjectId]; + + // This should not happen, logging error just in case + if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) + { + NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {sceneObject.NetworkObjectId}!"); + } + else // Trap to make sure the owner is not receiving any messages it sent + if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) + { + NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{sceneObject.NetworkObjectId}!"); + } + } + else + { + networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager, true); + } + + // DA - NGO CMB SERVICE NOTES: + // It is possible for two clients to connect at the exact same time which, due to client-side spawning, can cause each client + // to miss their spawns. For now, all player NetworkObject spawns will always be visible to all known connected clients + var clientList = hasObserverIdList && !networkObject.IsPlayerObject ? observerIds : networkManager.ConnectedClientsIds; + + // Update the observers for this instance + foreach (var clientId in clientList) + { + networkObject.Observers.Add(clientId); + } + + // Mock CMB Service and forward to all clients + if (networkManager.DAHost) + { + // DA - NGO CMB SERVICE NOTES: + // (*** See above notes fist ***) + // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively + // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, + // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each + // player's AOI. + if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) + { + // For same-frame newly spawned players that might not be aware of all other players, update the player's observer + // list. + observerIds = clientList.ToArray(); + } + + var createObjectMessage = new CreateObjectMessage() + { + ObjectInfo = sceneObject, + m_ReceivedNetworkVariableData = networkVariableData, + ObserverIds = hasObserverIdList ? observerIds : null, + NetworkObjectId = networkObject.NetworkObjectId, + IncludesSerializedObject = true, + }; + foreach (var clientId in clientList) + { + // DA - NGO CMB SERVICE NOTES: + // If the authority did not specify the list of clients and the client is not an observer or we are the owner/originator + // or we are the DAHost, then we skip sending the message. + if ((!hasObserverIdList && (!networkObject.Observers.Contains(clientId)) || + clientId == networkObject.OwnerClientId || clientId == NetworkManager.ServerClientId)) + { + continue; + } + + // DA - NGO CMB SERVICE NOTES: + // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. + // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated + // observers list to that client's instance. + createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); + + networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); + } + } + } + if (networkObject != null) + { + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); + } + } + catch (System.Exception ex) + { + UnityEngine.Debug.LogException(ex); + } + } + } } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 2ca00ea87b..331d5ff112 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -3154,7 +3154,6 @@ internal struct DeferredObjectCreation // When we transfer session owner and we are using a DAHost, this will be pertinent (otherwise it is not when connected to a DA service) internal ulong[] ObserverIds; internal ulong[] NewObserverIds; - internal byte[] CustomSpawnData; internal NetworkObject.SceneObject SceneObject; internal FastBufferReader FastBufferReader; } @@ -3172,7 +3171,7 @@ internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject. ObserverIds = observerIds, NewObserverIds = newObserverIds, SceneObject = sceneObject, - }; + }; unsafe { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs index 1a60509c45..d496dbb935 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkCustomSpawnDataReceiver.cs @@ -1,18 +1,20 @@ - namespace Unity.Netcode { - public interface INetworkCustomSpawnDataSynchronizer + /// + /// Interface for synchronizing custom instantiation payloads during NetworkObject spawning. + /// Used alongside to extend instantiation behavior. + /// + public interface INetworkInstantiationPayloadSynchronizer { - /// - /// Called on the client side after receiving custom spawn metadata during the instantiation process. - /// This extends the interface to allow for custom spawn data handling. - /// This method is used to pass additional data from the server to the client to help identify or configure - /// the local object instance that should be linked to the spawned NetworkObject. - /// - /// This is invoked just before is called, - /// allowing you to cache or prepare information needed during instantiation. - /// - /// The metadata buffer sent from the server during the spawn message. - void OnSynchronize(ref BufferSerializer serializer) where T : IReaderWriter; + /// + /// Provides a method for synchronizing instantiation payload data during the spawn process. + /// Extends to allow passing additional data prior to instantiation + /// to help identify or configure the local object instance that should be linked to the spawned NetworkObject. + /// + /// This method is invoked immediately before is called, + /// allowing you to cache or prepare information needed during instantiation. + /// + /// The buffer serializer used to read or write custom instantiation data. + void OnSynchronize(ref BufferSerializer serializer) where T : IReaderWriter; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index 0ce9f920e6..723db0720e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Unity.Collections; using UnityEngine; namespace Unity.Netcode @@ -11,26 +10,22 @@ namespace Unity.Netcode /// public interface INetworkPrefabInstanceHandler { - /// - /// Client Side Only - /// Once an implementation is registered with the , this method will be called every time - /// a Network Prefab associated is spawned on clients

- /// - /// Note On Hosts: Use the - /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client.

- /// - /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active - /// via the method.

- /// - ///Note on Custom Spawn Data: If you want to send custom data to be processed during this spawn, you can do so by implementing - /// via the method. - ///
- /// the owner for the to be instantiated - /// the initial/default position for the to be instantiated - /// the initial/default rotation for the to be instantiated - /// a byte array of custom data sent during the spawn to be instantiated - /// The instantiated NetworkObject instance. Returns null if instantiation fails. - NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); + /// + /// Client Side Only + /// Once an implementation is registered with the , this method will be called every time + /// a Network Prefab associated is spawned on clients + /// + /// Note On Hosts: Use the + /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client. + /// + /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active + /// via the method. + /// + /// the owner for the to be instantiated + /// the initial/default position for the to be instantiated + /// the initial/default rotation for the to be instantiated + /// The instantiated NetworkObject instance. Returns null if instantiation fails. + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); /// /// Invoked on Client and Server @@ -49,11 +44,11 @@ public interface INetworkPrefabInstanceHandler void Destroy(NetworkObject networkObject); } - /// - /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) - /// Register custom prefab handlers by implementing the interface. - /// - public class NetworkPrefabHandler + /// + /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) + /// Register custom prefab handlers by implementing the interface. + /// + public class NetworkPrefabHandler { private NetworkManager m_NetworkManager; @@ -258,36 +253,35 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) /// /// internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, ref BufferSerializer preInstanceDataSerializer, Vector3 position, Quaternion rotation) where T : IReaderWriter - { + { if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) { - if(prefabInstanceHandler is INetworkCustomSpawnDataSynchronizer synchronizer) - { - Debug.Log($"The sinchronizer will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); - synchronizer.OnSynchronize(ref preInstanceDataSerializer); - } - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); - if (networkObjectInstance != null) - { - if (preInstanceDataSerializer.IsReader) - { - Debug.Log($"The reader will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); - networkObjectInstance.preInstanceData = preInstanceDataSerializer.GetFastBufferReader(); - } - else - { - Debug.Log($"The writer will instantiate, check custom spawn data being called from {(NetworkManager.Singleton.IsServer ? "Server" : "client")}"); - // Si es writer, creamos un Reader desde el Writer - unsafe - { - var writer = preInstanceDataSerializer.GetFastBufferWriter(); - networkObjectInstance.preInstanceData = new FastBufferReader(writer.GetUnsafePtr(), Allocator.Persistent, writer.Length); - } - } - } - //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) - //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. - if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + if (prefabInstanceHandler is INetworkInstantiationPayloadSynchronizer synchronizer) + { + synchronizer.OnSynchronize(ref preInstanceDataSerializer); + } + var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); + if (networkObjectInstance != null) + { + if (preInstanceDataSerializer.IsReader) + { + networkObjectInstance.InstantiationPayload = preInstanceDataSerializer.GetFastBufferReader(); + } + else + { + var writer = preInstanceDataSerializer.GetFastBufferWriter(); + if (writer.Length > 0) + { + unsafe + { + networkObjectInstance.InstantiationPayload = new FastBufferReader(writer.GetUnsafePtr(), Collections.Allocator.Persistent, writer.Length); + } + } + } + } + //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) + //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. + if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) { m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); } @@ -298,11 +292,11 @@ internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, return null; } - /// - /// Will invoke the implementation's Destroy method - /// - /// - internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) + /// + /// Will invoke the implementation's Destroy method + /// + /// + internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) { var networkObjectInstanceHash = networkObjectInstance.GlobalObjectIdHash; diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index f6d8ef022b..dc4740c693 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -7,2252 +7,2242 @@ namespace Unity.Netcode { - /// - /// Class that handles object spawning - /// - public class NetworkSpawnManager - { - // Stores the objects that need to be shown at end-of-frame - internal Dictionary> ObjectsToShowToClient = new Dictionary>(); - - internal Dictionary> ClientsToShowObject = new Dictionary>(); - - /// - /// The currently spawned objects - /// - public readonly Dictionary SpawnedObjects = new Dictionary(); - - /// - /// A list of the spawned objects - /// - public readonly HashSet SpawnedObjectsList = new HashSet(); - - /// - /// Use to get all NetworkObjects owned by a client - /// Ownership to Objects Table Format: - /// [ClientId][NetworkObjectId][NetworkObject] - /// Server: Keeps track of all clients' ownership - /// Client: Keeps track of only its ownership - /// - public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); - - /// - /// Object to Ownership Table: - /// [NetworkObjectId][ClientId] - /// Used internally to find the client Id that currently owns - /// the NetworkObject - /// - private Dictionary m_ObjectToOwnershipTable = new Dictionary(); - - /// - /// In distributed authority mode, a list of known spawned player NetworkObject instance is maintained by each client. - /// - public IReadOnlyList PlayerObjects => m_PlayerObjects; - // Since NetworkSpawnManager is destroyed when NetworkManager shuts down, it will always be an empty list for each new network session. - // DANGO-TODO: We need to add something like a ConnectionStateMessage that is sent by either the DAHost or CMBService to each client when a client - // is connected and synchronized or when a cient disconnects (but the player objects list we should keep as it is useful to have). - private List m_PlayerObjects = new List(); - - private Dictionary> m_PlayerObjectsTable = new Dictionary>(); - - /// - /// Returns the connect client identifiers - /// - /// connected client identifier list - public List GetConnectedPlayers() - { - return m_PlayerObjectsTable.Keys.ToList(); - } - - /// - /// Adds a player object and updates all other players' observers list - /// - private void AddPlayerObject(NetworkObject playerObject) - { - if (!playerObject.IsPlayerObject) - { - if (NetworkManager.LogLevel == LogLevel.Normal) - { - NetworkLog.LogError($"Attempting to register a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); - return; - } - } - - foreach (var player in m_PlayerObjects) - { - // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer. - if (player.SpawnWithObservers) - { - player.Observers.Add(playerObject.OwnerClientId); - } - - // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object. - if (playerObject.SpawnWithObservers) - { - playerObject.Observers.Add(player.OwnerClientId); - } - } - - // Only if spawn with observers is set or we are using a distributed authority network topology and this is the client's player should we add - // the owner as an observer. - if (playerObject.SpawnWithObservers || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == playerObject.OwnerClientId)) - { - playerObject.Observers.Add(playerObject.OwnerClientId); - } - - m_PlayerObjects.Add(playerObject); - if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) - { - m_PlayerObjectsTable.Add(playerObject.OwnerClientId, new List()); - } - m_PlayerObjectsTable[playerObject.OwnerClientId].Add(playerObject); - } - - internal void UpdateNetworkClientPlayer(NetworkObject playerObject) - { - // If the player's client does not already have a NetworkClient entry - if (!NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId)) - { - // Add the player's client - NetworkManager.ConnectionManager.AddClient(playerObject.OwnerClientId); - } - var playerNetworkClient = NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId]; - - // If a client changes their player object, then we should adjust for the client's new player - if (playerNetworkClient.PlayerObject != null && m_PlayerObjects.Contains(playerNetworkClient.PlayerObject)) - { - // Just remove the previous player object but keep the assigned observers of the NetworkObject - RemovePlayerObject(playerNetworkClient.PlayerObject); - } - - // Now update the associated NetworkClient's player object - NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].AssignPlayerObject(ref playerObject); - AddPlayerObject(playerObject); - } - - /// - /// Removes a player object and updates all other players' observers list - /// - private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObject = false) - { - if (!playerObject.IsPlayerObject) - { - if (NetworkManager.LogLevel == LogLevel.Normal) - { - NetworkLog.LogError($"Attempting to deregister a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); - return; - } - } - playerObject.IsPlayerObject = false; - m_PlayerObjects.Remove(playerObject); - if (m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) - { - m_PlayerObjectsTable[playerObject.OwnerClientId].Remove(playerObject); - if (m_PlayerObjectsTable[playerObject.OwnerClientId].Count == 0) - { - m_PlayerObjectsTable.Remove(playerObject.OwnerClientId); - } - } - - if (NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId) && destroyingObject) - { - NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null; - } - } - - internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId) - { - if (!ObjectsToShowToClient.ContainsKey(clientId)) - { - ObjectsToShowToClient.Add(clientId, new List()); - } - ObjectsToShowToClient[clientId].Add(networkObject); - if (NetworkManager.DistributedAuthorityMode) - { - if (!ClientsToShowObject.ContainsKey(networkObject)) - { - ClientsToShowObject.Add(networkObject, new List()); - } - ClientsToShowObject[networkObject].Add(clientId); - } - } - - // returns whether any matching objects would have become visible and were returned to hidden state - internal bool RemoveObjectFromShowingTo(NetworkObject networkObject, ulong clientId) - { - if (NetworkManager.DistributedAuthorityMode) - { - if (ClientsToShowObject.ContainsKey(networkObject)) - { - ClientsToShowObject[networkObject].Remove(clientId); - if (ClientsToShowObject[networkObject].Count == 0) - { - ClientsToShowObject.Remove(networkObject); - } - } - } - var ret = false; - if (!ObjectsToShowToClient.ContainsKey(clientId)) - { - return false; - } - - // probably overkill, but deals with multiple entries - while (ObjectsToShowToClient[clientId].Contains(networkObject)) - { - Debug.LogWarning( - "Object was shown and hidden from the same client in the same Network frame. As a result, the client will _not_ receive a NetworkSpawn"); - ObjectsToShowToClient[clientId].Remove(networkObject); - ret = true; - } - - if (ret) - { - networkObject.Observers.Remove(clientId); - } - - return ret; - } - - /// - /// Used to update a NetworkObject's ownership - /// - internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false) - { - var previousOwner = newOwner; - - // Use internal lookup table to see if the NetworkObject has a previous owner - if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId)) - { - // Keep track of the previous owner's ClientId - previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId]; - - // We are either despawning (remove) or changing ownership (assign) - if (isRemoving) - { - m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId); - } - else - { - // If we already had this owner in our table then just exit - if (NetworkManager.DistributedAuthorityMode && previousOwner == newOwner) - { - return; - } - m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner; - } - } - else - { - // Otherwise, just add a new lookup entry - m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner); - } - - // Check to see if we had a previous owner - if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner)) - { - // Before updating the previous owner, assure this entry exists - if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId)) - { - // Remove the previous owner's entry - OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); - - // If we are removing the entry (i.e. despawning or client lost ownership) - if (isRemoving) - { - return; - } - } - else - { - // Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen - throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?"); - } - } - - // If the owner doesn't have an entry then create one - if (!OwnershipToObjectsTable.ContainsKey(newOwner)) - { - OwnershipToObjectsTable.Add(newOwner, new Dictionary()); - } - - // Sanity check to make sure we don't already have this entry (we shouldn't) - if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId)) - { - // Add the new ownership entry - OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject); - } - else if (isRemoving) - { - OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); - } - else if (NetworkManager.LogLevel == LogLevel.Developer && previousOwner == newOwner) - { - NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!"); - } - } - - /// - /// Returns an array of all NetworkObjects that belong to a client. - /// - /// the client's id - /// returns an array of the s owned by the client - public NetworkObject[] GetClientOwnedObjects(ulong clientId) - { - if (!OwnershipToObjectsTable.ContainsKey(clientId)) - { - OwnershipToObjectsTable.Add(clientId, new Dictionary()); - } - return OwnershipToObjectsTable[clientId].Values.ToArray(); - } - - /// - /// Gets the NetworkManager associated with this SpawnManager. - /// - public NetworkManager NetworkManager { get; } - - internal readonly Queue ReleasedNetworkObjectIds = new Queue(); - private ulong m_NetworkObjectIdCounter; - - // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations - private List m_TargetClientIds = new List(); - - internal ulong GetNetworkObjectId() - { - if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) - { - return ReleasedNetworkObjectIds.Dequeue().NetworkId; - } - - m_NetworkObjectIdCounter++; - - // DANGO-TODO: Need a more robust solution here. - return m_NetworkObjectIdCounter + (NetworkManager.LocalClientId * 10000); - } - - /// - /// Returns the local player object or null if one does not exist - /// - /// The local player object or null if one does not exist - public NetworkObject GetLocalPlayerObject() - { - return GetPlayerNetworkObject(NetworkManager.LocalClientId); - } - - /// - /// Returns all instances assigned to the client identifier - /// - /// the client identifier of the player - /// A list of instances (if more than one are assigned) - public List GetPlayerNetworkObjects(ulong clientId) - { - if (m_PlayerObjectsTable.ContainsKey(clientId)) - { - return m_PlayerObjectsTable[clientId]; - } - return null; - } - - /// - /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. - /// - /// the client identifier of the player - /// The player object with a given clientId or null if one does not exist - public NetworkObject GetPlayerNetworkObject(ulong clientId) - { - if (!NetworkManager.DistributedAuthorityMode) - { - if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId) - { - throw new NotServerException("Only the server can find player objects from other clients."); - } - if (TryGetNetworkClient(clientId, out NetworkClient networkClient)) - { - return networkClient.PlayerObject; - } - } - else - { - if (m_PlayerObjectsTable.ContainsKey(clientId)) - { - return m_PlayerObjectsTable[clientId].First(); - } - } - - return null; - } - - /// - /// Helper function to get a network client for a clientId from the NetworkManager. - /// On the server this will check the list. - /// On a non-server this will check the only. - /// - /// The clientId for which to try getting the NetworkClient for. - /// The found NetworkClient. Null if no client was found. - /// True if a NetworkClient with a matching id was found else false. - private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient) - { - if (NetworkManager.IsServer) - { - return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); - } - - if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId) - { - networkClient = NetworkManager.LocalClient; - return true; - } - - networkClient = null; - return false; - } - - /// - /// Not used. - /// - /// not used - /// not used - protected virtual void InternalOnOwnershipChanged(ulong perviousOwner, ulong newOwner) - { - - } - - internal void RemoveOwnership(NetworkObject networkObject) - { - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.ShutdownInProgress) - { - Debug.LogError($"Removing ownership is invalid in Distributed Authority Mode. Use {nameof(ChangeOwnership)} instead."); - return; - } - ChangeOwnership(networkObject, NetworkManager.ServerClientId, true); - } - - private Dictionary m_LastChangeInOwnership = new Dictionary(); - private const int k_MaximumTickOwnershipChangeMultiplier = 6; - - internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false) - { - if (clientId == networkObject.OwnerClientId) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)"); - } - return; - } - - // For client-server: - // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes, - // then notify the user they could potentially lose state updates if developer logging is enabled. - if (NetworkManager.LogLevel == LogLevel.Developer && !NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup) - { - for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) - { - if (networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0) - { - NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss."); - break; - } - } - } - - if (NetworkManager.DistributedAuthorityMode) - { - // Ensure only the session owner can change ownership (i.e. acquire) and that the session owner is not trying to assign a non-session owner client - // ownership of a NetworkObject with SessionOwner permissions. - if (networkObject.IsOwnershipSessionOwner && (!NetworkManager.LocalClient.IsSessionOwner || clientId != NetworkManager.CurrentSessionOwner)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Session Owner Only] You cannot change ownership of a {nameof(NetworkObject)} that has the {NetworkObject.OwnershipStatus.SessionOwner} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.SessionOwnerOnly); - return; - } - - // If are not authorized and this is not an approved ownership change, then check to see if we can change ownership - if (!isAuthorized && !isRequestApproval) - { - if (networkObject.IsOwnershipLocked) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Locked] You cannot change ownership while a {nameof(NetworkObject)} is locked!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.Locked); - return; - } - if (networkObject.IsRequestInProgress) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Request Pending] You cannot change ownership while a {nameof(NetworkObject)} has a pending ownership request!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestInProgress); - return; - } - if (networkObject.IsOwnershipRequestRequired) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Request Required] You cannot change ownership directly if a {nameof(NetworkObject)} has the {NetworkObject.OwnershipStatus.RequestRequired} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired); - return; - } - if (!networkObject.IsOwnershipTransferable) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"[{networkObject.name}][Not transferrable] You cannot change ownership of a {nameof(NetworkObject)} that does not have the {NetworkObject.OwnershipStatus.Transferable} flag set!"); - } - networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.NotTransferrable); - return; - } - } - } - else if (!isAuthorized) - { - throw new NotServerException("Only the server can change ownership"); - } - - if (!networkObject.IsSpawned) - { - throw new SpawnStateException("Object is not spawned"); - } - - if (networkObject.OwnerClientId == clientId && networkObject.PreviousOwnerId == clientId) - { - if (NetworkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarningServer($"[Already Owner] Unnecessary ownership change for {networkObject.name} as it is already the owned by client-{clientId}"); - } - return; - } - - if (!networkObject.Observers.Contains(clientId)) - { - if (NetworkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarningServer($"[Invalid Owner] Cannot send Ownership change as client-{clientId} cannot see {networkObject.name}! Use {nameof(NetworkObject.NetworkShow)} first."); - } - return; - } - - // Save the previous owner to work around a bit of a nasty issue with assuring NetworkVariables are serialized when ownership changes - var originalPreviousOwnerId = networkObject.PreviousOwnerId; - var originalOwner = networkObject.OwnerClientId; - - // Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates - networkObject.PreviousOwnerId = networkObject.OwnerClientId; - - // Assign the new owner - networkObject.OwnerClientId = clientId; - - // Always notify locally on the server when ownership is lost - networkObject.InvokeBehaviourOnLostOwnership(); - - // Authority adds entries for all client ownership - UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); - - // Always notify locally on the server when a new owner is assigned - networkObject.InvokeBehaviourOnGainedOwnership(); - - // If we are the original owner, then we want to synchronize owner read & write NetworkVariables. - if (originalOwner == NetworkManager.LocalClientId) - { - networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId); - } - - var size = 0; - if (NetworkManager.DistributedAuthorityMode) - { - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId, - DistributedAuthorityMode = NetworkManager.DistributedAuthorityMode, - RequestApproved = isRequestApproval, - OwnershipIsChanging = true, - RequestClientId = networkObject.PreviousOwnerId, - OwnershipFlags = (ushort)networkObject.Ownership, - }; - // If we are connected to the CMB service or not the DAHost (i.e. pure DA-Clients only) - if (NetworkManager.CMBServiceConnection || !NetworkManager.DAHost) - { - // Always update the network properties in distributed authority mode for the client gaining ownership - for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) - { - networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties(); - } - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(NetworkManager.LocalClientId, networkObject, size); - } - else // We are the DAHost so broadcast the ownership change - { - foreach (var client in NetworkManager.ConnectedClients) - { - if (client.Value.ClientId == NetworkManager.ServerClientId) - { - continue; - } - if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) - { - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); - } - } - } - } - else // Normal Client-Server mode - { - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId, - }; - foreach (var client in NetworkManager.ConnectedClients) - { - if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) - { - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); - } - } - } - - // After we have sent the change ownership message to all client observers, invoke the ownership changed notification. - /// !!Important!! - /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership - /// change can be sent from NetworkBehaviours that override the - networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); - - // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. - if (!NetworkManager.DistributedAuthorityMode) - { - if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId)) - { - m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f); - } - var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; - m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier); - } - } - - internal bool HasPrefab(NetworkObject.SceneObject sceneObject) - { - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) - { - if (NetworkManager.PrefabHandler.ContainsHandler(sceneObject.Hash)) - { - return true; - } - if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.TryGetValue(sceneObject.Hash, out var networkPrefab)) - { - switch (networkPrefab.Override) - { - default: - case NetworkPrefabOverride.None: - return networkPrefab.Prefab != null; - case NetworkPrefabOverride.Hash: - case NetworkPrefabOverride.Prefab: - return networkPrefab.OverridingTargetPrefab != null; - } - } - - return false; - } - var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(sceneObject.Hash, sceneObject.NetworkSceneHandle); - return networkObject != null; - } - - internal enum InstantiateAndSpawnErrorTypes - { - NetworkPrefabNull, - NotAuthority, - InvokedWhenShuttingDown, - NotRegisteredNetworkPrefab, - NetworkManagerNull, - NoActiveSession, - } - - internal static readonly Dictionary InstantiateAndSpawnErrors = new Dictionary( - new KeyValuePair[]{ - new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkPrefabNull, $"The {nameof(NetworkObject)} prefab parameter was null!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NotAuthority, $"Only the server has authority to {nameof(InstantiateAndSpawn)}!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown, $"Invoking {nameof(InstantiateAndSpawn)} while shutting down! Calls to {nameof(InstantiateAndSpawn)} will be ignored."), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab, $"The {nameof(NetworkObject)} parameter is not a registered network prefab. Did you forget to register it or are you trying to instantiate and spawn an instance of a network prefab?"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkManagerNull, $"The {nameof(NetworkManager)} parameter was null!"), - new KeyValuePair(InstantiateAndSpawnErrorTypes.NoActiveSession, "You can only invoke this method when you are connected to an existing/in-progress network session!") - }); - - /// - /// Use this method to easily instantiate and spawn an instance of a network prefab. - /// InstantiateAndSpawn will: - /// - Find any override associated with the prefab - /// - If there is no override, then the current prefab type is used. - /// - Create an instance of the prefab (or its override). - /// - Spawn the prefab instance - /// - /// The of the pefab asset. - /// The owner of the instance (defaults to server). - /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). - /// Whether the instance is a player object or not (default is false). - /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and - /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). - /// The starting poisiton of the instance. - /// The starting rotation of the instance. - /// The newly instantiated and spawned prefab instance. - public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - { - if (networkPrefab == null) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NetworkPrefabNull]); - return null; - } - - ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; - // We only need to check for authority when running in client-server mode - if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); - return null; - } - - if (NetworkManager.ShutdownInProgress) - { - Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); - return null; - } - - // Verify it is actually a valid prefab - if (!NetworkManager.NetworkConfig.Prefabs.Contains(networkPrefab.gameObject)) - { - Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); - return null; - } - - return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); - } - - /// - /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! - /// - internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) - { - var networkObject = networkPrefab; - // - Host and clients always instantiate the override if one exists. - // - Server instantiates the original prefab unless: - // -- forceOverride is set to true =or= - // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. - // - Distributed authority mode always spawns the override if one exists. - if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) - { - Debug.Assert(networkPrefab.GlobalObjectIdHash != 0, $"The {nameof(NetworkObject)} prefab passed in does not have a valid {nameof(NetworkObject.GlobalObjectIdHash)} value!"); - Debug.Log("SENDING WRITERR, EN TEORIA AQUI SE PASA EL WRITTER PARA ESCRIBIERLO Y LUEGO HAY QUE SACARLE EL READER Y PASARLO POR UN MENSAJE"); - var bufferWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(20,Collections.Allocator.Temp))); - networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, ref bufferWriter, position, rotation); - - } - else // Under this case, server instantiate the prefab passed in. - { - networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation); - } - - if (networkObject == null) - { - Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); - return null; - } - networkObject.IsPlayerObject = isPlayerObject; - networkObject.transform.position = position; - networkObject.transform.rotation = rotation; - // If spawning as a player, then invoke SpawnAsPlayerObject - if (isPlayerObject) - { - networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene); - } - else // Otherwise just spawn with ownership - { - networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); - } - return networkObject; - } - - /// - /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the - /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. - /// - internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, ref BufferSerializer preInstanceDataSerializer, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) where T : IReaderWriter - { - - NetworkObject networkObject = null; - // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class - if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) - { - // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, ref preInstanceDataSerializer, position ?? default, rotation ?? default); - networkObject.NetworkManagerOwner = NetworkManager; - } - else - { - // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash - var networkPrefabReference = (GameObject)null; - var inScenePlacedWithNoSceneManagement = !NetworkManager.NetworkConfig.EnableSceneManagement && isScenePlaced; - - if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) - { - var networkPrefab = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash]; - - switch (networkPrefab.Override) - { - default: - case NetworkPrefabOverride.None: - networkPrefabReference = networkPrefab.Prefab; - break; - case NetworkPrefabOverride.Hash: - case NetworkPrefabOverride.Prefab: - { - // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the - // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically - // but might want to use the same source network prefab as an in-scene placed NetworkObject. - // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically - // spawning them so the original prefab placed is preserved and this is not needed) - if (inScenePlacedWithNoSceneManagement) - { - networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; - } - else - { - networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; - } - break; - } - } - } - - // If not, then there is an issue (user possibly didn't register the prefab properly?) - if (networkPrefabReference == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); - } - } - else - { - // Create prefab instance while applying any pre-assigned position and rotation values - networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation); - } - } - return networkObject; - } - - /// - /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients - /// the instance. - /// !!! Should only be invoked by unless used by an integration test !!! - /// - /// - /// should be the base prefab value and not the - /// overrided value. - /// (Can be used for integration testing) - /// - /// prefab to instantiate - /// of the base prefab instance - /// conditional position in place of the network prefab's default position - /// conditional rotation in place of the network prefab's default rotation - /// the instance of the - internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) - { - var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); - networkObject.transform.position = position ?? networkObject.transform.position; - networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; - networkObject.NetworkManagerOwner = NetworkManager; - networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; - return networkObject; - } - - /// - /// Creates a local NetowrkObject to be spawned. - /// - /// - /// For most cases this is client-side only, with the exception of when the server - /// is spawning a player. - /// - internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject, byte[] customSpawnData) - { - NetworkObject networkObject = null; - var globalObjectIdHash = sceneObject.Hash; - var position = sceneObject.HasTransform ? sceneObject.Transform.Position : default; - var rotation = sceneObject.HasTransform ? sceneObject.Transform.Rotation : default; - var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default; - var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default; - var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays; - - // If scene management is disabled or the NetworkObject was dynamically spawned - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) - { - Debug.Log("Here is needed to check the handler has or not that custom data (DYNAMIC SPAWN)"); - Debug.Log("SENDING READER"); - Debug.Log($"SceneObject preInstanceData: {sceneObject.preInstanceData.IsInitialized.ToString()}"); - var bufferWriter = new BufferSerializer(new BufferSerializerReader(sceneObject.preInstanceData)); - networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, ref bufferWriter, position, rotation, sceneObject.IsSceneObject); - } - else // Get the in-scene placed NetworkObject - { - Debug.Log("Here is needed to check the handler has or not that custom data (SCENE PLACED)"); - networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); - if (networkObject == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); - } - } - - // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so - // NetworkBehaviours will have their OnNetworkSpawn method invoked - if (networkObject != null && !networkObject.gameObject.activeInHierarchy) - { - networkObject.gameObject.SetActive(true); - } - } - - if (networkObject != null) - { - networkObject.DestroyWithScene = sceneObject.DestroyWithScene; - networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; - networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner; - networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags; - - var nonNetworkObjectParent = false; - // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) - // This is a special case scenario where a late joining client has joined and loaded one or - // more scenes that contain nested in-scene placed NetworkObject children yet the server's - // synchronization information does not indicate the NetworkObject in question has a parent =or= - // the parent has changed. - // For this we will want to remove the parent before spawning and setting the transform values based - // on several possible scenarios. - if (sceneObject.IsSceneObject && networkObject.transform.parent != null) - { - var parentNetworkObject = networkObject.transform.parent.GetComponent(); - - // special case to handle being parented under a GameObject with no NetworkObject - nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent; - - // If the in-scene placed NetworkObject has a parent NetworkObject... - if (parentNetworkObject) - { - // Then remove the parent only if: - // - The authority says we don't have a parent (but locally we do). - // - The auhtority says we have a parent but either of the two are true: - // -- It isn't the same parent. - // -- It was parented using world position stays. - if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet - && (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays))) - { - // If parenting without notifications then we are temporarily removing the parent to set the transform - // values before reparenting under the current parent. - networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent); - } - } - } - - // Set the transform only if the sceneObject includes transform information. - if (sceneObject.HasTransform) - { - // If world position stays is true or we have auto object parent synchronization disabled - // then we want to apply the position and rotation values world space relative - if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) - { - networkObject.transform.position = position; - networkObject.transform.rotation = rotation; - } - else - { - networkObject.transform.localPosition = position; - networkObject.transform.localRotation = rotation; - } - - // SPECIAL CASE: - // Since players are created uniquely we don't apply scale because - // the ConnectionApprovalResponse does not currently provide the - // ability to specify scale. So, we just use the default scale of - // the network prefab used to represent the player. - // Note: not doing this would set the player's scale to zero since - // that is the default value of Vector3. - if (!sceneObject.IsPlayerObject) - { - // Since scale is always applied to local space scale, we do the transform - // space logic during serialization such that it works out whether AutoObjectParentSync - // is enabled or not (see NetworkObject.SceneObject) - networkObject.transform.localScale = scale; - } - } - - if (sceneObject.HasParent) - { - // Go ahead and set network parenting properties, if the latest parent is not set then pass in null - // (we always want to set worldPositionStays) - ulong? parentId = null; - if (sceneObject.IsLatestParentSet) - { - parentId = parentNetworkId; - } - networkObject.SetNetworkParenting(parentId, worldPositionStays); - } - - // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL - // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. - if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) - { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); - } - } - return networkObject; - } - - /// - /// Invoked from: - /// - ConnectionManager after instantiating a player prefab when running in client-server. - /// - NetworkObject when spawning a newly instantiated NetworkObject for the first time. - /// - NetworkSceneManager after a server/session-owner has loaded a scene to locally spawn the newly instantiated in-scene placed NetworkObjects. - /// - NetworkSpawnManager when spawning any already loaded in-scene placed NetworkObjects (client-server or session owner). - /// - /// Client-Server: - /// Server is the only instance that invokes this method. - /// - /// Distributed Authority: - /// DAHost client and standard DA clients invoke this method. - /// - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) - { - if (networkObject == null) - { - throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); - } - - if (networkObject.IsSpawned) - { - Debug.LogError($"{networkObject.name} is already spawned!"); - return; - } - - if (!sceneObject) - { - var networkObjectChildren = networkObject.GetComponentsInChildren(); - if (networkObjectChildren.Length > 1) - { - Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); - } - } - // Invoke NetworkBehaviour.OnPreSpawn methods - networkObject.InvokeBehaviourNetworkPreSpawn(); - - // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning - // For now, this is the best place I could find to add all connected clients as observers for newly - // instantiated and spawned NetworkObjects on the authoritative side. - if (NetworkManager.DistributedAuthorityMode) - { - if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) - { - networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; - networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; - } - - // Always add the owner/authority even if SpawnWithObservers is false - // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) - if (!networkObject.SpawnWithObservers) - { - networkObject.Observers.Add(ownerClientId); - } - else - { - foreach (var clientId in NetworkManager.ConnectedClientsIds) - { - // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) - { - continue; - } - networkObject.Observers.Add(clientId); - } - - // Sanity check to make sure the owner is always included - // Itentionally checking as opposed to just assigning in order to generate notification. - if (!networkObject.Observers.Contains(ownerClientId)) - { - Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); - networkObject.Observers.Add(ownerClientId); - } - } - } - SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); - - // Invoke NetworkBehaviour.OnPostSpawn methods - networkObject.InvokeBehaviourNetworkPostSpawn(); - } - - /// - /// This is only invoked to instantiate a serialized NetworkObject via - /// - /// - /// - /// IMPORTANT: Pre spawn methods need to be invoked from within . - /// - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene) - { - if (networkObject == null) - { - throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); - } - - if (networkObject.IsSpawned) - { - throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); - } - - // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) - SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene); - - // It is ok to invoke NetworkBehaviour.OnPostSpawn methods - networkObject.InvokeBehaviourNetworkPostSpawn(); - } - - private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) - { - if (SpawnedObjects.ContainsKey(networkId)) - { - Debug.LogWarning($"[{NetworkManager.name}] Trying to spawn {networkObject.name} with a {nameof(NetworkObject.NetworkObjectId)} of {networkId} but it is already in the spawned list!"); - return; - } - - networkObject.IsSpawned = true; - networkObject.IsSceneObject = sceneObject; - - // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects - // Note: Always check SceneOriginHandle directly at this specific location. - if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0) - { - networkObject.SceneOrigin = networkObject.gameObject.scene; - } - - // For integration testing, this makes sure that the appropriate NetworkManager is assigned to - // the NetworkObject since it uses the NetworkManager.Singleton when not set - if (networkObject.NetworkManagerOwner != NetworkManager) - { - networkObject.NetworkManagerOwner = NetworkManager; - } - - networkObject.NetworkObjectId = networkId; - - networkObject.DestroyWithScene = sceneObject || destroyWithScene; - - networkObject.IsPlayerObject = playerObject; - - networkObject.OwnerClientId = ownerClientId; - - // When spawned, previous owner is always the first assigned owner - networkObject.PreviousOwnerId = ownerClientId; - - // If this the player and the client is the owner, then lock ownership by default - if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) - { - networkObject.SetOwnershipLock(); - } - SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); - SpawnedObjectsList.Add(networkObject); - - // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, - // then add all connected clients as observers - if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) - { - // If running as a server only, then make sure to always add the server's client identifier - if (!NetworkManager.IsHost) - { - networkObject.Observers.Add(NetworkManager.LocalClientId); - } - - // Add client observers - for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) - { - // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) - { - continue; - } - networkObject.Observers.Add(NetworkManager.ConnectedClientsIds[i]); - } - } - - networkObject.ApplyNetworkParenting(); - NetworkObject.CheckOrphanChildren(); - - AddNetworkObjectToSceneChangedUpdates(networkObject); - - networkObject.InvokeBehaviourNetworkSpawn(); - - NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId); - - // propagate the IsSceneObject setting to child NetworkObjects - var children = networkObject.GetComponentsInChildren(); - foreach (var childObject in children) - { - // Do not propagate the in-scene object setting if a child was dynamically spawned. - if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value) - { - continue; - } - childObject.IsSceneObject = sceneObject; - } - - // Only dynamically spawned NetworkObjects are allowed - if (!sceneObject) - { - networkObject.SubscribeToActiveSceneForSynch(); - } - - if (networkObject.IsPlayerObject) - { - UpdateNetworkClientPlayer(networkObject); - } - - // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set - // then assign this to the PrefabGlobalObjectIdHash - if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) - { - networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; - } - } - - internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); - - // Pre-allocating to avoid the initial constructor hit - internal Stack CleanUpDisposedObjects = new Stack(); - - internal void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject) - { - if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && - !NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) - { - if (networkObject.UpdateForSceneChanges()) - { - NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject); - } - } - } - - internal void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject) - { - if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && - NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) - { - NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId); - } - } - - internal unsafe void UpdateNetworkObjectSceneChanges() - { - foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) - { - // If it fails the first update then don't add for updates - if (!entry.Value.UpdateForSceneChanges()) - { - CleanUpDisposedObjects.Push(entry.Key); - } - } - - // Clean up any NetworkObjects that no longer exist (destroyed before they should be or the like) - while (CleanUpDisposedObjects.Count > 0) - { - NetworkObjectsToSynchronizeSceneChanges.Remove(CleanUpDisposedObjects.Pop()); - } - } - - ///AAA - internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) - { - // If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message. - var updateObservers = NetworkManager.DistributedAuthorityMode && networkObject.SpawnWithObservers; - - // Only skip if distributed authority mode is not enabled - if (clientId == NetworkManager.ServerClientId && !NetworkManager.DistributedAuthorityMode) - { - return; - } - - var message = new CreateObjectMessage - { - ObjectInfo = networkObject.GetMessageSceneObject(clientId, NetworkManager.DistributedAuthorityMode), - IncludesSerializedObject = true, - UpdateObservers = NetworkManager.DistributedAuthorityMode, - ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, - }; - Debug.Log($"Sending spawn call for object, parameters: ClientID {clientId} networkObject{networkObject} object has preInstanceData {networkObject.preInstanceData.IsInitialized.ToString()}"); - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - } - - /// - /// Only used to update object visibility/observers. - /// ** Clients are the only instances that use this method ** - /// - internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject networkObject) - { - if (!NetworkManager.DistributedAuthorityMode) - { - throw new Exception("[SendSpawnCallForObserverUpdate] Invoking a distributed authority only method when distributed authority is not enabled!"); - } - - var message = new CreateObjectMessage - { - ObjectInfo = networkObject.GetMessageSceneObject(), - ObserverIds = networkObject.Observers.ToArray(), - NewObserverIds = newObservers.ToArray(), - IncludesSerializedObject = true, - UpdateObservers = true, - UpdateNewObservers = true, - }; - Debug.Log("customSpawnData:2 SendSpawnCallForObserverUpdate" + message.CustomSpawnData.Length); - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); - foreach (var clientId in newObservers) - { - // TODO: We might want to track observer update sent as well? - NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - } - } - - internal ulong? GetSpawnParentId(NetworkObject networkObject) - { - NetworkObject parentNetworkObject = null; - - if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) - { - parentNetworkObject = networkObject.transform.parent.GetComponent(); - } - - if (parentNetworkObject == null) - { - return null; - } - - return parentNetworkObject.NetworkObjectId; - } - - internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false, bool playerDisconnect = false) - { - if (!networkObject.IsSpawned) - { - NetworkLog.LogErrorServer("Object is not spawned!"); - return; - } - - if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) - { - NetworkLog.LogErrorServer("Only server can despawn objects"); - return; - } - - if (NetworkManager.DistributedAuthorityMode && networkObject.OwnerClientId != NetworkManager.LocalClientId) - { - if (!NetworkManager.DAHost || NetworkManager.DAHost && !playerDisconnect) - { - NetworkLog.LogErrorServer($"In distributed authority mode, only the owner of the NetworkObject can despawn it! Local Client is ({NetworkManager.LocalClientId}) while the owner is ({networkObject.OwnerClientId})"); - return; - } - } - OnDespawnObject(networkObject, destroyObject, playerDisconnect); - } - - // Makes scene objects ready to be reused - internal void ServerResetShudownStateForSceneObjects() - { + /// + /// Class that handles object spawning + /// + public class NetworkSpawnManager + { + // Stores the objects that need to be shown at end-of-frame + internal Dictionary> ObjectsToShowToClient = new Dictionary>(); + + internal Dictionary> ClientsToShowObject = new Dictionary>(); + + /// + /// The currently spawned objects + /// + public readonly Dictionary SpawnedObjects = new Dictionary(); + + /// + /// A list of the spawned objects + /// + public readonly HashSet SpawnedObjectsList = new HashSet(); + + /// + /// Use to get all NetworkObjects owned by a client + /// Ownership to Objects Table Format: + /// [ClientId][NetworkObjectId][NetworkObject] + /// Server: Keeps track of all clients' ownership + /// Client: Keeps track of only its ownership + /// + public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); + + /// + /// Object to Ownership Table: + /// [NetworkObjectId][ClientId] + /// Used internally to find the client Id that currently owns + /// the NetworkObject + /// + private Dictionary m_ObjectToOwnershipTable = new Dictionary(); + + /// + /// In distributed authority mode, a list of known spawned player NetworkObject instance is maintained by each client. + /// + public IReadOnlyList PlayerObjects => m_PlayerObjects; + // Since NetworkSpawnManager is destroyed when NetworkManager shuts down, it will always be an empty list for each new network session. + // DANGO-TODO: We need to add something like a ConnectionStateMessage that is sent by either the DAHost or CMBService to each client when a client + // is connected and synchronized or when a cient disconnects (but the player objects list we should keep as it is useful to have). + private List m_PlayerObjects = new List(); + + private Dictionary> m_PlayerObjectsTable = new Dictionary>(); + + /// + /// Returns the connect client identifiers + /// + /// connected client identifier list + public List GetConnectedPlayers() + { + return m_PlayerObjectsTable.Keys.ToList(); + } + + /// + /// Adds a player object and updates all other players' observers list + /// + private void AddPlayerObject(NetworkObject playerObject) + { + if (!playerObject.IsPlayerObject) + { + if (NetworkManager.LogLevel == LogLevel.Normal) + { + NetworkLog.LogError($"Attempting to register a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); + return; + } + } + + foreach (var player in m_PlayerObjects) + { + // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer. + if (player.SpawnWithObservers) + { + player.Observers.Add(playerObject.OwnerClientId); + } + + // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object. + if (playerObject.SpawnWithObservers) + { + playerObject.Observers.Add(player.OwnerClientId); + } + } + + // Only if spawn with observers is set or we are using a distributed authority network topology and this is the client's player should we add + // the owner as an observer. + if (playerObject.SpawnWithObservers || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == playerObject.OwnerClientId)) + { + playerObject.Observers.Add(playerObject.OwnerClientId); + } + + m_PlayerObjects.Add(playerObject); + if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) + { + m_PlayerObjectsTable.Add(playerObject.OwnerClientId, new List()); + } + m_PlayerObjectsTable[playerObject.OwnerClientId].Add(playerObject); + } + + internal void UpdateNetworkClientPlayer(NetworkObject playerObject) + { + // If the player's client does not already have a NetworkClient entry + if (!NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId)) + { + // Add the player's client + NetworkManager.ConnectionManager.AddClient(playerObject.OwnerClientId); + } + var playerNetworkClient = NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId]; + + // If a client changes their player object, then we should adjust for the client's new player + if (playerNetworkClient.PlayerObject != null && m_PlayerObjects.Contains(playerNetworkClient.PlayerObject)) + { + // Just remove the previous player object but keep the assigned observers of the NetworkObject + RemovePlayerObject(playerNetworkClient.PlayerObject); + } + + // Now update the associated NetworkClient's player object + NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].AssignPlayerObject(ref playerObject); + AddPlayerObject(playerObject); + } + + /// + /// Removes a player object and updates all other players' observers list + /// + private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObject = false) + { + if (!playerObject.IsPlayerObject) + { + if (NetworkManager.LogLevel == LogLevel.Normal) + { + NetworkLog.LogError($"Attempting to deregister a {nameof(NetworkObject)} as a player object but {nameof(NetworkObject.IsPlayerObject)} is not set!"); + return; + } + } + playerObject.IsPlayerObject = false; + m_PlayerObjects.Remove(playerObject); + if (m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId)) + { + m_PlayerObjectsTable[playerObject.OwnerClientId].Remove(playerObject); + if (m_PlayerObjectsTable[playerObject.OwnerClientId].Count == 0) + { + m_PlayerObjectsTable.Remove(playerObject.OwnerClientId); + } + } + + if (NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId) && destroyingObject) + { + NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null; + } + } + + internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId) + { + if (!ObjectsToShowToClient.ContainsKey(clientId)) + { + ObjectsToShowToClient.Add(clientId, new List()); + } + ObjectsToShowToClient[clientId].Add(networkObject); + if (NetworkManager.DistributedAuthorityMode) + { + if (!ClientsToShowObject.ContainsKey(networkObject)) + { + ClientsToShowObject.Add(networkObject, new List()); + } + ClientsToShowObject[networkObject].Add(clientId); + } + } + + // returns whether any matching objects would have become visible and were returned to hidden state + internal bool RemoveObjectFromShowingTo(NetworkObject networkObject, ulong clientId) + { + if (NetworkManager.DistributedAuthorityMode) + { + if (ClientsToShowObject.ContainsKey(networkObject)) + { + ClientsToShowObject[networkObject].Remove(clientId); + if (ClientsToShowObject[networkObject].Count == 0) + { + ClientsToShowObject.Remove(networkObject); + } + } + } + var ret = false; + if (!ObjectsToShowToClient.ContainsKey(clientId)) + { + return false; + } + + // probably overkill, but deals with multiple entries + while (ObjectsToShowToClient[clientId].Contains(networkObject)) + { + Debug.LogWarning( + "Object was shown and hidden from the same client in the same Network frame. As a result, the client will _not_ receive a NetworkSpawn"); + ObjectsToShowToClient[clientId].Remove(networkObject); + ret = true; + } + + if (ret) + { + networkObject.Observers.Remove(clientId); + } + + return ret; + } + + /// + /// Used to update a NetworkObject's ownership + /// + internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false) + { + var previousOwner = newOwner; + + // Use internal lookup table to see if the NetworkObject has a previous owner + if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId)) + { + // Keep track of the previous owner's ClientId + previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId]; + + // We are either despawning (remove) or changing ownership (assign) + if (isRemoving) + { + m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId); + } + else + { + // If we already had this owner in our table then just exit + if (NetworkManager.DistributedAuthorityMode && previousOwner == newOwner) + { + return; + } + m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner; + } + } + else + { + // Otherwise, just add a new lookup entry + m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner); + } + + // Check to see if we had a previous owner + if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner)) + { + // Before updating the previous owner, assure this entry exists + if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Remove the previous owner's entry + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + + // If we are removing the entry (i.e. despawning or client lost ownership) + if (isRemoving) + { + return; + } + } + else + { + // Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen + throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?"); + } + } + + // If the owner doesn't have an entry then create one + if (!OwnershipToObjectsTable.ContainsKey(newOwner)) + { + OwnershipToObjectsTable.Add(newOwner, new Dictionary()); + } + + // Sanity check to make sure we don't already have this entry (we shouldn't) + if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Add the new ownership entry + OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject); + } + else if (isRemoving) + { + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + } + else if (NetworkManager.LogLevel == LogLevel.Developer && previousOwner == newOwner) + { + NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!"); + } + } + + /// + /// Returns an array of all NetworkObjects that belong to a client. + /// + /// the client's id + /// returns an array of the s owned by the client + public NetworkObject[] GetClientOwnedObjects(ulong clientId) + { + if (!OwnershipToObjectsTable.ContainsKey(clientId)) + { + OwnershipToObjectsTable.Add(clientId, new Dictionary()); + } + return OwnershipToObjectsTable[clientId].Values.ToArray(); + } + + /// + /// Gets the NetworkManager associated with this SpawnManager. + /// + public NetworkManager NetworkManager { get; } + + internal readonly Queue ReleasedNetworkObjectIds = new Queue(); + private ulong m_NetworkObjectIdCounter; + + // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations + private List m_TargetClientIds = new List(); + + internal ulong GetNetworkObjectId() + { + if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) + { + return ReleasedNetworkObjectIds.Dequeue().NetworkId; + } + + m_NetworkObjectIdCounter++; + + // DANGO-TODO: Need a more robust solution here. + return m_NetworkObjectIdCounter + (NetworkManager.LocalClientId * 10000); + } + + /// + /// Returns the local player object or null if one does not exist + /// + /// The local player object or null if one does not exist + public NetworkObject GetLocalPlayerObject() + { + return GetPlayerNetworkObject(NetworkManager.LocalClientId); + } + + /// + /// Returns all instances assigned to the client identifier + /// + /// the client identifier of the player + /// A list of instances (if more than one are assigned) + public List GetPlayerNetworkObjects(ulong clientId) + { + if (m_PlayerObjectsTable.ContainsKey(clientId)) + { + return m_PlayerObjectsTable[clientId]; + } + return null; + } + + /// + /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. + /// + /// the client identifier of the player + /// The player object with a given clientId or null if one does not exist + public NetworkObject GetPlayerNetworkObject(ulong clientId) + { + if (!NetworkManager.DistributedAuthorityMode) + { + if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId) + { + throw new NotServerException("Only the server can find player objects from other clients."); + } + if (TryGetNetworkClient(clientId, out NetworkClient networkClient)) + { + return networkClient.PlayerObject; + } + } + else + { + if (m_PlayerObjectsTable.ContainsKey(clientId)) + { + return m_PlayerObjectsTable[clientId].First(); + } + } + + return null; + } + + /// + /// Helper function to get a network client for a clientId from the NetworkManager. + /// On the server this will check the list. + /// On a non-server this will check the only. + /// + /// The clientId for which to try getting the NetworkClient for. + /// The found NetworkClient. Null if no client was found. + /// True if a NetworkClient with a matching id was found else false. + private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient) + { + if (NetworkManager.IsServer) + { + return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); + } + + if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId) + { + networkClient = NetworkManager.LocalClient; + return true; + } + + networkClient = null; + return false; + } + + /// + /// Not used. + /// + /// not used + /// not used + protected virtual void InternalOnOwnershipChanged(ulong perviousOwner, ulong newOwner) + { + + } + + internal void RemoveOwnership(NetworkObject networkObject) + { + if (NetworkManager.DistributedAuthorityMode && !NetworkManager.ShutdownInProgress) + { + Debug.LogError($"Removing ownership is invalid in Distributed Authority Mode. Use {nameof(ChangeOwnership)} instead."); + return; + } + ChangeOwnership(networkObject, NetworkManager.ServerClientId, true); + } + + private Dictionary m_LastChangeInOwnership = new Dictionary(); + private const int k_MaximumTickOwnershipChangeMultiplier = 6; + + internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false) + { + if (clientId == networkObject.OwnerClientId) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[{nameof(NetworkSpawnManager)}][{nameof(ChangeOwnership)}] Attempting to change ownership to Client-{clientId} when the owner is already {networkObject.OwnerClientId}! (Ignoring)"); + } + return; + } + + // For client-server: + // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes, + // then notify the user they could potentially lose state updates if developer logging is enabled. + if (NetworkManager.LogLevel == LogLevel.Developer && !NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup) + { + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + if (networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0) + { + NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss."); + break; + } + } + } + + if (NetworkManager.DistributedAuthorityMode) + { + // Ensure only the session owner can change ownership (i.e. acquire) and that the session owner is not trying to assign a non-session owner client + // ownership of a NetworkObject with SessionOwner permissions. + if (networkObject.IsOwnershipSessionOwner && (!NetworkManager.LocalClient.IsSessionOwner || clientId != NetworkManager.CurrentSessionOwner)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Session Owner Only] You cannot change ownership of a {nameof(NetworkObject)} that has the {NetworkObject.OwnershipStatus.SessionOwner} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.SessionOwnerOnly); + return; + } + + // If are not authorized and this is not an approved ownership change, then check to see if we can change ownership + if (!isAuthorized && !isRequestApproval) + { + if (networkObject.IsOwnershipLocked) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Locked] You cannot change ownership while a {nameof(NetworkObject)} is locked!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.Locked); + return; + } + if (networkObject.IsRequestInProgress) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Request Pending] You cannot change ownership while a {nameof(NetworkObject)} has a pending ownership request!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestInProgress); + return; + } + if (networkObject.IsOwnershipRequestRequired) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Request Required] You cannot change ownership directly if a {nameof(NetworkObject)} has the {NetworkObject.OwnershipStatus.RequestRequired} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.RequestRequired); + return; + } + if (!networkObject.IsOwnershipTransferable) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"[{networkObject.name}][Not transferrable] You cannot change ownership of a {nameof(NetworkObject)} that does not have the {NetworkObject.OwnershipStatus.Transferable} flag set!"); + } + networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.NotTransferrable); + return; + } + } + } + else if (!isAuthorized) + { + throw new NotServerException("Only the server can change ownership"); + } + + if (!networkObject.IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (networkObject.OwnerClientId == clientId && networkObject.PreviousOwnerId == clientId) + { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarningServer($"[Already Owner] Unnecessary ownership change for {networkObject.name} as it is already the owned by client-{clientId}"); + } + return; + } + + if (!networkObject.Observers.Contains(clientId)) + { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarningServer($"[Invalid Owner] Cannot send Ownership change as client-{clientId} cannot see {networkObject.name}! Use {nameof(NetworkObject.NetworkShow)} first."); + } + return; + } + + // Save the previous owner to work around a bit of a nasty issue with assuring NetworkVariables are serialized when ownership changes + var originalPreviousOwnerId = networkObject.PreviousOwnerId; + var originalOwner = networkObject.OwnerClientId; + + // Used to distinguish whether a new owner should receive any currently dirty NetworkVariable updates + networkObject.PreviousOwnerId = networkObject.OwnerClientId; + + // Assign the new owner + networkObject.OwnerClientId = clientId; + + // Always notify locally on the server when ownership is lost + networkObject.InvokeBehaviourOnLostOwnership(); + + // Authority adds entries for all client ownership + UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); + + // Always notify locally on the server when a new owner is assigned + networkObject.InvokeBehaviourOnGainedOwnership(); + + // If we are the original owner, then we want to synchronize owner read & write NetworkVariables. + if (originalOwner == NetworkManager.LocalClientId) + { + networkObject.SynchronizeOwnerNetworkVariables(originalOwner, originalPreviousOwnerId); + } + + var size = 0; + if (NetworkManager.DistributedAuthorityMode) + { + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId, + DistributedAuthorityMode = NetworkManager.DistributedAuthorityMode, + RequestApproved = isRequestApproval, + OwnershipIsChanging = true, + RequestClientId = networkObject.PreviousOwnerId, + OwnershipFlags = (ushort)networkObject.Ownership, + }; + // If we are connected to the CMB service or not the DAHost (i.e. pure DA-Clients only) + if (NetworkManager.CMBServiceConnection || !NetworkManager.DAHost) + { + // Always update the network properties in distributed authority mode for the client gaining ownership + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties(); + } + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(NetworkManager.LocalClientId, networkObject, size); + } + else // We are the DAHost so broadcast the ownership change + { + foreach (var client in NetworkManager.ConnectedClients) + { + if (client.Value.ClientId == NetworkManager.ServerClientId) + { + continue; + } + if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + } + } + else // Normal Client-Server mode + { + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId, + }; + foreach (var client in NetworkManager.ConnectedClients) + { + if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + } + + // After we have sent the change ownership message to all client observers, invoke the ownership changed notification. + /// !!Important!! + /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership + /// change can be sent from NetworkBehaviours that override the + networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); + + // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. + if (!NetworkManager.DistributedAuthorityMode) + { + if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId)) + { + m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f); + } + var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; + m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier); + } + } + + internal bool HasPrefab(NetworkObject.SceneObject sceneObject) + { + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) + { + if (NetworkManager.PrefabHandler.ContainsHandler(sceneObject.Hash)) + { + return true; + } + if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.TryGetValue(sceneObject.Hash, out var networkPrefab)) + { + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + return networkPrefab.Prefab != null; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + return networkPrefab.OverridingTargetPrefab != null; + } + } + + return false; + } + var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(sceneObject.Hash, sceneObject.NetworkSceneHandle); + return networkObject != null; + } + + internal enum InstantiateAndSpawnErrorTypes + { + NetworkPrefabNull, + NotAuthority, + InvokedWhenShuttingDown, + NotRegisteredNetworkPrefab, + NetworkManagerNull, + NoActiveSession, + } + + internal static readonly Dictionary InstantiateAndSpawnErrors = new Dictionary( + new KeyValuePair[]{ + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkPrefabNull, $"The {nameof(NetworkObject)} prefab parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotAuthority, $"Only the server has authority to {nameof(InstantiateAndSpawn)}!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown, $"Invoking {nameof(InstantiateAndSpawn)} while shutting down! Calls to {nameof(InstantiateAndSpawn)} will be ignored."), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab, $"The {nameof(NetworkObject)} parameter is not a registered network prefab. Did you forget to register it or are you trying to instantiate and spawn an instance of a network prefab?"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkManagerNull, $"The {nameof(NetworkManager)} parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NoActiveSession, "You can only invoke this method when you are connected to an existing/in-progress network session!") + }); + + /// + /// Use this method to easily instantiate and spawn an instance of a network prefab. + /// InstantiateAndSpawn will: + /// - Find any override associated with the prefab + /// - If there is no override, then the current prefab type is used. + /// - Create an instance of the prefab (or its override). + /// - Spawn the prefab instance + /// + /// The of the pefab asset. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + if (networkPrefab == null) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NetworkPrefabNull]); + return null; + } + + ownerClientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : ownerClientId; + // We only need to check for authority when running in client-server mode + if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); + return null; + } + + if (NetworkManager.ShutdownInProgress) + { + Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); + return null; + } + + // Verify it is actually a valid prefab + if (!NetworkManager.NetworkConfig.Prefabs.Contains(networkPrefab.gameObject)) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); + return null; + } + + return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); + } + + /// + /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! + /// + internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + var networkObject = networkPrefab; + // - Host and clients always instantiate the override if one exists. + // - Server instantiates the original prefab unless: + // -- forceOverride is set to true =or= + // -- The prefab has a registered prefab handler, then we let user code determine what to spawn. + // - Distributed authority mode always spawns the override if one exists. + if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash)) + { + Debug.Assert(networkPrefab.GlobalObjectIdHash != 0, $"The {nameof(NetworkObject)} prefab passed in does not have a valid {nameof(NetworkObject.GlobalObjectIdHash)} value!"); + var intantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(20, Collections.Allocator.Temp))); + networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, ref intantiationPayloadWriter, position, rotation); + } + else // Under this case, server instantiate the prefab passed in. + { + networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation); + } + + if (networkObject == null) + { + Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); + return null; + } + networkObject.IsPlayerObject = isPlayerObject; + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + // If spawning as a player, then invoke SpawnAsPlayerObject + if (isPlayerObject) + { + networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene); + } + else // Otherwise just spawn with ownership + { + networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); + } + return networkObject; + } + + /// + /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the + /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. + /// + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, ref BufferSerializer instantiationPayloadSerializer, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) where T : IReaderWriter + { + NetworkObject networkObject = null; + // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class + if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) + { + // Let the handler spawn the NetworkObject + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, ref instantiationPayloadSerializer, position ?? default, rotation ?? default); + networkObject.NetworkManagerOwner = NetworkManager; + } + else + { + // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash + var networkPrefabReference = (GameObject)null; + var inScenePlacedWithNoSceneManagement = !NetworkManager.NetworkConfig.EnableSceneManagement && isScenePlaced; + + if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) + { + var networkPrefab = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash]; + + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + networkPrefabReference = networkPrefab.Prefab; + break; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + { + // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the + // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically + // but might want to use the same source network prefab as an in-scene placed NetworkObject. + // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically + // spawning them so the original prefab placed is preserved and this is not needed) + if (inScenePlacedWithNoSceneManagement) + { + networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; + } + else + { + networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; + } + break; + } + } + } + + // If not, then there is an issue (user possibly didn't register the prefab properly?) + if (networkPrefabReference == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); + } + } + else + { + // Create prefab instance while applying any pre-assigned position and rotation values + networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation); + } + } + return networkObject; + } + + /// + /// Instantiates a network prefab instance, assigns the base prefab , positions, and orients + /// the instance. + /// !!! Should only be invoked by unless used by an integration test !!! + /// + /// + /// should be the base prefab value and not the + /// overrided value. + /// (Can be used for integration testing) + /// + /// prefab to instantiate + /// of the base prefab instance + /// conditional position in place of the network prefab's default position + /// conditional rotation in place of the network prefab's default rotation + /// the instance of the + internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) + { + var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); + networkObject.transform.position = position ?? networkObject.transform.position; + networkObject.transform.rotation = rotation ?? networkObject.transform.rotation; + networkObject.NetworkManagerOwner = NetworkManager; + networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; + return networkObject; + } + + /// + /// Creates a local NetowrkObject to be spawned. + /// + /// + /// For most cases this is client-side only, with the exception of when the server + /// is spawning a player. + /// + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject) + { + NetworkObject networkObject = null; + var globalObjectIdHash = sceneObject.Hash; + var position = sceneObject.HasTransform ? sceneObject.Transform.Position : default; + var rotation = sceneObject.HasTransform ? sceneObject.Transform.Rotation : default; + var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default; + var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default; + var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays; + + // If scene management is disabled or the NetworkObject was dynamically spawned + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) + { + var instantiationPayloadReader = new BufferSerializer(new BufferSerializerReader(sceneObject.InstantiationPayload)); + networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, ref instantiationPayloadReader, position, rotation, sceneObject.IsSceneObject); + } + else // Get the in-scene placed NetworkObject + { + networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); + } + } + + // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so + // NetworkBehaviours will have their OnNetworkSpawn method invoked + if (networkObject != null && !networkObject.gameObject.activeInHierarchy) + { + networkObject.gameObject.SetActive(true); + } + } + + if (networkObject != null) + { + networkObject.DestroyWithScene = sceneObject.DestroyWithScene; + networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner; + networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags; + + var nonNetworkObjectParent = false; + // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) + // This is a special case scenario where a late joining client has joined and loaded one or + // more scenes that contain nested in-scene placed NetworkObject children yet the server's + // synchronization information does not indicate the NetworkObject in question has a parent =or= + // the parent has changed. + // For this we will want to remove the parent before spawning and setting the transform values based + // on several possible scenarios. + if (sceneObject.IsSceneObject && networkObject.transform.parent != null) + { + var parentNetworkObject = networkObject.transform.parent.GetComponent(); + + // special case to handle being parented under a GameObject with no NetworkObject + nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent; + + // If the in-scene placed NetworkObject has a parent NetworkObject... + if (parentNetworkObject) + { + // Then remove the parent only if: + // - The authority says we don't have a parent (but locally we do). + // - The auhtority says we have a parent but either of the two are true: + // -- It isn't the same parent. + // -- It was parented using world position stays. + if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet + && (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays))) + { + // If parenting without notifications then we are temporarily removing the parent to set the transform + // values before reparenting under the current parent. + networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent); + } + } + } + + // Set the transform only if the sceneObject includes transform information. + if (sceneObject.HasTransform) + { + // If world position stays is true or we have auto object parent synchronization disabled + // then we want to apply the position and rotation values world space relative + if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) + { + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + } + else + { + networkObject.transform.localPosition = position; + networkObject.transform.localRotation = rotation; + } + + // SPECIAL CASE: + // Since players are created uniquely we don't apply scale because + // the ConnectionApprovalResponse does not currently provide the + // ability to specify scale. So, we just use the default scale of + // the network prefab used to represent the player. + // Note: not doing this would set the player's scale to zero since + // that is the default value of Vector3. + if (!sceneObject.IsPlayerObject) + { + // Since scale is always applied to local space scale, we do the transform + // space logic during serialization such that it works out whether AutoObjectParentSync + // is enabled or not (see NetworkObject.SceneObject) + networkObject.transform.localScale = scale; + } + } + + if (sceneObject.HasParent) + { + // Go ahead and set network parenting properties, if the latest parent is not set then pass in null + // (we always want to set worldPositionStays) + ulong? parentId = null; + if (sceneObject.IsLatestParentSet) + { + parentId = parentNetworkId; + } + networkObject.SetNetworkParenting(parentId, worldPositionStays); + } + + // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL + // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. + if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } + } + return networkObject; + } + + /// + /// Invoked from: + /// - ConnectionManager after instantiating a player prefab when running in client-server. + /// - NetworkObject when spawning a newly instantiated NetworkObject for the first time. + /// - NetworkSceneManager after a server/session-owner has loaded a scene to locally spawn the newly instantiated in-scene placed NetworkObjects. + /// - NetworkSpawnManager when spawning any already loaded in-scene placed NetworkObjects (client-server or session owner). + /// + /// Client-Server: + /// Server is the only instance that invokes this method. + /// + /// Distributed Authority: + /// DAHost client and standard DA clients invoke this method. + /// + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + Debug.LogError($"{networkObject.name} is already spawned!"); + return; + } + + if (!sceneObject) + { + var networkObjectChildren = networkObject.GetComponentsInChildren(); + if (networkObjectChildren.Length > 1) + { + Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); + } + } + // Invoke NetworkBehaviour.OnPreSpawn methods + networkObject.InvokeBehaviourNetworkPreSpawn(); + + // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning + // For now, this is the best place I could find to add all connected clients as observers for newly + // instantiated and spawned NetworkObjects on the authoritative side. + if (NetworkManager.DistributedAuthorityMode) + { + if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) + { + networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; + networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; + } + + // Always add the owner/authority even if SpawnWithObservers is false + // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) + if (!networkObject.SpawnWithObservers) + { + networkObject.Observers.Add(ownerClientId); + } + else + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration + if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) + { + continue; + } + networkObject.Observers.Add(clientId); + } + + // Sanity check to make sure the owner is always included + // Itentionally checking as opposed to just assigning in order to generate notification. + if (!networkObject.Observers.Contains(ownerClientId)) + { + Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); + networkObject.Observers.Add(ownerClientId); + } + } + } + SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + + // Invoke NetworkBehaviour.OnPostSpawn methods + networkObject.InvokeBehaviourNetworkPostSpawn(); + } + + /// + /// This is only invoked to instantiate a serialized NetworkObject via + /// + /// + /// + /// IMPORTANT: Pre spawn methods need to be invoked from within . + /// + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); + } + + // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) + SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene); + + // It is ok to invoke NetworkBehaviour.OnPostSpawn methods + networkObject.InvokeBehaviourNetworkPostSpawn(); + } + + private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + { + if (SpawnedObjects.ContainsKey(networkId)) + { + Debug.LogWarning($"[{NetworkManager.name}] Trying to spawn {networkObject.name} with a {nameof(NetworkObject.NetworkObjectId)} of {networkId} but it is already in the spawned list!"); + return; + } + + networkObject.IsSpawned = true; + networkObject.IsSceneObject = sceneObject; + + // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects + // Note: Always check SceneOriginHandle directly at this specific location. + if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0) + { + networkObject.SceneOrigin = networkObject.gameObject.scene; + } + + // For integration testing, this makes sure that the appropriate NetworkManager is assigned to + // the NetworkObject since it uses the NetworkManager.Singleton when not set + if (networkObject.NetworkManagerOwner != NetworkManager) + { + networkObject.NetworkManagerOwner = NetworkManager; + } + + networkObject.NetworkObjectId = networkId; + + networkObject.DestroyWithScene = sceneObject || destroyWithScene; + + networkObject.IsPlayerObject = playerObject; + + networkObject.OwnerClientId = ownerClientId; + + // When spawned, previous owner is always the first assigned owner + networkObject.PreviousOwnerId = ownerClientId; + + // If this the player and the client is the owner, then lock ownership by default + if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) + { + networkObject.SetOwnershipLock(); + } + SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); + SpawnedObjectsList.Add(networkObject); + + // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, + // then add all connected clients as observers + if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) + { + // If running as a server only, then make sure to always add the server's client identifier + if (!NetworkManager.IsHost) + { + networkObject.Observers.Add(NetworkManager.LocalClientId); + } + + // Add client observers + for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) + { + // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. + if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) + { + continue; + } + networkObject.Observers.Add(NetworkManager.ConnectedClientsIds[i]); + } + } + + networkObject.ApplyNetworkParenting(); + NetworkObject.CheckOrphanChildren(); + + AddNetworkObjectToSceneChangedUpdates(networkObject); + + networkObject.InvokeBehaviourNetworkSpawn(); + + NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId); + + // propagate the IsSceneObject setting to child NetworkObjects + var children = networkObject.GetComponentsInChildren(); + foreach (var childObject in children) + { + // Do not propagate the in-scene object setting if a child was dynamically spawned. + if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value) + { + continue; + } + childObject.IsSceneObject = sceneObject; + } + + // Only dynamically spawned NetworkObjects are allowed + if (!sceneObject) + { + networkObject.SubscribeToActiveSceneForSynch(); + } + + if (networkObject.IsPlayerObject) + { + UpdateNetworkClientPlayer(networkObject); + } + + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) + { + networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; + } + } + + internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); + + // Pre-allocating to avoid the initial constructor hit + internal Stack CleanUpDisposedObjects = new Stack(); + + internal void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject) + { + if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && + !NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) + { + if (networkObject.UpdateForSceneChanges()) + { + NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject); + } + } + } + + internal void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject) + { + if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) && + NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) + { + NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId); + } + } + + internal unsafe void UpdateNetworkObjectSceneChanges() + { + foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) + { + // If it fails the first update then don't add for updates + if (!entry.Value.UpdateForSceneChanges()) + { + CleanUpDisposedObjects.Push(entry.Key); + } + } + + // Clean up any NetworkObjects that no longer exist (destroyed before they should be or the like) + while (CleanUpDisposedObjects.Count > 0) + { + NetworkObjectsToSynchronizeSceneChanges.Remove(CleanUpDisposedObjects.Pop()); + } + } + + internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) + { + // If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message. + var updateObservers = NetworkManager.DistributedAuthorityMode && networkObject.SpawnWithObservers; + + // Only skip if distributed authority mode is not enabled + if (clientId == NetworkManager.ServerClientId && !NetworkManager.DistributedAuthorityMode) + { + return; + } + + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(clientId, NetworkManager.DistributedAuthorityMode), + IncludesSerializedObject = true, + UpdateObservers = NetworkManager.DistributedAuthorityMode, + ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, + }; + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + } + + /// + /// Only used to update object visibility/observers. + /// ** Clients are the only instances that use this method ** + /// + internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject networkObject) + { + if (!NetworkManager.DistributedAuthorityMode) + { + throw new Exception("[SendSpawnCallForObserverUpdate] Invoking a distributed authority only method when distributed authority is not enabled!"); + } + + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(), + ObserverIds = networkObject.Observers.ToArray(), + NewObserverIds = newObservers.ToArray(), + IncludesSerializedObject = true, + UpdateObservers = true, + UpdateNewObservers = true, + }; + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); + foreach (var clientId in newObservers) + { + // TODO: We might want to track observer update sent as well? + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + } + } + + internal ulong? GetSpawnParentId(NetworkObject networkObject) + { + NetworkObject parentNetworkObject = null; + + if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) + { + parentNetworkObject = networkObject.transform.parent.GetComponent(); + } + + if (parentNetworkObject == null) + { + return null; + } + + return parentNetworkObject.NetworkObjectId; + } + + internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false, bool playerDisconnect = false) + { + if (!networkObject.IsSpawned) + { + NetworkLog.LogErrorServer("Object is not spawned!"); + return; + } + + if (!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) + { + NetworkLog.LogErrorServer("Only server can despawn objects"); + return; + } + + if (NetworkManager.DistributedAuthorityMode && networkObject.OwnerClientId != NetworkManager.LocalClientId) + { + if (!NetworkManager.DAHost || NetworkManager.DAHost && !playerDisconnect) + { + NetworkLog.LogErrorServer($"In distributed authority mode, only the owner of the NetworkObject can despawn it! Local Client is ({NetworkManager.LocalClientId}) while the owner is ({networkObject.OwnerClientId})"); + return; + } + } + OnDespawnObject(networkObject, destroyObject, playerDisconnect); + } + + // Makes scene objects ready to be reused + internal void ServerResetShudownStateForSceneObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); #else var networkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); #endif - foreach (var sobj in networkObjects) - { - sobj.IsSpawned = false; - sobj.DestroyWithScene = false; - sobj.IsSceneObject = null; - } - } - - /// - /// Gets called only by NetworkSceneManager.SwitchScene - /// - internal void ServerDestroySpawnedSceneObjects() - { - // This Allocation is "OK" for now because this code only executes when a new scene is switched to - // We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove - // objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur. - var spawnedObjects = SpawnedObjectsList.ToList(); - - foreach (var sobj in spawnedObjects) - { - if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) - { - SpawnedObjectsList.Remove(sobj); - UnityEngine.Object.Destroy(sobj.gameObject); - } - } - } - - internal void DespawnAndDestroyNetworkObjects() - { + foreach (var sobj in networkObjects) + { + sobj.IsSpawned = false; + sobj.DestroyWithScene = false; + sobj.IsSceneObject = null; + } + } + + /// + /// Gets called only by NetworkSceneManager.SwitchScene + /// + internal void ServerDestroySpawnedSceneObjects() + { + // This Allocation is "OK" for now because this code only executes when a new scene is switched to + // We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove + // objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur. + var spawnedObjects = SpawnedObjectsList.ToList(); + + foreach (var sobj in spawnedObjects) + { + if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) + { + SpawnedObjectsList.Remove(sobj); + UnityEngine.Object.Destroy(sobj.gameObject); + } + } + } + + internal void DespawnAndDestroyNetworkObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) - { - OnDespawnObject(networkObjects[i], false); - // Leave destruction up to the handler - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - } - else - { - // If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene - // is unloaded. Otherwise, despawn and destroy it. - var shouldDestroy = !(networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value)); - - // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed - if (shouldDestroy) - { - // Check to see if there are any in-scene placed children that are marked to be destroyed with the scene - var childrenObjects = networkObjects[i].GetComponentsInChildren(); - foreach (var childObject in childrenObjects) - { - if (childObject == networkObjects[i]) - { - continue; - } - - // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) - // and set its parent to root - if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) - { - childObject.TryRemoveParent(childObject.WorldPositionStays()); - } - } - } - - // If spawned, then despawn and potentially destroy. - if (networkObjects[i].IsSpawned) - { - OnDespawnObject(networkObjects[i], shouldDestroy); - } - else // Otherwise, if we are not spawned and we should destroy...then destroy. - if (shouldDestroy) - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); - } - } - } - } - } - - internal void DestroySceneObjects() - { + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + OnDespawnObject(networkObjects[i], false); + // Leave destruction up to the handler + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + } + else + { + // If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene + // is unloaded. Otherwise, despawn and destroy it. + var shouldDestroy = !(networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value)); + + // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed + if (shouldDestroy) + { + // Check to see if there are any in-scene placed children that are marked to be destroyed with the scene + var childrenObjects = networkObjects[i].GetComponentsInChildren(); + foreach (var childObject in childrenObjects) + { + if (childObject == networkObjects[i]) + { + continue; + } + + // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) + // and set its parent to root + if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) + { + childObject.TryRemoveParent(childObject.WorldPositionStays()); + } + } + } + + // If spawned, then despawn and potentially destroy. + if (networkObjects[i].IsSpawned) + { + OnDespawnObject(networkObjects[i], shouldDestroy); + } + else // Otherwise, if we are not spawned and we should destroy...then destroy. + if (shouldDestroy) + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void DestroySceneObjects() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) - { - if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) - { - // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. - OnDespawnObject(networkObjects[i], false); - } - else // If not spawned, then just invoke the handler - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - } - } - else - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); - } - } - } - } - } - - internal void ServerSpawnSceneObjectsOnStartSweep() - { + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) + { + // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. + OnDespawnObject(networkObjects[i], false); + } + else // If not spawned, then just invoke the handler + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + } + } + else + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void ServerSpawnSceneObjectsOnStartSweep() + { #if UNITY_2023_1_OR_NEWER - var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); + var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif - var networkObjectsToSpawn = new List(); - for (int i = 0; i < networkObjects.Length; i++) - { - if (networkObjects[i].NetworkManager == NetworkManager) - { - // This used to be two loops. - // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. - // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. - if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) - { - var ownerId = networkObjects[i].OwnerClientId; - if (NetworkManager.DistributedAuthorityMode) - { - ownerId = NetworkManager.LocalClientId; - } - - SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); - networkObjectsToSpawn.Add(networkObjects[i]); - } - } - } - - // Since we are spawing in-scene placed NetworkObjects for already loaded scenes, - // we need to add any in-scene placed NetworkObject to our tracking table - var clearFirst = true; - foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded) - { - NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst); - clearFirst = false; - } - - // Notify all in-scene placed NetworkObjects have been spawned - foreach (var networkObject in networkObjectsToSpawn) - { - networkObject.InternalInSceneNetworkObjectsSpawned(); - } - networkObjectsToSpawn.Clear(); - } - - /// - /// Called when destroying an object after receiving a . - /// Processes logic for how to destroy objects on the non-authority client. - /// - internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject) - { - if (networkObject.HasAuthority) - { - NetworkLog.LogError($"OnDespawnNonAuthorityObject called on object {networkObject.NetworkObjectId} when is current client {NetworkManager.LocalClientId} has authority on this object."); - } - - // On the non-authority, never destroy the game object when InScenePlaced, otherwise always destroy on non-authority side - OnDespawnObject(networkObject, networkObject.IsSceneObject == false); - } - - internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject, bool modeDestroy = false) - { - if (!NetworkManager) - { - return; - } - - // We have to do this check first as subsequent checks assume we can access NetworkObjectId. - if (!networkObject) - { - Debug.LogWarning($"Trying to destroy network object but it is null"); - return; - } - - // Removal of spawned object - if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId)) - { - if (!NetworkManager.ShutdownInProgress) - { - Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!"); - } - return; - } - - // If we are shutting down the NetworkManager, then ignore resetting the parent - // and only attempt to remove the child's parent on the server-side - var distributedAuthority = NetworkManager.DistributedAuthorityMode; - if (!NetworkManager.ShutdownInProgress && (NetworkManager.IsServer || distributedAuthority)) - { - // Get all child NetworkObjects - var objectsToRemoveParent = networkObject.GetComponentsInChildren(); - - // Move child NetworkObjects to the root when parent NetworkObject is destroyed - foreach (var spawnedNetObj in objectsToRemoveParent) - { - if (spawnedNetObj == networkObject) - { - continue; - } - var latestParent = spawnedNetObj.GetNetworkParenting(); - // Only deparent the first generation children of the NetworkObject being spawned. - // Ignore any nested children under first generation children. - if (latestParent.HasValue && latestParent.Value != networkObject.NetworkObjectId) - { - continue; - } - // For mixed authority hierarchies, if the parent is despawned then any removal of children - // is considered "authority approved". If we don't have authority over the object and we are - // in distributed authority mode, then set the AuthorityAppliedParenting flag. - spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !spawnedNetObj.HasAuthority; - - // Try to remove the parent using the cached WorldPositionStays value - // Note: WorldPositionStays will still default to true if this was an - // in-scene placed NetworkObject and parenting was predefined in the - // scene via the editor. - if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays()) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed"); - } - } - else - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) - { - NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); - } - } - } - - networkObject.InvokeBehaviourNetworkDespawn(); - - if (NetworkManager != null && ((NetworkManager.IsServer && (!distributedAuthority || - (distributedAuthority && modeDestroy))) || - (distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId))) - { - if (NetworkManager.NetworkConfig.RecycleNetworkIds) - { - ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() - { - NetworkId = networkObject.NetworkObjectId, - ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime - }); - } - m_TargetClientIds.Clear(); - - // If clients are not allowed to spawn locally then go ahead and send the despawn message or if we are in distributed authority mode, we are the server, we own this NetworkObject - // send the despawn message, and as long as we have any remaining clients, then notify of the object being destroy. - if (NetworkManager.IsServer && NetworkManager.ConnectedClientsList.Count > 0 && (!distributedAuthority || - (NetworkManager.DAHost && distributedAuthority && - (networkObject.OwnerClientId == NetworkManager.LocalClientId || modeDestroy)))) - { - // We keep only the client for which the object is visible - // as the other clients have them already despawned - foreach (var clientId in NetworkManager.ConnectedClientsIds) - { - if ((distributedAuthority && clientId == networkObject.OwnerClientId) || clientId == NetworkManager.LocalClientId) - { - continue; - } - if (networkObject.IsNetworkVisibleTo(clientId)) - { - m_TargetClientIds.Add(clientId); - } - } - } - else // DANGO-TODO: If we are not the server, distributed authority mode is enabled, and we are the owner then inform the DAHost to despawn the NetworkObject - if (!NetworkManager.IsServer && distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId) - { - // DANGO-TODO: If a shutdown is not in progress or a shutdown is in progress and we can destroy with the owner then notify the DAHost - if (!NetworkManager.ShutdownInProgress || (NetworkManager.ShutdownInProgress && !networkObject.DontDestroyWithOwner)) - { - m_TargetClientIds.Add(NetworkManager.ServerClientId); - } - } - - if (m_TargetClientIds.Count > 0 && !NetworkManager.ShutdownInProgress) - { - var message = new DestroyObjectMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - DeferredDespawnTick = networkObject.DeferredDespawnTick, - DestroyGameObject = destroyGameObject, - IsTargetedDestroy = false, - IsDistributedAuthority = distributedAuthority, - }; - foreach (var clientId in m_TargetClientIds) - { - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, networkObject, size); - } - } - } - - networkObject.IsSpawned = false; - networkObject.DeferredDespawnTick = 0; - - if (SpawnedObjects.Remove(networkObject.NetworkObjectId)) - { - SpawnedObjectsList.Remove(networkObject); - } - - if (networkObject.IsPlayerObject) - { - RemovePlayerObject(networkObject, destroyGameObject); - } - - // Always clear out the observers list when despawned - networkObject.Observers.Clear(); - - var gobj = networkObject.gameObject; - if (destroyGameObject && gobj != null) - { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); - } - else - { - UnityEngine.Object.Destroy(gobj); - } - } - } - - /// - /// Updates all spawned for the specified newly connected client - /// Note: if the clientId is the server then it is observable to all spawned 's - /// - /// - /// This method is to only to be used for newly connected clients in order to update the observers list for - /// each NetworkObject instance. - /// - internal void UpdateObservedNetworkObjects(ulong clientId) - { - foreach (var sobj in SpawnedObjectsList) - { - // If the NetworkObject has no visibility check then prepare to add this client as an observer - if (sobj.CheckObjectVisibility == null) - { - // If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server/host/DAHost - if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId)) - { - sobj.Observers.Add(clientId); - } - } - else - { - // CheckObject visibility overrides SpawnWithObservers under this condition - if (sobj.CheckObjectVisibility(clientId)) - { - sobj.Observers.Add(clientId); - } - else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false - { - sobj.Observers.Remove(clientId); - } - } - } - } - - /// - /// See - /// - internal void HandleNetworkObjectShow() - { - // In distributed authority mode, we send a single message that is broadcasted to all clients - // that will be shown the object (i.e. 1 message to service that then broadcasts that to the - // targeted clients). When using a DAHost, we skip this and send like we do in client-server - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) - { - foreach (var entry in ClientsToShowObject) - { - if (entry.Key != null && entry.Key.IsSpawned) - { - try - { - SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); - } - catch (Exception ex) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogException(ex); - } - } - } - } - ClientsToShowObject.Clear(); - ObjectsToShowToClient.Clear(); - return; - } - - // Server or Host handling of NetworkObjects to show - foreach (var client in ObjectsToShowToClient) - { - ulong clientId = client.Key; - foreach (var networkObject in client.Value) - { - if (networkObject != null && networkObject.IsSpawned) - { - try - { - SendSpawnCallForObject(clientId, networkObject); - } - catch (Exception ex) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogException(ex); - } - } - } - } - } - ObjectsToShowToClient.Clear(); - } - - internal NetworkSpawnManager(NetworkManager networkManager) - { - NetworkManager = networkManager; - } - - /// - /// Finalizer that ensures proper cleanup of spawn manager resources - /// - ~NetworkSpawnManager() - { - Shutdown(); - } - - internal void Shutdown() - { - NetworkObjectsToSynchronizeSceneChanges.Clear(); - CleanUpDisposedObjects.Clear(); - } - - /// - /// DANGO-TODO: Until we have the CMB Server end-to-end with all features verified working via integration tests, - /// I am keeping this debug toggle available. (NSS) - /// - internal bool EnableDistributeLogging = false; - - /// - /// Fills the first table passed in with the current distribution of prefab types relative to their owners - /// Fills the second table passed in with the total number of spawned objects of that particular type. - /// The second table allows us to calculate how many objects per client there should be in order to determine - /// how many of that type should be distributed. - /// - /// the table to populate - /// the total number of the specific object type to distribute - internal void GetObjectDistribution(ulong clientId, ref Dictionary>> objectByTypeAndOwner, ref Dictionary objectTypeCount) - { - // DANGO-TODO-MVP: Remove this once the service handles object distribution - var onlyIncludeOwnedObjects = NetworkManager.CMBServiceConnection; - - foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList) - { - if (networkObject.IsOwnershipSessionOwner) - { - continue; - } - - // We first check for a parent and then if the parent is a NetworkObject - if (networkObject.transform.parent != null) - { - // If the parent is a NetworkObject, has the same owner, and this NetworkObject has the distributable =or= transferable permission - // then skip this child (NetworkObjects are always parented directly under other NetworkObjects) - // (later we determine if all children with the same owner will be transferred together as a group) - var parentNetworkObject = networkObject.transform.parent.GetComponent(); - if (parentNetworkObject != null && parentNetworkObject.OwnerClientId == networkObject.OwnerClientId - && (networkObject.IsOwnershipDistributable || networkObject.IsOwnershipTransferable)) - { - continue; - } - } - - // At this point we only allow things marked with the distributable permission and is not locked to be distributed - if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked) - { - // Don't include anything that is not visible to the new client - if (!networkObject.Observers.Contains(clientId)) - { - continue; - } - - // We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance - // since all in-scene placed instances use unique GlobalObjectIdHash values. - var globalOjectIdHash = networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value ? networkObject.InScenePlacedSourceGlobalObjectIdHash : networkObject.GlobalObjectIdHash; - - if (!objectTypeCount.ContainsKey(globalOjectIdHash)) - { - objectTypeCount.Add(globalOjectIdHash, 0); - } - objectTypeCount[globalOjectIdHash] += 1; - - // DANGO-TODO-MVP: Remove this once the service handles object distribution - if (onlyIncludeOwnedObjects && !networkObject.IsOwner) - { - continue; - } - - - // Divide up by prefab type (GlobalObjectIdHash) to get a better distribution of object types - if (!objectByTypeAndOwner.ContainsKey(globalOjectIdHash)) - { - objectByTypeAndOwner.Add(globalOjectIdHash, new Dictionary>()); - } - - // Sub-divide each type by owner - if (!objectByTypeAndOwner[globalOjectIdHash].ContainsKey(networkObject.OwnerClientId)) - { - objectByTypeAndOwner[globalOjectIdHash].Add(networkObject.OwnerClientId, new List()); - } - - // Add to the client's spawned object list - objectByTypeAndOwner[globalOjectIdHash][networkObject.OwnerClientId].Add(networkObject); - } - } - } - - /// - /// Distributes an even portion of spawned NetworkObjects to the given client. - /// This is called as part of the client connection process to ensure that all clients have a fair share of the spawned NetworkObjects. - /// DANGO-TODO: This will be handled by the CMB Service in the future. - /// - /// Client to distribute NetworkObjects to - internal void DistributeNetworkObjects(ulong clientId) - { - if (!NetworkManager.DistributedAuthorityMode) - { - return; - } - - if (NetworkManager.SessionConfig.ServiceSideDistribution) - { - return; - } - - // DA-NGO CMB SERVICE NOTES: - // The most basic object distribution should be broken up into a table of spawned object types - // where each type contains a list of each client's owned objects of that type that can be - // distributed. - // The table format: - // [GlobalObjectIdHashValue][ClientId][List of Owned Objects] - var distributedNetworkObjects = new Dictionary>>(); - - // DA-NGO CMB SERVICE NOTES: - // This is optional, but I found it easier to get the total count of spawned objects for each prefab - // type contained in the previous table in order to be able to calculate the targeted object distribution - // count of that type per client. - var objectTypeCount = new Dictionary(); - - // Get all spawned objects by type and then by client owner that are spawned and can be distributed - GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); - - var clientCount = NetworkManager.ConnectedClientsIds.Count; - - // Cycle through each prefab type - foreach (var objectTypeEntry in distributedNetworkObjects) - { - // Calculate the number of objects that should be distributed amongst the clients - var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key]; - var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount); - var floorValue = (int)Math.Floor(objPerClientF); - var fractional = objPerClientF - floorValue; - var objPerClient = 0; - if (fractional >= 0.556f) - { - objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount)); - } - else - { - objPerClient = floorValue; - } - - // If the object per client count is zero, then move to the next type. - if (objPerClient <= 0) - { - continue; - } - - // Evenly distribute this object type amongst the clients - foreach (var ownerList in objectTypeEntry.Value) - { - if (ownerList.Value.Count <= 1) - { - continue; - } - - var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1); - var distributed = 0; - // For now when we have more players then distributed NetworkObjects that - // a specific client owns, just assign half of the NetworkObjects to the new client - var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1); - if (EnableDistributeLogging) - { - Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}"); - } - - for (int i = 0; i < ownerList.Value.Count; i++) - { - if ((i % offsetCount) == 0) - { - var children = ownerList.Value[i].GetComponentsInChildren(); - // Since the ownerList.Value[i] has to be distributable, then transfer all child NetworkObjects - // with the same owner clientId and are marked as distributable also to the same client to keep - // the owned distributable parent with the owned distributable children - foreach (var child in children) - { - // Ignore any child that does not have the same owner, that is already owned by the currently targeted client, or that doesn't have the targeted client as an observer - if (child == ownerList.Value[i] || child.OwnerClientId != ownerList.Value[i].OwnerClientId || child.OwnerClientId == clientId || !child.Observers.Contains(clientId)) - { - continue; - } - if (!child.IsOwnershipDistributable || !child.IsOwnershipTransferable) - { - NetworkLog.LogWarning($"Sibling {child.name} of root parent {ownerList.Value[i].name} is neither transferable or distributable! Object distribution skipped and could lead to a potentially un-owned or owner-mismatched {nameof(NetworkObject)}!"); - continue; - } - // Transfer ownership of all distributable =or= transferable children with the same owner to the same client to preserve the sibling ownership tree. - ChangeOwnership(child, clientId, true); - // Note: We don't increment the distributed count for these children as they are skipped when getting the object distribution - } - // Finally, transfer ownership of the root parent - ChangeOwnership(ownerList.Value[i], clientId, true); - if (EnableDistributeLogging) - { - Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}"); - } - distributed++; - } - if (distributed == maxDistributeCount) - { - break; - } - } - } - } - - // If EnableDistributeLogging is enabled, log the object type distribution counts per client - if (EnableDistributeLogging) - { - var builder = new StringBuilder(); - distributedNetworkObjects.Clear(); - objectTypeCount.Clear(); - GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); - builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)"); - // Cycle through each prefab type - foreach (var objectTypeEntry in distributedNetworkObjects) - { - builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]"); - builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:"); - // Evenly distribute this type amongst clients - foreach (var ownerList in objectTypeEntry.Value) - { - builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}"); - } - } - Debug.Log(builder.ToString()); - } - } - - internal struct DeferredDespawnObject - { - public int TickToDespawn; - public bool HasDeferredDespawnCheck; - public ulong NetworkObjectId; - } - - internal List DeferredDespawnObjects = new List(); - - /// - /// Adds a deferred despawn entry to be processed - /// - /// associated NetworkObject - /// when to despawn the NetworkObject - /// if true, user script is to be invoked to determine when to despawn - internal void DeferDespawnNetworkObject(ulong networkObjectId, int tickToDespawn, bool hasDeferredDespawnCheck) - { - var deferredDespawnObject = new DeferredDespawnObject() - { - TickToDespawn = tickToDespawn, - HasDeferredDespawnCheck = hasDeferredDespawnCheck, - NetworkObjectId = networkObjectId, - }; - DeferredDespawnObjects.Add(deferredDespawnObject); - } - - /// - /// Processes any deferred despawn entries - /// - internal void DeferredDespawnUpdate(NetworkTime serverTime) - { - // Exit early if there is nothing to process - if (DeferredDespawnObjects.Count == 0) - { - return; - } - var currentTick = serverTime.Tick; - var deferredDespawnCount = DeferredDespawnObjects.Count; - // Loop forward and to process user callbacks and update despawn ticks - for (int i = 0; i < deferredDespawnCount; i++) - { - var deferredObjectEntry = DeferredDespawnObjects[i]; - if (!deferredObjectEntry.HasDeferredDespawnCheck) - { - continue; - } - - if (!SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) - { - continue; - } - - // Double check to make sure user did not remove the callback - if (networkObject.OnDeferredDespawnComplete != null) - { - // If the user callback returns true, then we despawn it this tick - if (networkObject.OnDeferredDespawnComplete.Invoke()) - { - deferredObjectEntry.TickToDespawn = currentTick; - } - else - { - // If the user callback does not verify the NetworkObject can be despawned, - // continue setting this value in the event user script adjusts it. - deferredObjectEntry.TickToDespawn = networkObject.DeferredDespawnTick; - } - } - else - { - // If it was removed, then in case it is being left to defer naturally exclude it - // from the next query. - deferredObjectEntry.HasDeferredDespawnCheck = false; - } - } - - // Parse backwards so we can remove objects as we parse through them - for (int i = deferredDespawnCount - 1; i >= 0; i--) - { - var deferredObjectEntry = DeferredDespawnObjects[i]; - if (deferredObjectEntry.TickToDespawn >= currentTick) - { - continue; - } - - if (SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) - { - // Local instance despawns the instance - OnDespawnNonAuthorityObject(networkObject); - } - - DeferredDespawnObjects.RemoveAt(i); - } - } - - internal void NotifyNetworkObjectsSynchronized() - { - // Users could spawn NetworkObjects during these notifications. - // Create a separate list from the hashset to avoid list modification errors. - var spawnedObjects = SpawnedObjectsList.ToList(); - foreach (var networkObject in spawnedObjects) - { - networkObject.InternalNetworkSessionSynchronized(); - } - } - - /// - /// Distributed Authority Only - /// Should be invoked on non-session owner clients when a newly joined client is finished - /// synchronizing in order to "show" (spawn) anything that might be currently hidden from - /// the session owner. - /// - /// - /// Replacement is: SynchronizeObjectsToNewlyJoinedClient - /// - internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId) - { - if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); - return; - } - - if (!NetworkManager.DistributedAuthorityMode) - { - Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); - return; - } - - if (NetworkManager.LocalClient.IsSessionOwner) - { - Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!"); - return; - } - var localClientId = NetworkManager.LocalClient.ClientId; - var sessionOwnerId = NetworkManager.CurrentSessionOwner; - foreach (var networkObject in SpawnedObjectsList) - { - if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId)) - { - if (networkObject.Observers.Contains(newClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner - Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!"); - } - // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added - // code to handle this edge case works. - networkObject.Observers.Remove(newClientId); - } - networkObject.NetworkShow(newClientId); - } - } - } - - internal void SynchronizeObjectsToNewlyJoinedClient(ulong newClientId) - { - if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); - return; - } - - if (!NetworkManager.DistributedAuthorityMode) - { - Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); - return; - } - - if (NetworkManager.NetworkConfig.EnableSceneManagement) - { - Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when scene management is disabled!"); - return; - } - - var localClientId = NetworkManager.LocalClient.ClientId; - foreach (var networkObject in SpawnedObjectsList) - { - if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId) - { - if (networkObject.Observers.Contains(newClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Developer) - { - // Temporary tracking to make sure we are not showing something already visibile (should never be the case for this) - Debug.LogWarning($"[{nameof(SynchronizeObjectsToNewlyJoinedClient)}][{networkObject.name}] New client as already an observer!"); - } - // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added - // code to handle this edge case works. - networkObject.Observers.Remove(newClientId); - } - networkObject.NetworkShow(newClientId); - } - } - } - } + var networkObjectsToSpawn = new List(); + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + // This used to be two loops. + // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. + // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. + if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) + { + var ownerId = networkObjects[i].OwnerClientId; + if (NetworkManager.DistributedAuthorityMode) + { + ownerId = NetworkManager.LocalClientId; + } + + SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); + networkObjectsToSpawn.Add(networkObjects[i]); + } + } + } + + // Since we are spawing in-scene placed NetworkObjects for already loaded scenes, + // we need to add any in-scene placed NetworkObject to our tracking table + var clearFirst = true; + foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded) + { + NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst); + clearFirst = false; + } + + // Notify all in-scene placed NetworkObjects have been spawned + foreach (var networkObject in networkObjectsToSpawn) + { + networkObject.InternalInSceneNetworkObjectsSpawned(); + } + networkObjectsToSpawn.Clear(); + } + + /// + /// Called when destroying an object after receiving a . + /// Processes logic for how to destroy objects on the non-authority client. + /// + internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject) + { + if (networkObject.HasAuthority) + { + NetworkLog.LogError($"OnDespawnNonAuthorityObject called on object {networkObject.NetworkObjectId} when is current client {NetworkManager.LocalClientId} has authority on this object."); + } + + // On the non-authority, never destroy the game object when InScenePlaced, otherwise always destroy on non-authority side + OnDespawnObject(networkObject, networkObject.IsSceneObject == false); + } + + internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject, bool modeDestroy = false) + { + if (!NetworkManager) + { + return; + } + + // We have to do this check first as subsequent checks assume we can access NetworkObjectId. + if (!networkObject) + { + Debug.LogWarning($"Trying to destroy network object but it is null"); + return; + } + + // Removal of spawned object + if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId)) + { + if (!NetworkManager.ShutdownInProgress) + { + Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!"); + } + return; + } + + // If we are shutting down the NetworkManager, then ignore resetting the parent + // and only attempt to remove the child's parent on the server-side + var distributedAuthority = NetworkManager.DistributedAuthorityMode; + if (!NetworkManager.ShutdownInProgress && (NetworkManager.IsServer || distributedAuthority)) + { + // Get all child NetworkObjects + var objectsToRemoveParent = networkObject.GetComponentsInChildren(); + + // Move child NetworkObjects to the root when parent NetworkObject is destroyed + foreach (var spawnedNetObj in objectsToRemoveParent) + { + if (spawnedNetObj == networkObject) + { + continue; + } + var latestParent = spawnedNetObj.GetNetworkParenting(); + // Only deparent the first generation children of the NetworkObject being spawned. + // Ignore any nested children under first generation children. + if (latestParent.HasValue && latestParent.Value != networkObject.NetworkObjectId) + { + continue; + } + // For mixed authority hierarchies, if the parent is despawned then any removal of children + // is considered "authority approved". If we don't have authority over the object and we are + // in distributed authority mode, then set the AuthorityAppliedParenting flag. + spawnedNetObj.AuthorityAppliedParenting = distributedAuthority && !spawnedNetObj.HasAuthority; + + // Try to remove the parent using the cached WorldPositionStays value + // Note: WorldPositionStays will still default to true if this was an + // in-scene placed NetworkObject and parenting was predefined in the + // scene via the editor. + if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays()) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed"); + } + } + else + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + } + } + } + + networkObject.InvokeBehaviourNetworkDespawn(); + + if (NetworkManager != null && ((NetworkManager.IsServer && (!distributedAuthority || + (distributedAuthority && modeDestroy))) || + (distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId))) + { + if (NetworkManager.NetworkConfig.RecycleNetworkIds) + { + ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() + { + NetworkId = networkObject.NetworkObjectId, + ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime + }); + } + m_TargetClientIds.Clear(); + + // If clients are not allowed to spawn locally then go ahead and send the despawn message or if we are in distributed authority mode, we are the server, we own this NetworkObject + // send the despawn message, and as long as we have any remaining clients, then notify of the object being destroy. + if (NetworkManager.IsServer && NetworkManager.ConnectedClientsList.Count > 0 && (!distributedAuthority || + (NetworkManager.DAHost && distributedAuthority && + (networkObject.OwnerClientId == NetworkManager.LocalClientId || modeDestroy)))) + { + // We keep only the client for which the object is visible + // as the other clients have them already despawned + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if ((distributedAuthority && clientId == networkObject.OwnerClientId) || clientId == NetworkManager.LocalClientId) + { + continue; + } + if (networkObject.IsNetworkVisibleTo(clientId)) + { + m_TargetClientIds.Add(clientId); + } + } + } + else // DANGO-TODO: If we are not the server, distributed authority mode is enabled, and we are the owner then inform the DAHost to despawn the NetworkObject + if (!NetworkManager.IsServer && distributedAuthority && networkObject.OwnerClientId == NetworkManager.LocalClientId) + { + // DANGO-TODO: If a shutdown is not in progress or a shutdown is in progress and we can destroy with the owner then notify the DAHost + if (!NetworkManager.ShutdownInProgress || (NetworkManager.ShutdownInProgress && !networkObject.DontDestroyWithOwner)) + { + m_TargetClientIds.Add(NetworkManager.ServerClientId); + } + } + + if (m_TargetClientIds.Count > 0 && !NetworkManager.ShutdownInProgress) + { + var message = new DestroyObjectMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + DeferredDespawnTick = networkObject.DeferredDespawnTick, + DestroyGameObject = destroyGameObject, + IsTargetedDestroy = false, + IsDistributedAuthority = distributedAuthority, + }; + foreach (var clientId in m_TargetClientIds) + { + var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, networkObject, size); + } + } + } + + networkObject.IsSpawned = false; + networkObject.DeferredDespawnTick = 0; + + if (SpawnedObjects.Remove(networkObject.NetworkObjectId)) + { + SpawnedObjectsList.Remove(networkObject); + } + + if (networkObject.IsPlayerObject) + { + RemovePlayerObject(networkObject, destroyGameObject); + } + + // Always clear out the observers list when despawned + networkObject.Observers.Clear(); + + var gobj = networkObject.gameObject; + if (destroyGameObject && gobj != null) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); + } + else + { + UnityEngine.Object.Destroy(gobj); + } + } + } + + /// + /// Updates all spawned for the specified newly connected client + /// Note: if the clientId is the server then it is observable to all spawned 's + /// + /// + /// This method is to only to be used for newly connected clients in order to update the observers list for + /// each NetworkObject instance. + /// + internal void UpdateObservedNetworkObjects(ulong clientId) + { + foreach (var sobj in SpawnedObjectsList) + { + // If the NetworkObject has no visibility check then prepare to add this client as an observer + if (sobj.CheckObjectVisibility == null) + { + // If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server/host/DAHost + if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId)) + { + sobj.Observers.Add(clientId); + } + } + else + { + // CheckObject visibility overrides SpawnWithObservers under this condition + if (sobj.CheckObjectVisibility(clientId)) + { + sobj.Observers.Add(clientId); + } + else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false + { + sobj.Observers.Remove(clientId); + } + } + } + } + + /// + /// See + /// + internal void HandleNetworkObjectShow() + { + // In distributed authority mode, we send a single message that is broadcasted to all clients + // that will be shown the object (i.e. 1 message to service that then broadcasts that to the + // targeted clients). When using a DAHost, we skip this and send like we do in client-server + if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) + { + foreach (var entry in ClientsToShowObject) + { + if (entry.Key != null && entry.Key.IsSpawned) + { + try + { + SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } + } + ClientsToShowObject.Clear(); + ObjectsToShowToClient.Clear(); + return; + } + + // Server or Host handling of NetworkObjects to show + foreach (var client in ObjectsToShowToClient) + { + ulong clientId = client.Key; + foreach (var networkObject in client.Value) + { + if (networkObject != null && networkObject.IsSpawned) + { + try + { + SendSpawnCallForObject(clientId, networkObject); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } + } + } + ObjectsToShowToClient.Clear(); + } + + internal NetworkSpawnManager(NetworkManager networkManager) + { + NetworkManager = networkManager; + } + + /// + /// Finalizer that ensures proper cleanup of spawn manager resources + /// + ~NetworkSpawnManager() + { + Shutdown(); + } + + internal void Shutdown() + { + NetworkObjectsToSynchronizeSceneChanges.Clear(); + CleanUpDisposedObjects.Clear(); + } + + /// + /// DANGO-TODO: Until we have the CMB Server end-to-end with all features verified working via integration tests, + /// I am keeping this debug toggle available. (NSS) + /// + internal bool EnableDistributeLogging = false; + + /// + /// Fills the first table passed in with the current distribution of prefab types relative to their owners + /// Fills the second table passed in with the total number of spawned objects of that particular type. + /// The second table allows us to calculate how many objects per client there should be in order to determine + /// how many of that type should be distributed. + /// + /// the table to populate + /// the total number of the specific object type to distribute + internal void GetObjectDistribution(ulong clientId, ref Dictionary>> objectByTypeAndOwner, ref Dictionary objectTypeCount) + { + // DANGO-TODO-MVP: Remove this once the service handles object distribution + var onlyIncludeOwnedObjects = NetworkManager.CMBServiceConnection; + + foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList) + { + if (networkObject.IsOwnershipSessionOwner) + { + continue; + } + + // We first check for a parent and then if the parent is a NetworkObject + if (networkObject.transform.parent != null) + { + // If the parent is a NetworkObject, has the same owner, and this NetworkObject has the distributable =or= transferable permission + // then skip this child (NetworkObjects are always parented directly under other NetworkObjects) + // (later we determine if all children with the same owner will be transferred together as a group) + var parentNetworkObject = networkObject.transform.parent.GetComponent(); + if (parentNetworkObject != null && parentNetworkObject.OwnerClientId == networkObject.OwnerClientId + && (networkObject.IsOwnershipDistributable || networkObject.IsOwnershipTransferable)) + { + continue; + } + } + + // At this point we only allow things marked with the distributable permission and is not locked to be distributed + if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked) + { + // Don't include anything that is not visible to the new client + if (!networkObject.Observers.Contains(clientId)) + { + continue; + } + + // We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance + // since all in-scene placed instances use unique GlobalObjectIdHash values. + var globalOjectIdHash = networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value ? networkObject.InScenePlacedSourceGlobalObjectIdHash : networkObject.GlobalObjectIdHash; + + if (!objectTypeCount.ContainsKey(globalOjectIdHash)) + { + objectTypeCount.Add(globalOjectIdHash, 0); + } + objectTypeCount[globalOjectIdHash] += 1; + + // DANGO-TODO-MVP: Remove this once the service handles object distribution + if (onlyIncludeOwnedObjects && !networkObject.IsOwner) + { + continue; + } + + + // Divide up by prefab type (GlobalObjectIdHash) to get a better distribution of object types + if (!objectByTypeAndOwner.ContainsKey(globalOjectIdHash)) + { + objectByTypeAndOwner.Add(globalOjectIdHash, new Dictionary>()); + } + + // Sub-divide each type by owner + if (!objectByTypeAndOwner[globalOjectIdHash].ContainsKey(networkObject.OwnerClientId)) + { + objectByTypeAndOwner[globalOjectIdHash].Add(networkObject.OwnerClientId, new List()); + } + + // Add to the client's spawned object list + objectByTypeAndOwner[globalOjectIdHash][networkObject.OwnerClientId].Add(networkObject); + } + } + } + + /// + /// Distributes an even portion of spawned NetworkObjects to the given client. + /// This is called as part of the client connection process to ensure that all clients have a fair share of the spawned NetworkObjects. + /// DANGO-TODO: This will be handled by the CMB Service in the future. + /// + /// Client to distribute NetworkObjects to + internal void DistributeNetworkObjects(ulong clientId) + { + if (!NetworkManager.DistributedAuthorityMode) + { + return; + } + + if (NetworkManager.SessionConfig.ServiceSideDistribution) + { + return; + } + + // DA-NGO CMB SERVICE NOTES: + // The most basic object distribution should be broken up into a table of spawned object types + // where each type contains a list of each client's owned objects of that type that can be + // distributed. + // The table format: + // [GlobalObjectIdHashValue][ClientId][List of Owned Objects] + var distributedNetworkObjects = new Dictionary>>(); + + // DA-NGO CMB SERVICE NOTES: + // This is optional, but I found it easier to get the total count of spawned objects for each prefab + // type contained in the previous table in order to be able to calculate the targeted object distribution + // count of that type per client. + var objectTypeCount = new Dictionary(); + + // Get all spawned objects by type and then by client owner that are spawned and can be distributed + GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); + + var clientCount = NetworkManager.ConnectedClientsIds.Count; + + // Cycle through each prefab type + foreach (var objectTypeEntry in distributedNetworkObjects) + { + // Calculate the number of objects that should be distributed amongst the clients + var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key]; + var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount); + var floorValue = (int)Math.Floor(objPerClientF); + var fractional = objPerClientF - floorValue; + var objPerClient = 0; + if (fractional >= 0.556f) + { + objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount)); + } + else + { + objPerClient = floorValue; + } + + // If the object per client count is zero, then move to the next type. + if (objPerClient <= 0) + { + continue; + } + + // Evenly distribute this object type amongst the clients + foreach (var ownerList in objectTypeEntry.Value) + { + if (ownerList.Value.Count <= 1) + { + continue; + } + + var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1); + var distributed = 0; + // For now when we have more players then distributed NetworkObjects that + // a specific client owns, just assign half of the NetworkObjects to the new client + var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1); + if (EnableDistributeLogging) + { + Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}"); + } + + for (int i = 0; i < ownerList.Value.Count; i++) + { + if ((i % offsetCount) == 0) + { + var children = ownerList.Value[i].GetComponentsInChildren(); + // Since the ownerList.Value[i] has to be distributable, then transfer all child NetworkObjects + // with the same owner clientId and are marked as distributable also to the same client to keep + // the owned distributable parent with the owned distributable children + foreach (var child in children) + { + // Ignore any child that does not have the same owner, that is already owned by the currently targeted client, or that doesn't have the targeted client as an observer + if (child == ownerList.Value[i] || child.OwnerClientId != ownerList.Value[i].OwnerClientId || child.OwnerClientId == clientId || !child.Observers.Contains(clientId)) + { + continue; + } + if (!child.IsOwnershipDistributable || !child.IsOwnershipTransferable) + { + NetworkLog.LogWarning($"Sibling {child.name} of root parent {ownerList.Value[i].name} is neither transferable or distributable! Object distribution skipped and could lead to a potentially un-owned or owner-mismatched {nameof(NetworkObject)}!"); + continue; + } + // Transfer ownership of all distributable =or= transferable children with the same owner to the same client to preserve the sibling ownership tree. + ChangeOwnership(child, clientId, true); + // Note: We don't increment the distributed count for these children as they are skipped when getting the object distribution + } + // Finally, transfer ownership of the root parent + ChangeOwnership(ownerList.Value[i], clientId, true); + if (EnableDistributeLogging) + { + Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}"); + } + distributed++; + } + if (distributed == maxDistributeCount) + { + break; + } + } + } + } + + // If EnableDistributeLogging is enabled, log the object type distribution counts per client + if (EnableDistributeLogging) + { + var builder = new StringBuilder(); + distributedNetworkObjects.Clear(); + objectTypeCount.Clear(); + GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount); + builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)"); + // Cycle through each prefab type + foreach (var objectTypeEntry in distributedNetworkObjects) + { + builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]"); + builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:"); + // Evenly distribute this type amongst clients + foreach (var ownerList in objectTypeEntry.Value) + { + builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}"); + } + } + Debug.Log(builder.ToString()); + } + } + + internal struct DeferredDespawnObject + { + public int TickToDespawn; + public bool HasDeferredDespawnCheck; + public ulong NetworkObjectId; + } + + internal List DeferredDespawnObjects = new List(); + + /// + /// Adds a deferred despawn entry to be processed + /// + /// associated NetworkObject + /// when to despawn the NetworkObject + /// if true, user script is to be invoked to determine when to despawn + internal void DeferDespawnNetworkObject(ulong networkObjectId, int tickToDespawn, bool hasDeferredDespawnCheck) + { + var deferredDespawnObject = new DeferredDespawnObject() + { + TickToDespawn = tickToDespawn, + HasDeferredDespawnCheck = hasDeferredDespawnCheck, + NetworkObjectId = networkObjectId, + }; + DeferredDespawnObjects.Add(deferredDespawnObject); + } + + /// + /// Processes any deferred despawn entries + /// + internal void DeferredDespawnUpdate(NetworkTime serverTime) + { + // Exit early if there is nothing to process + if (DeferredDespawnObjects.Count == 0) + { + return; + } + var currentTick = serverTime.Tick; + var deferredDespawnCount = DeferredDespawnObjects.Count; + // Loop forward and to process user callbacks and update despawn ticks + for (int i = 0; i < deferredDespawnCount; i++) + { + var deferredObjectEntry = DeferredDespawnObjects[i]; + if (!deferredObjectEntry.HasDeferredDespawnCheck) + { + continue; + } + + if (!SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) + { + continue; + } + + // Double check to make sure user did not remove the callback + if (networkObject.OnDeferredDespawnComplete != null) + { + // If the user callback returns true, then we despawn it this tick + if (networkObject.OnDeferredDespawnComplete.Invoke()) + { + deferredObjectEntry.TickToDespawn = currentTick; + } + else + { + // If the user callback does not verify the NetworkObject can be despawned, + // continue setting this value in the event user script adjusts it. + deferredObjectEntry.TickToDespawn = networkObject.DeferredDespawnTick; + } + } + else + { + // If it was removed, then in case it is being left to defer naturally exclude it + // from the next query. + deferredObjectEntry.HasDeferredDespawnCheck = false; + } + } + + // Parse backwards so we can remove objects as we parse through them + for (int i = deferredDespawnCount - 1; i >= 0; i--) + { + var deferredObjectEntry = DeferredDespawnObjects[i]; + if (deferredObjectEntry.TickToDespawn >= currentTick) + { + continue; + } + + if (SpawnedObjects.TryGetValue(deferredObjectEntry.NetworkObjectId, out var networkObject)) + { + // Local instance despawns the instance + OnDespawnNonAuthorityObject(networkObject); + } + + DeferredDespawnObjects.RemoveAt(i); + } + } + + internal void NotifyNetworkObjectsSynchronized() + { + // Users could spawn NetworkObjects during these notifications. + // Create a separate list from the hashset to avoid list modification errors. + var spawnedObjects = SpawnedObjectsList.ToList(); + foreach (var networkObject in spawnedObjects) + { + networkObject.InternalNetworkSessionSynchronized(); + } + } + + /// + /// Distributed Authority Only + /// Should be invoked on non-session owner clients when a newly joined client is finished + /// synchronizing in order to "show" (spawn) anything that might be currently hidden from + /// the session owner. + /// + /// + /// Replacement is: SynchronizeObjectsToNewlyJoinedClient + /// + internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId) + { + if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); + return; + } + + if (!NetworkManager.DistributedAuthorityMode) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); + return; + } + + if (NetworkManager.LocalClient.IsSessionOwner) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!"); + return; + } + var localClientId = NetworkManager.LocalClient.ClientId; + var sessionOwnerId = NetworkManager.CurrentSessionOwner; + foreach (var networkObject in SpawnedObjectsList) + { + if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId)) + { + if (networkObject.Observers.Contains(newClientId)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner + Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!"); + } + // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added + // code to handle this edge case works. + networkObject.Observers.Remove(newClientId); + } + networkObject.NetworkShow(newClientId); + } + } + } + + internal void SynchronizeObjectsToNewlyJoinedClient(ulong newClientId) + { + if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!"); + return; + } + + if (!NetworkManager.DistributedAuthorityMode) + { + Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); + return; + } + + if (NetworkManager.NetworkConfig.EnableSceneManagement) + { + Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when scene management is disabled!"); + return; + } + + var localClientId = NetworkManager.LocalClient.ClientId; + foreach (var networkObject in SpawnedObjectsList) + { + if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId) + { + if (networkObject.Observers.Contains(newClientId)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + // Temporary tracking to make sure we are not showing something already visibile (should never be the case for this) + Debug.LogWarning($"[{nameof(SynchronizeObjectsToNewlyJoinedClient)}][{networkObject.name}] New client as already an observer!"); + } + // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added + // code to handle this edge case works. + networkObject.Observers.Remove(newClientId); + } + networkObject.NetworkShow(newClientId); + } + } + } + } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs index d93b68f20d..50fa3636dd 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs @@ -110,9 +110,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) //Test result of registering via GameObject reference Assert.True(gameObjectRegistered); - var bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - Debug.Log("SENDING Writter BUFFFER Tests"); - var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref bufferReaderSerializer, prefabPosition, prefabRotation); + var instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -137,9 +136,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(2.0f, 1.0f, 5.0f); prefabRotation = new Quaternion(4.0f, 1.5f, 5.4f, 5.1f); - bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - Debug.Log("SENDING Writter BUFFFER Tests"); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0,ref bufferReaderSerializer, prefabPosition, prefabRotation); + instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0,ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -164,9 +162,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(6.0f, 4.0f, 1.0f); prefabRotation = new Quaternion(3f, 2f, 4f, 1f); - bufferReaderSerializer = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - Debug.Log("SENDING Writter BUFFFER Tests"); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref bufferReaderSerializer, prefabPosition, prefabRotation); + instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); From 458657e40f0e00115976bfd9cbb3292e5d622d37 Mon Sep 17 00:00:00 2001 From: Extrys Date: Sun, 27 Apr 2025 01:54:43 +0200 Subject: [PATCH 7/7] cleaning diff --- .../Runtime/Prefabs/NetworkPrefabHandlerTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs index 50fa3636dd..40b48e7a35 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerTests.cs @@ -110,8 +110,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) //Test result of registering via GameObject reference Assert.True(gameObjectRegistered); - var instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); + var instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -136,8 +136,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(2.0f, 1.0f, 5.0f); prefabRotation = new Quaternion(4.0f, 1.5f, 5.4f, 5.1f); - instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0,ref instantiationPayloadWriter, prefabPosition, prefabRotation); + instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject); @@ -162,8 +162,8 @@ public void NetworkPrefabHandlerClass([Values] bool distributedAuthority) prefabPosition = new Vector3(6.0f, 4.0f, 1.0f); prefabRotation = new Quaternion(3f, 2f, 4f, 1f); - instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); - spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); + instantiationPayloadWriter = new BufferSerializer(new BufferSerializerWriter(new FastBufferWriter(0, Collections.Allocator.Temp))); + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, ref instantiationPayloadWriter, prefabPosition, prefabRotation); //Test that something was instantiated Assert.NotNull(spawnedObject);