using FishNet.Connection; using FishNet.Managing.Logging; using FishNet.Managing.Object; using FishNet.Object; using FishNet.Object.Helping; using FishNet.Serializing; using FishNet.Utility.Extension; using FishNet.Utility.Performance; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Scripting; namespace FishNet.Managing.Client { /// /// Information about cached network objects. /// internal class ClientObjectCache { #region Types. public enum CacheSearchType { Any = 0, Spawning = 1, Despawning = 2 } #endregion #region Internal. /// /// Objets which are being spawned during iteration. /// internal Dictionary SpawningObjects = new Dictionary(); #endregion #region Private. /// /// Cached objects buffer. Contains spawns and despawns. /// private ListCache _cachedObjects = new ListCache(); /// /// NetworkObjects which have been spawned already during the current iteration. /// private HashSet _iteratedSpawns = new HashSet(); /// /// Despawns which are occurring the same tick as their spawn. /// private HashSet _conflictingDespawns = new HashSet(); /// /// ClientObjects reference. /// private ClientObjects _clientObjects; /// /// NetworkManager for this cache. /// private NetworkManager _networkManager; /// /// True if logged the warning about despawning on the same tick as the spawn. /// This exist to prevent excessive spam of the warning. /// private bool _loggedSameTickWarning; /// /// True if initializeOrder was not default for any spawned objects. /// private bool _initializeOrderChanged; #endregion public ClientObjectCache(ClientObjects cobs, NetworkManager networkManager) { _clientObjects = cobs; _networkManager = networkManager; } /// /// Returns a NetworkObject found in spawned cache using objectId. /// /// /// public NetworkObject GetInCached(int objectId, CacheSearchType searchType) { int count = _cachedObjects.Written; List collection = _cachedObjects.Collection; for (int i = 0; i < count; i++) { CachedNetworkObject cnob = collection[i]; if (cnob.ObjectId == objectId) { //Any condition always returns. if (searchType == CacheSearchType.Any) return cnob.NetworkObject; bool spawning = (searchType == CacheSearchType.Spawning); bool spawnAction = (cnob.Action == CachedNetworkObject.ActionType.Spawn); if (spawning == spawnAction) return cnob.NetworkObject; else return null; } } //Fall through. return null; } /// /// Initializes for a spawned NetworkObject. /// /// /// /// public void AddSpawn(NetworkManager manager, ushort collectionId, int objectId, sbyte initializeOrder, int ownerId, SpawnType ost, byte componentIndex, int rootObjectId, int? parentObjectId, byte? parentComponentIndex , int? prefabId, Vector3? localPosition, Quaternion? localRotation, Vector3? localScale, ulong sceneId, ArraySegment rpcLinks, ArraySegment syncValues) { //Set if initialization order has changed. _initializeOrderChanged |= (initializeOrder != 0); CachedNetworkObject cnob = null; //If order has not changed then add normally. if (!_initializeOrderChanged) { cnob = _cachedObjects.AddReference(); } //Otherwise see if values need to be sorted. else { /* Spawns will be ordered at the end of their nearest order. * If spawns arrived with Id order of 5, 7, 2 then the result * would be as shown below... * Id 5 / order -5 * Id 7 / order -5 * Id 2 / order 0 * Not as if the values were inserted first such as... * Id 7 / order -5 * Id 5 / order -5 * Id 2 / order 0 * This is to prevent the likeliness of child nobs being out of order * as well to preserve user spawn order if they spawned multiple * objects the same which, with the same order. */ int written = _cachedObjects.Written; for (int i = 0; i < written; i++) { CachedNetworkObject item = _cachedObjects.Collection[i]; /* If item order is larger then that means * initializeOrder has reached the last entry * of its value. Insert just before item index. */ if (initializeOrder < item.InitializeOrder) { cnob = _cachedObjects.InsertReference(i); break; } } //If here and cnob is null then it was not inserted; add to end. if (cnob == null) cnob = _cachedObjects.AddReference(); } cnob.InitializeSpawn(manager, collectionId, objectId, initializeOrder, ownerId, ost, componentIndex, rootObjectId, parentObjectId, parentComponentIndex , prefabId, localPosition, localRotation, localScale, sceneId, rpcLinks, syncValues); } public void AddDespawn(int objectId, DespawnType despawnType) { CachedNetworkObject cnob = _cachedObjects.AddReference(); cnob.InitializeDespawn(objectId, despawnType); } /// /// Iterates any written objects. /// public void Iterate() { int written = _cachedObjects.Written; if (written == 0) return; try { //Indexes which have already been processed. HashSet processedIndexes = new HashSet(); List collection = _cachedObjects.Collection; _conflictingDespawns.Clear(); /* The next iteration will set rpclinks, * synctypes, and so on. */ for (int i = 0; i < written; i++) { /* An index may already be processed if it was pushed ahead. * This can occur if a nested object spawn exists but the root * object has not spawned yet. In this situation the root spawn is * found and performed first. */ if (processedIndexes.Contains(i)) continue; CachedNetworkObject cnob = collection[i]; bool spawn = (cnob.Action == CachedNetworkObject.ActionType.Spawn); /* See if nested, and if so check if root is already spawned. * If parent is not spawned then find it and process the parent first. */ if (spawn) { /* When an object is nested or has a parent it is * dependent upon either the root of nested, or the parent, * being spawned to setup properly. * * When either of these are true check spawned objects first * to see if the objects exist. If not check if they are appearing * later in the cache. Root or parent objects can appear later * in the cache depending on the order of which observers are rebuilt. * While it is possible to have the server ensure spawns always send * root/parents first, that's a giant can of worms that's not worth getting into. * Not only are there many scenarios to cover, but it also puts more work * on the server. It's more effective to have the client handle the sorting. */ //Nested. if (cnob.IsNested || cnob.HasParent) { bool nested = cnob.IsNested; //It's not possible to be nested and have a parent. Set the Id to look for based on if nested or parented. int targetObjectId = (nested) ? cnob.RootObjectId : cnob.ParentObjectId.Value; NetworkObject nob = GetSpawnedObject(targetObjectId); //If not spawned yet. if (nob == null) { bool found = false; string errMsg; for (int z = (i + 1); z < written; z++) { CachedNetworkObject zCnob = collection[z]; if (zCnob.ObjectId == targetObjectId) { found = true; if (cnob.Action != CachedNetworkObject.ActionType.Spawn) { errMsg = (nested) ? $"ObjectId {targetObjectId} was found for a nested spawn, but ActionType is not spawn. ComponentIndex {cnob.ComponentIndex} will not be spawned." : $"ObjectId {targetObjectId} was found for a parented spawn, but ActionType is not spawn. ObjectId {cnob.ObjectId} will not be spawned."; _networkManager.LogError(errMsg); break; } else { ProcessObject(zCnob, true, z); break; } } } //Root nob could not be found. if (!found) { errMsg = (nested) ? $"ObjectId {targetObjectId} could not be found for a nested spawn. ComponentIndex {cnob.ComponentIndex} will not be spawned." : $"ObjectId {targetObjectId} was found for a parented spawn. ObjectId {cnob.ObjectId} will not be spawned."; _networkManager.LogError(errMsg); } } } } ProcessObject(cnob, spawn, i); } void ProcessObject(CachedNetworkObject cnob, bool spawn, int index) { processedIndexes.Add(index); if (spawn) { if (cnob.IsSceneObject) cnob.NetworkObject = _clientObjects.GetSceneNetworkObject(cnob.SceneId); else if (cnob.IsNested) cnob.NetworkObject = _clientObjects.GetNestedNetworkObject(cnob); else cnob.NetworkObject = _clientObjects.GetInstantiatedNetworkObject(cnob); /* Apply transform changes but only if not host. * These would have already been applied server side. */ if (!_networkManager.IsHost && cnob.NetworkObject != null) { Transform t = cnob.NetworkObject.transform; _clientObjects.GetTransformProperties(cnob.LocalPosition, cnob.LocalRotation, cnob.LocalScale, t, out Vector3 pos, out Quaternion rot, out Vector3 scale); t.SetLocalPositionRotationAndScale(pos, rot, scale); } } else { cnob.NetworkObject = _clientObjects.GetSpawnedNetworkObject(cnob); /* Do not log unless not nested. Nested nobs sometimes * could be destroyed if parent was first. */ if (!_networkManager.IsHost && cnob.NetworkObject == null && !cnob.IsNested) _networkManager.Log($"NetworkObject for ObjectId of {cnob.ObjectId} was found null. Unable to despawn object. This may occur if a nested NetworkObject had it's parent object unexpectedly destroyed. This incident is often safe to ignore."); } NetworkObject nob = cnob.NetworkObject; //No need to error here, the other Gets above would have. if (nob == null) return; if (spawn) { //If not also server then object also has to be preinitialized. if (!_networkManager.IsServer) { int ownerId = cnob.OwnerId; //If local client is owner then use localconnection reference. NetworkConnection localConnection = _networkManager.ClientManager.Connection; NetworkConnection owner; //If owner is self. if (ownerId == localConnection.ClientId) { owner = localConnection; } else { /* If owner cannot be found then share owners * is disabled */ if (!_networkManager.ClientManager.Clients.TryGetValueIL2CPP(ownerId, out owner)) owner = NetworkManager.EmptyConnection; } nob.Preinitialize_Internal(_networkManager, cnob.ObjectId, owner, false); } _clientObjects.AddToSpawned(cnob.NetworkObject, false); SpawningObjects.Add(cnob.ObjectId, cnob.NetworkObject); IterateSpawn(cnob); _iteratedSpawns.Add(cnob.NetworkObject); /* Enable networkObject here if client only. * This is to ensure Awake fires in the same order * as InitializeOrder settings. There is no need * to perform this action if server because server * would have already spawned in order. */ if (!_networkManager.IsServer && cnob.NetworkObject != null) cnob.NetworkObject.gameObject.SetActive(true); } else { /* If spawned already this iteration then the nob * must be initialized so that the start/stop cycles * complete normally. Otherwise, the despawn callbacks will * fire immediately while the start callbacks will run after all * spawns have been iterated. * The downside to this is that synctypes * for spawns later in this iteration will not be initialized * yet, and if the nob being spawned/despawned references * those synctypes the values will be default. * * The alternative is to delay the despawning until after * all spawns are iterated, but that will break the order * reliability. This is unfortunately a lose/lose situation so * the best we can do is let the user know the risk. */ if (_iteratedSpawns.Contains(cnob.NetworkObject)) { if (!_loggedSameTickWarning) { _loggedSameTickWarning = true; _networkManager.LogWarning($"NetworkObject {cnob.NetworkObject.name} is being despawned on the same tick it's spawned." + $" When this occurs SyncTypes will not be set on other objects during the time of this despawn." + $" In result, if NetworkObject {cnob.NetworkObject.name} is referencing a SyncType of another object being spawned this tick, the returned values will be default."); } _conflictingDespawns.Add(cnob.ObjectId); cnob.NetworkObject.gameObject.SetActive(true); cnob.NetworkObject.Initialize(false, true); } //Now being initialized, despawn the object. IterateDespawn(cnob); } } /* Activate the objects after all data * has been synchronized. This will apply synctypes. */ for (int i = 0; i < written; i++) { CachedNetworkObject cnob = collection[i]; if (cnob.Action == CachedNetworkObject.ActionType.Spawn && cnob.NetworkObject != null) { /* Apply syncTypes. It's very important to do this after all * spawns have been processed and added to the manager.Objects collection. * Otherwise, the synctype may reference an object spawning the same tick * and the result would be null due to said object not being in spawned. * * At this time the NetworkObject is not initialized so by calling * OnSyncType the changes are cached to invoke callbacks after initialization, * not during the time of this action. */ foreach (NetworkBehaviour nb in cnob.NetworkObject.NetworkBehaviours) { PooledReader reader = cnob.SyncValuesReader; //SyncVars. int length = reader.ReadInt32(); nb.OnSyncType(reader, length, false); //SyncObjects length = reader.ReadInt32(); nb.OnSyncType(reader, length, true); } /* Only continue with the initialization if it wasn't initialized * early to prevent a despawn conflict. */ bool canInitialize = (!_conflictingDespawns.Contains(cnob.ObjectId) || !_iteratedSpawns.Contains(cnob.NetworkObject)); if (canInitialize) cnob.NetworkObject.Initialize(false, false); } } //Invoke synctype callbacks. for (int i = 0; i < written; i++) { CachedNetworkObject cnob = collection[i]; if (cnob.Action == CachedNetworkObject.ActionType.Spawn && cnob.NetworkObject != null) cnob.NetworkObject.InvokeSyncTypeCallbacks(false); } } finally { //Once all have been iterated reset. Reset(); } } /// /// Initializes an object on clients and spawns the NetworkObject. /// /// private void IterateSpawn(CachedNetworkObject cnob) { /* All nob spawns have been added to spawned before * they are processed. This ensures they will be found if * anything is referencing them before/after initialization. */ /* However, they have to be added again here should an ItereteDespawn * had removed them. This can occur if an object is set to be spawned, * thus added to spawned before iterations, then a despawn runs which * removes it from spawn. */ _clientObjects.AddToSpawned(cnob.NetworkObject, false); _clientObjects.ApplyRpcLinks(cnob.NetworkObject, cnob.RpcLinkReader); } /// /// Deinitializes an object on clients and despawns the NetworkObject. /// /// private void IterateDespawn(CachedNetworkObject cnob) { _clientObjects.Despawn(cnob.NetworkObject, cnob.DespawnType, false); } /// /// Returns a NetworkObject found in spawn cache, or Spawned. /// /// internal NetworkObject GetSpawnedObject(int objectId) { NetworkObject result; //If not found in Spawning then check Spawned. if (!SpawningObjects.TryGetValue(objectId, out result)) { Dictionary spawned = (_networkManager.IsHost) ? _networkManager.ServerManager.Objects.Spawned : _networkManager.ClientManager.Objects.Spawned; spawned.TryGetValue(objectId, out result); } return result; } /// /// Resets cache. /// public void Reset() { _initializeOrderChanged = false; _cachedObjects.Reset(); _iteratedSpawns.Clear(); SpawningObjects.Clear(); } } /// /// A cached network object which exist in world but has not been Initialized yet. /// [Preserve] internal class CachedNetworkObject { #region Types. public enum ActionType { Unset = 0, Spawn = 1, Despawn = 2, } #endregion /// /// True if cached object is nested. /// public bool IsNested => (ComponentIndex > 0); /// /// True if a scene object. /// public bool IsSceneObject => (SceneId > 0); /// /// True if this object has a parent. /// public bool HasParent => (ParentObjectId != null); /// /// True if the parent object is a NetworkBehaviour. /// public bool ParentIsNetworkBehaviour => (HasParent && (ParentComponentIndex != null)); public ushort CollectionId; public int ObjectId; public sbyte InitializeOrder; public int OwnerId; public SpawnType SpawnType; public DespawnType DespawnType; public byte ComponentIndex; public int RootObjectId; public int? ParentObjectId; public byte? ParentComponentIndex; public int? PrefabId; public Vector3? LocalPosition; public Quaternion? LocalRotation; public Vector3? LocalScale; public ulong SceneId; public ArraySegment RpcLinks; public ArraySegment SyncValues; /// /// True if spawning. /// public ActionType Action { get; private set; } /// /// Cached NetworkObject. /// #pragma warning disable 0649 public NetworkObject NetworkObject; /// /// Reader containing rpc links for the network object. /// public PooledReader RpcLinkReader { get; private set; } /// /// Reader containing sync values for the network object. /// public PooledReader SyncValuesReader { get; private set; } #pragma warning restore 0649 public void InitializeSpawn(NetworkManager manager, ushort collectionId, int objectId, sbyte initializeOrder, int ownerId, SpawnType objectSpawnType, byte componentIndex, int rootObjectId, int? parentObjectId, byte? parentComponentIndex , int? prefabId, Vector3? localPosition, Quaternion? localRotation, Vector3? localScale, ulong sceneId, ArraySegment rpcLinks, ArraySegment syncValues) { ResetValues(); Action = ActionType.Spawn; CollectionId = collectionId; ObjectId = objectId; InitializeOrder = initializeOrder; OwnerId = ownerId; SpawnType = objectSpawnType; ComponentIndex = componentIndex; RootObjectId = rootObjectId; ParentObjectId = parentObjectId; ParentComponentIndex = parentComponentIndex; PrefabId = prefabId; LocalPosition = localPosition; LocalRotation = localRotation; LocalScale = localScale; SceneId = sceneId; RpcLinks = rpcLinks; SyncValues = syncValues; RpcLinkReader = ReaderPool.GetReader(rpcLinks, manager); SyncValuesReader = ReaderPool.GetReader(syncValues, manager); } /// /// Initializes for a despawned NetworkObject. /// /// public void InitializeDespawn(int objectId, DespawnType despawnType) { ResetValues(); Action = ActionType.Despawn; DespawnType = despawnType; ObjectId = objectId; } /// /// Resets values which could malform identify the cached object. /// private void ResetValues() { NetworkObject = null; } ~CachedNetworkObject() { if (RpcLinkReader != null) RpcLinkReader.Dispose(); if (SyncValuesReader != null) SyncValuesReader.Dispose(); } } }