#if UNITY_EDITOR || DEVELOPMENT_BUILD #define DEVELOPMENT #endif using FishNet.Connection; using FishNet.Managing.Object; using FishNet.Managing.Timing; using FishNet.Object; using FishNet.Serializing; using FishNet.Transporting; using FishNet.Utility.Extension; using GameKit.Dependencies.Utilities; using GameKit.Dependencies.Utilities.Types; using System; using System.Collections.Generic; using FishNet.Managing.Logging; using FishNet.Object.Synchronizing; using FishNet.Serializing.Helping; using FishNet.Utility.Performance; using UnityEngine; using UnityEngine.SceneManagement; namespace FishNet.Managing.Server { /// /// Handles objects and information about objects for the server. See ManagedObjects for inherited options. /// public partial class ServerObjects : ManagedObjects { #region Public. /// /// Called right before client objects are destroyed when a client disconnects. /// public event Action OnPreDestroyClientObjects; #endregion #region Internal. /// /// Collection of NetworkObjects recently despawned. /// Key: objectId. /// Value: despawn tick. /// This is used primarily to track if client is sending messages for recently despawned objects. /// Objects are automatically removed after RECENTLY_DESPAWNED_DURATION seconds. /// internal Dictionary RecentlyDespawnedIds = new(); #endregion #region Private. /// /// Cached ObjectIds which may be used when exceeding available ObjectIds. /// private Queue _objectIdCache = new(); /// /// Returns the ObjectId cache. /// /// internal Queue GetObjectIdCache() => _objectIdCache; /// /// NetworkBehaviours which have dirty SyncObjects. /// private List _dirtySyncTypeBehaviours = new(20); /// /// Objects which need to be destroyed next tick. /// This is needed when running as host so host client will get any final messages for the object before they're destroyed. /// private HashSet _pendingDestroy = new(); /// /// NetworkObjects in a recently loaded scene. /// private List<(int, List)> _loadedSceneNetworkObjects = new List<(int frame, List nobs)>(); /// /// Cache of spawning objects, used for recursively spawning nested NetworkObjects. /// private List _spawnCache = new(); /// /// True if one or more scenes are currently loading through the SceneManager. /// private bool _scenesLoading; /// /// Number of ticks which must pass to clear a recently despawned. /// private uint _cleanRecentlyDespawnedMaxTicks => base.NetworkManager.TimeManager.TimeToTicks(30d, TickRounding.RoundUp); #endregion internal ServerObjects(NetworkManager networkManager) { base.Initialize(networkManager); networkManager.SceneManager.OnLoadStart += SceneManager_OnLoadStart; networkManager.SceneManager.OnActiveSceneSetInternal += SceneManager_OnActiveSceneSet; networkManager.TimeManager.OnUpdate += TimeManager_OnUpdate; } /// /// Called when MonoBehaviours call Update. /// private void TimeManager_OnUpdate() { if (!base.NetworkManager.IsServerStarted) { _scenesLoading = false; ClearSceneLoadedNetworkObjects(); return; } CleanRecentlyDespawned(); if (!_scenesLoading) IterateLoadedScenes(false); Observers_OnUpdate(); } /// /// Clears NetworkObjects pending initialization from a recently loaded scene. /// private void ClearSceneLoadedNetworkObjects() { for (int i = 0; i < _loadedSceneNetworkObjects.Count; i++) { (int frame, List nobs) value = _loadedSceneNetworkObjects[i]; CollectionCaches.Store(value.nobs); } _loadedSceneNetworkObjects.Clear(); } #region Checking dirty SyncTypes. /// /// Iterates NetworkBehaviours with dirty SyncTypes. /// internal void WriteDirtySyncTypes() { List collection = _dirtySyncTypeBehaviours; int colStart = collection.Count; if (colStart == 0) return; /* Tells networkbehaviours to check their * dirty synctypes. */ for (int i = 0; i < collection.Count; i++) { bool dirtyCleared = collection[i].WriteDirtySyncTypes(SyncTypeWriteFlag.Unset); if (dirtyCleared) { collection.RemoveAt(i); i--; } } } /// /// Sets that a NetworkBehaviour has a dirty syncVars. /// internal void SetDirtySyncType(NetworkBehaviour nb) { _dirtySyncTypeBehaviours.Add(nb); } #endregion #region Connection Handling. /// /// Called when the connection state changes for the local server. /// /// internal void OnServerConnectionState(ServerConnectionStateArgs args) { //If server just connected. if (args.ConnectionState == LocalConnectionState.Started) { /* If there's no servers started besides the one * that just started then build Ids and setup scene objects. */ if (base.NetworkManager.ServerManager.IsOnlyOneServerStarted()) { BuildObjectIdCache(); SetupSceneObjects(); } } //Server in anything but started state. else { //If no servers are started then reset. if (!base.NetworkManager.ServerManager.IsAnyServerStarted()) { base.DespawnWithoutSynchronization(recursive: true, asServer: true); base.SceneObjects_Internal.Clear(); _objectIdCache.Clear(); base.NetworkManager.ClearClientsCollection(base.NetworkManager.ServerManager.Clients); } //If at least one server is started then only clear for disconnecting server. else { int transportIndex = args.TransportIndex; //Remove connection from all NetworkObjects to ensure they are not stuck in observers. foreach (NetworkConnection c in base.NetworkManager.ServerManager.Clients.Values) { if (c.TransportIndex == transportIndex) RemoveFromObserversWithoutSynchronization(c); } //Remove connections only for transportIndex. base.NetworkManager.ClearClientsCollection(base.NetworkManager.ServerManager.Clients, transportIndex); } } } /// /// Called when a client disconnects. /// /// internal void ClientDisconnected(NetworkConnection connection) { RemoveFromObserversWithoutSynchronization(connection); if (OnPreDestroyClientObjects != null) OnPreDestroyClientObjects.Invoke(connection); /* A cache is made because the Objects * collection would end up modified during * iteration from removing ownership and despawning. */ List nobs = CollectionCaches.RetrieveList(); foreach (NetworkObject nob in connection.Objects) nobs.Add(nob); int nobsCount = nobs.Count; for (int i = 0; i < nobsCount; i++) { NetworkObject n = nobs[i]; /* Objects may already be deinitializing when a client disconnects * because the root object could have been despawned first, and in result * all child objects would have been recursively despawned. * * EG: object is: * A (nob) * B (nob) * * Both A and B are owned by the client so they will both be * in collection. Should A despawn first B will recursively despawn * from it. Then once that finishes and the next index of collection * is run, which would B, the object B would have already been deinitialized. */ if (!n.IsDeinitializing && !n.PreventDespawnOnDisconnect) base.NetworkManager.ServerManager.Despawn(nobs[i]); } CollectionCaches.Store(nobs); } #endregion #region ObjectIds. /// /// Builds the ObjectId cache with all possible Ids. /// private void BuildObjectIdCache() { _objectIdCache.Clear(); /* Shuffle Ids to make it more difficult * for clients to track spawned object * count. */ List shuffledCache = new(); //Ignore ushort.maxvalue as that indicates null. for (int i = 0; i < (ushort.MaxValue - 1); i++) //for (int i = 0; i < (2200); i++) //QUICK-TEST Uncomment this, and comment the line above. shuffledCache.Add(i); /* Only shuffle when NOT in editor and not * development build. * Debugging could be easier when Ids are ordered. */ #if !DEVELOPMENT shuffledCache.Shuffle(); #endif //Add shuffled to objectIdCache. //Build Id cache. int cacheCount = shuffledCache.Count; for (int i = 0; i < cacheCount; i++) _objectIdCache.Enqueue(shuffledCache[i]); } /// /// Caches a NetworkObject ObjectId. /// private void CacheObjectId(NetworkObject nob) { if (nob.ObjectId != NetworkObject.UNSET_OBJECTID_VALUE) CacheObjectId(nob.ObjectId); } /// /// Adds an ObjectId to objectId cache. /// /// internal void CacheObjectId(int id) { if (!_objectIdCache.Contains(id)) _objectIdCache.Enqueue(id); else NetworkManager.LogError($"Object Id [{id}] already exists within ObjectId Cache. Please report this error."); } /// /// Gets the next ObjectId to use for NetworkObjects. /// /// protected internal override bool GetNextNetworkObjectId(out int nextNetworkObjectId) { if (!_objectIdCache.TryDequeue(out nextNetworkObjectId)) { nextNetworkObjectId = NetworkObject.UNSET_OBJECTID_VALUE; base.NetworkManager.LogError($"No more available ObjectIds. How the heck did you manage to have {ushort.MaxValue} objects spawned at once?"); } return (nextNetworkObjectId != NetworkObject.UNSET_OBJECTID_VALUE); } #endregion #region Initializing Objects In Scenes. /// /// Called when a scene load starts. /// private void SceneManager_OnLoadStart(Scened.SceneLoadStartEventArgs obj) { _scenesLoading = true; } /// /// Called after the active scene has been scene, immediately after scene loads. /// private void SceneManager_OnActiveSceneSet(bool asServer) { _scenesLoading = false; IterateLoadedScenes(true); } /// /// Iterates loaded scenes and sets them up. /// /// True to ignore the frame restriction when iterating. internal void IterateLoadedScenes(bool ignoreFrameRestriction) { //Not started, clear loaded scenes. if (!NetworkManager.ServerManager.Started) { ClearSceneLoadedNetworkObjects(); return; } for (int i = 0; i < _loadedSceneNetworkObjects.Count; i++) { (int frame, List networkObjects) value = _loadedSceneNetworkObjects[i]; if (ignoreFrameRestriction || (Time.frameCount > value.frame)) { SetupSceneObjects(value.networkObjects); CollectionCaches.Store(value.networkObjects); _loadedSceneNetworkObjects.RemoveAt(i); i--; } } } /// /// Called when a scene loads on the server. /// /// /// protected internal override void SceneManager_sceneLoaded(Scene s, LoadSceneMode arg1) { base.SceneManager_sceneLoaded(s, arg1); if (!NetworkManager.ServerManager.Started) return; List sceneNobs = CollectionCaches.RetrieveList(); Scenes.GetSceneNetworkObjects(s, firstOnly: false, errorOnDuplicates: true, ignoreUnsetSceneIds: true, ref sceneNobs); _loadedSceneNetworkObjects.Add((Time.frameCount, sceneNobs)); InitializeRootNetworkObjects(sceneNobs); } /// /// Sets initial values for NetworkObjects. /// /// private void InitializeRootNetworkObjects(List nobs) { /* First update the nested status on all nobs, as well * set them as not initialized. This is done as some scene objets might be prefabs * that were changed in scene but have not had the prefab settings updated to those * changes. */ foreach (NetworkObject nob in nobs) { nob.SetIsNestedThroughTraversal(); nob.UnsetInitializedValuesSet(); } //Initialize sceneNobs cache, but do not invoke callbacks till next frame. foreach (NetworkObject nob in nobs) { if (nob.IsSceneObject && !nob.IsNested) nob.SetInitializedValues(parentNob: null, force: false); } } /// /// Setup all NetworkObjects in scenes. Should only be called when server is active. /// protected internal void SetupSceneObjects() { Scene ddolScene = DDOL.GetDDOL().gameObject.scene; bool ddolLoaded = ddolScene.isLoaded; /* Becomes false if setup in GetSceenAt. * This is a safety check for if Unity ever changes * the behavior where DDOL scenes appear in the sceneCount. */ bool trySetupDdol = true; for (int i = 0; i < SceneManager.sceneCount; i++) { Scene s = SceneManager.GetSceneAt(i); if (ddolLoaded && s.handle == ddolScene.handle) trySetupDdol = false; SetupSceneObjects(s); } if (trySetupDdol) SetupSceneObjects(ddolScene); } /// /// Setup NetworkObjects in a scene. Should only be called when server is active. /// /// private void SetupSceneObjects(Scene s) { if (!s.IsValid()) return; List sceneNobs = CollectionCaches.RetrieveList(); Scenes.GetSceneNetworkObjects(s, firstOnly: false, errorOnDuplicates: true, ignoreUnsetSceneIds: true, ref sceneNobs); SetupSceneObjects(sceneNobs); CollectionCaches.Store(sceneNobs); } /// /// Setup NetworkObjects in a scene. Should only be called when server is active. /// /// private void SetupSceneObjects(List sceneNobs) { InitializeRootNetworkObjects(sceneNobs); List cache = SortRootAndNestedByInitializeOrder(sceneNobs); bool isHost = base.NetworkManager.IsHostStarted; int nobsCount = cache.Count; for (int i = 0; i < nobsCount; i++) { NetworkObject nob = cache[i]; //Only setup if a scene object and not initialzied. if (nob.GetIsNetworked() && nob.IsSceneObject && nob.IsDeinitializing) { if (!nob.WasActiveDuringEdit_Set1) { NetworkManager.LogError($"NetworkObject {nob.name} in scene {nob.gameObject.scene.name} needs to be reserialized. Please use the Fish-Networking menu -> Utility -> Reserialize NetworkObjects."); continue; } base.AddToSceneObjects(nob); /* If was active in the editor (before hitting play), or currently active * then PreInitialize without synchronizing to clients. There is no reason * to synchronize to clients because the scene just loaded on server, * which means clients are not yet in the scene. */ if (nob.WasActiveDuringEdit || nob.gameObject.activeInHierarchy) { //If not host then object doesn't need to be spawned until a client joins. if (!isHost) SetupWithoutSynchronization(nob); //Otherwise spawn object so observers update for clientHost. else SpawnWithoutChecks(nob); } } } CollectionCaches.Store(cache); } /// /// Performs setup on a NetworkObject without synchronizing the actions to clients. /// /// Override ObjectId to use. private bool SetupWithoutSynchronization(NetworkObject nob, NetworkConnection ownerConnection = null, int? objectId = null, bool initializeEarly = true) { if (nob.GetIsNetworked()) { int objectIdValue; if (objectId != null) { objectIdValue = objectId.Value; } else { if (!GetNextNetworkObjectId(out objectIdValue)) return false; } if (initializeEarly) nob.InitializeEarly(NetworkManager, objectIdValue, ownerConnection, true); base.AddToSpawned(nob, true); nob.gameObject.SetActive(true); nob.Initialize(true, true); return true; } return false; } #endregion #region Spawning. /// /// Spawns an object over the network. /// internal void Spawn(NetworkObject networkObject, NetworkConnection ownerConnection = null, UnityEngine.SceneManagement.Scene scene = default) { //Default as false, will change if needed. bool predictedSpawn = false; if (networkObject == null) { base.NetworkManager.LogError($"Specified networkObject is null."); return; } if (!NetworkManager.ServerManager.Started) { //Neither server nor client are started. if (!NetworkManager.ClientManager.Started) { base.NetworkManager.LogWarning("Cannot spawn object because server nor client are active."); return; } //Server has predicted spawning disabled. if (!NetworkManager.ServerManager.GetAllowPredictedSpawning()) { base.NetworkManager.LogWarning("Cannot spawn object because server is not active and predicted spawning is not enabled."); return; } //Various predicted spawn checks. if (!base.CanPredictedSpawn(networkObject, NetworkManager.ClientManager.Connection, false)) return; //Since server is not started then run TrySpawn for client, given this is a client trying to predicted spawn. if (!networkObject.PredictedSpawn.OnTrySpawnClient()) return; predictedSpawn = true; } if (!networkObject.gameObject.scene.IsValid()) { base.NetworkManager.LogError($"{networkObject.name} is a prefab. You must instantiate the prefab first, then use Spawn on the instantiated copy."); return; } if (ownerConnection != null && ownerConnection.IsActive && !ownerConnection.LoadedStartScenes(!predictedSpawn)) { base.NetworkManager.LogWarning($"{networkObject.name} was spawned but it's recommended to not spawn objects for connections until they have loaded start scenes. You can be notified when a connection loads start scenes by using connection.OnLoadedStartScenes on the connection, or SceneManager.OnClientLoadStartScenes."); } if (networkObject.IsSpawned) { base.NetworkManager.LogWarning($"{networkObject.name} is already spawned."); return; } NetworkBehaviour networkBehaviourParent = networkObject.CurrentParentNetworkBehaviour; if (networkBehaviourParent != null && !networkBehaviourParent.IsSpawned) { base.NetworkManager.LogError($"{networkObject.name} cannot be spawned because it has a parent NetworkObject {networkBehaviourParent} which is not spawned."); return; } /* If scene is specified make sure the object is root, * and if not move it before network spawning. */ if (scene.IsValid()) { if (networkObject.transform.parent != null) { base.NetworkManager.LogError($"{networkObject.name} cannot be moved to scene name {scene.name}, handle {scene.handle} because {networkObject.name} is not root and only root objects may be moved."); return; } else { UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene); } } if (predictedSpawn) base.NetworkManager.ClientManager.Objects.PredictedSpawn(networkObject, ownerConnection); else SpawnWithoutChecks(networkObject, recursiveSpawnCache: null, ownerConnection); } /// /// Spawns networkObject without any checks. /// private void SpawnWithoutChecks(NetworkObject networkObject, List recursiveSpawnCache = null, NetworkConnection ownerConnection = null, int? objectId = null, bool rebuildObservers = true, bool initializeEarly = true, bool isRecursiveIteration = false) { /* Setup locally without sending to clients. * When observers are built for the network object * during initialization spawn messages will * be sent. */ networkObject.SetIsNetworked(true); /* Grab the nested before spawning the networkObject. This prevents double initialization * if one of the OnStart callbacks adds nested to networkObject. * * EG: called on NetworkObject during 'SetupWithoutSynchronization' * private void OnStartNetwork() * { * NetworkObject n = Instantiate(xyz); * n.SetParent(this); //this being NetworkObject * base.Spawn(n); * } * * If nested was fetched after SetupWithout Synchronize just below then it would * include the newly nested object, and try to initialize it twice. */ List nestedNetworkObjects = (isRecursiveIteration) ? null : networkObject.GetNetworkObjects(GetNetworkObjectOption.AllNestedRecursive); if (SetupWithoutSynchronization(networkObject, ownerConnection, objectId, initializeEarly)) _spawnCache.Add(networkObject); if (nestedNetworkObjects != null) { foreach (NetworkObject item in nestedNetworkObjects) { /* Only spawn recursively if the nob state is unset. * Unset indicates that the nob has not been manually spawned or despawned. */ if (item.gameObject.activeInHierarchy || item.State == NetworkObjectState.Spawned) SpawnWithoutChecks(item, recursiveSpawnCache: null, ownerConnection, isRecursiveIteration: true); } } /* Copy to a new cache then reset _spawnCache * just incase rebuilding observers would lead to * more additions into _spawnCache. EG: rebuilding * may result in additional objects being spawned * for clients and if _spawnCache were not reset * the same objects would be rebuilt again. This likely * would not affect anything other than perf but who * wants that. */ bool recursiveCacheWasNull = (recursiveSpawnCache == null); if (recursiveCacheWasNull) recursiveSpawnCache = CollectionCaches.RetrieveList(); recursiveSpawnCache.AddRange(_spawnCache); _spawnCache.Clear(); //Also rebuild observers for the object so it spawns for others. if (rebuildObservers) RebuildObservers(recursiveSpawnCache); int spawnCacheCopyCount = recursiveSpawnCache.Count; /* If also client then we need to make sure the object renderers have correct visibility. * Set visibility based on if the observers contains the clientHost connection. */ if (NetworkManager.IsClientStarted) { int count = spawnCacheCopyCount; NetworkConnection localConnection = NetworkManager.ClientManager.Connection; for (int i = 0; i < count; i++) { NetworkObject nob = recursiveSpawnCache[i]; nob.SetRenderersVisible(nob.Observers.Contains(localConnection)); } } CollectionCaches.StoreAndDefault(ref nestedNetworkObjects); /* If collection was null then store the one retrieved. * Otherwise, let the calling method handle the provided * cache. */ if (recursiveCacheWasNull) CollectionCaches.Store(recursiveSpawnCache); } /// /// Reads a predicted spawn. /// internal void ReadSpawn(PooledReader reader, NetworkConnection conn) { ushort spawnLength = reader.ReadUInt16Unpacked(); int readStartPosition = reader.Position; SpawnType st = (SpawnType)reader.ReadUInt8Unpacked(); bool sceneObject = st.FastContains(SpawnType.Scene); bool isGlobal = st.FastContains(SpawnType.InstantiatedGlobal); ReadNestedSpawnIds(reader, st, out byte? nobComponentId, out int? parentObjectId, out byte? parentComponentId, readSpawningObjects: null); int objectId = reader.ReadNetworkObjectForSpawn(out _, out ushort collectionId); //No predicted spawn ids left. Should not be possible as client would have stopped this locally. if (conn.PredictedObjectIds.Count == 0 || !conn.PredictedObjectIds.TryDequeue(out int serverPredictedObjectId)) { reader.Clear(); conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection {conn.ClientId} used predicting spawning without any Ids in queue."); return; } //Ids should match. Client cannot send predicted spawn Ids out of order, so they should always match the server in order. if (objectId != serverPredictedObjectId) { reader.Clear(); conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection {conn.ClientId} used predicted Id of {objectId} while the server Id is {serverPredictedObjectId}."); return; } NetworkObject nob = null; NetworkConnection owner = null; /* See if the parent exists. If not, then do not * continue and send failed response to client. */ if (parentObjectId.HasValue && !Spawned.TryGetValueIL2CPP(parentObjectId.Value, out NetworkObject _)) { NetworkManager.Log($"Predicted spawn failed due to the NetworkObject's parent not being found. Scene object: {sceneObject}, ObjectId {objectId}, CollectionId {collectionId}."); SendFailedResponse(objectId); return; } owner = reader.ReadNetworkConnection(); //Read transform values which differ from serialized values. base.ReadTransformProperties(reader, out Vector3? nullablePosition, out Quaternion? nullableRotation, out Vector3? nullableScale); int prefabId; ulong sceneId = 0; string sceneName = string.Empty; string objectName = string.Empty; if (sceneObject) { base.ReadSceneObjectId(reader, out sceneId); #if DEVELOPMENT if (NetworkManager.ClientManager.IsServerDevelopment) base.CheckReadSceneObjectDetails(reader, ref sceneName, ref objectName); #endif nob = base.GetSceneNetworkObject(sceneId, sceneName, objectName); } else { prefabId = reader.ReadNetworkObjectId(); ObjectPoolRetrieveOption retrieveOptions = (ObjectPoolRetrieveOption.MakeActive | ObjectPoolRetrieveOption.LocalSpace); nob = NetworkManager.GetPooledInstantiated(prefabId, collectionId, retrieveOptions, null, nullablePosition, nullableRotation, nullableScale, false); } /* NetworkObject could not be found. User could be sending an invalid Id, * or perhaps it was a scene object and the scene had unloaded prior to getting * this spawn message. */ if (nob == null) { NetworkManager.Log($"Predicted spawn failed due to the NetworkObject not being found. Scene object: {sceneObject}, ObjectId {objectId}, CollectionId {collectionId}."); SendFailedResponse(objectId); return; } if (!nob.WasActiveDuringEdit_Set1) { string sceneOrPrefabText = (sceneObject) ? $"in scene {nob.gameObject.scene.name}" : "prefab"; NetworkManager.LogError($"NetworkObject {nob.name} {sceneOrPrefabText}. Please use the Fish-Networking menu -> Utility -> Reserialize NetworkObjects."); } /* Update sceneObject position. * There is no need to do this on instantiate since the position is set * during the instantiation. */ if (sceneObject) { nob.transform.SetLocalPositionRotationAndScale(nullablePosition, nullableRotation, nullableScale); } //Check if nob allows predicted spawning. if (!base.CanPredictedSpawn(nob, conn, true, reader)) return; nob.SetIsGlobal(isGlobal); nob.SetIsNetworked(true); nob.InitializeEarly(NetworkManager, objectId, owner, true); //Initialize for prediction. nob.InitializePredictedObject_Server(conn); base.ReadPayload(conn, nob, reader); base.ReadRpcLinks(reader); base.ReadSyncTypesForSpawn(reader); //Check user implementation of trySpawn. if (!nob.PredictedSpawn.OnTrySpawnServer(conn, owner)) { //Inform client of failure. SendFailedResponse(objectId); return; } //Once here everything is good. //Get connections to send spawn to. List conns = RetrieveAuthenticatedConnections(); SendSuccessResponse(objectId); //Store caches used. CollectionCaches.Store(conns); //Sends a failed response. void SendFailedResponse(int lObjectId) { SkipRemainingSpawnLength(); if (nob != null) { //TODO support pooling. This first requires a rework of the initialization / clientHost message system. UnityEngine.Object.Destroy(nob.gameObject); //base.NetworkManager.StorePooledInstantiated(nob, true); } PooledWriter writer = WriteResponseHeader(success: false, lObjectId); conn.SendToClient((byte)Channel.Reliable, writer.GetArraySegment()); WriterPool.Store(writer); } //Sends a success spawn result and returns nobs recursively spawned, including original. void SendSuccessResponse(int lObjectId) { PooledWriter writer = WriteResponseHeader(success: true, lObjectId); SpawnWithoutChecks(nob, recursiveSpawnCache: null, owner, lObjectId, rebuildObservers: true, initializeEarly: false); conn.SendToClient((byte)Channel.Reliable, writer.GetArraySegment()); WriterPool.Store(writer); } //Writes response header and returns writer used. PooledWriter WriteResponseHeader(bool success, int lObjectId) { PooledWriter writer = WriterPool.Retrieve(); writer.WritePacketIdUnpacked(PacketId.PredictedSpawnResult); writer.WriteBoolean(success); //Id of object which was predicted spawned. writer.WriteNetworkObjectId(lObjectId); //Write the next Id even if not succesful. int nextId; if (NetworkManager.ServerManager.Objects.GetObjectIdCache().TryDequeue(out nextId)) conn.PredictedObjectIds.Enqueue(nextId); else nextId = NetworkObject.UNSET_OBJECTID_VALUE; //Write nextId even if invalid. Client will not add invalid Ids. writer.WriteNetworkObjectId(nextId); return writer; } //Skips remaining data for the spawn. void SkipRemainingSpawnLength() { /* Simply setting the position to readStart + spawnLength works * too but when possible use supplied API. */ int skipLength = spawnLength - (reader.Position - readStartPosition); reader.Skip(skipLength); } } #endregion #region Despawning. /// /// Cleans recently despawned objects. /// private void CleanRecentlyDespawned() { //Only iterate if frame ticked to save perf. if (!base.NetworkManager.TimeManager.FrameTicked) return; List intCache = CollectionCaches.RetrieveList(); uint requiredTicks = _cleanRecentlyDespawnedMaxTicks; uint currentTick = base.NetworkManager.TimeManager.LocalTick; //Iterate 20, or 5% of the collection, whichever is higher. int iterations = Mathf.Max(20, (int)(RecentlyDespawnedIds.Count * 0.05f)); /* Given this is a dictionary there is no gaurantee which order objects are * added. Because of this it's possible some objects may take much longer to * be removed. This is okay so long as a consistent chunk of objects are removed * at a time; eventually all objects will be iterated. */ int count = 0; foreach (KeyValuePair kvp in RecentlyDespawnedIds) { long result = (currentTick - kvp.Value); //If enough ticks have passed to remove. if (result > requiredTicks) intCache.Add(kvp.Key); count++; if (count == iterations) break; } //Remove cached entries. int cCount = intCache.Count; for (int i = 0; i < cCount; i++) RecentlyDespawnedIds.Remove(intCache[i]); CollectionCaches.Store(intCache); } /// /// Returns if an objectId was recently despawned. /// /// ObjectId to check. /// Passed ticks to be within to be considered recently despawned. /// True if an objectId was despawned with specified number of ticks. public bool RecentlyDespawned(int objectId, uint ticks) { uint despawnTick; if (!RecentlyDespawnedIds.TryGetValue(objectId, out despawnTick)) return false; return ((NetworkManager.TimeManager.LocalTick - despawnTick) <= ticks); } /// /// Adds to objects pending destroy due to clientHost environment. /// /// internal void AddToPending(NetworkObject nob) { _pendingDestroy.Add(nob); } /// /// Tries to removes objectId from PendingDestroy and returns if successful. /// internal bool RemoveFromPending(NetworkObject nob) { return _pendingDestroy.Remove(nob); } /// /// Returns a NetworkObject in PendingDestroy. /// internal NetworkObject GetFromPending(int objectId) { /* Becomes true if there was a null entry and pending must be rebuilt. * This would be very uncommon */ bool rebuildPending = false; foreach (NetworkObject n in _pendingDestroy) { if (n == null) { rebuildPending = true; continue; } if (n.ObjectId == objectId) return n; } if (rebuildPending) { HashSet newPending = CollectionCaches.RetrieveHashSet(); foreach (NetworkObject n in _pendingDestroy) { if (n != null) newPending.Add(n); } CollectionCaches.Store(_pendingDestroy); _pendingDestroy = newPending; } //Fall through, nothing found. return null; } /// /// Destroys NetworkObjects pending for destruction. /// internal void DestroyPending() { foreach (NetworkObject item in _pendingDestroy) { if (item != null) UnityEngine.Object.Destroy(item.gameObject); } _pendingDestroy.Clear(); } /// /// Despawns an object over the network. /// internal override void Despawn(NetworkObject networkObject, DespawnType despawnType, bool asServer) { //Default as false, will change if needed. bool predictedDespawn = false; if (networkObject == null) { base.NetworkManager.LogWarning($"NetworkObject cannot be despawned because it is null."); return; } if (networkObject.IsDeinitializing) { base.NetworkManager.LogWarning($"Object {networkObject.name} cannot be despawned because it is already deinitializing."); return; } if (!NetworkManager.ServerManager.Started) { //Neither server nor client are started. if (!NetworkManager.ClientManager.Started) { base.NetworkManager.LogWarning("Cannot despawn object because server nor client are active."); return; } //Server has predicted spawning disabled. if (!NetworkManager.ServerManager.GetAllowPredictedSpawning()) { base.NetworkManager.LogWarning("Cannot despawn object because server is not active and predicted spawning is not enabled."); return; } //Various predicted despawn checks. if (!base.CanPredictedDespawn(networkObject, NetworkManager.ClientManager.Connection, false)) return; predictedDespawn = true; } if (!networkObject.gameObject.scene.IsValid()) { base.NetworkManager.LogError($"{networkObject.name} is a prefab. You must instantiate the prefab first, then use Spawn on the instantiated copy."); return; } if (predictedDespawn) { base.NetworkManager.ClientManager.Objects.PredictedDespawn(networkObject); } else { FinalizeDespawn(networkObject, despawnType); RecentlyDespawnedIds[networkObject.ObjectId] = base.NetworkManager.TimeManager.LocalTick; base.Despawn(networkObject, despawnType, asServer); } } /// /// Called when a NetworkObject is destroyed without being deactivated first. /// /// internal override void NetworkObjectDestroyed(NetworkObject nob, bool asServer) { //Only finalize despawn if not already deinitialized. if (!nob.IsDeinitializing) FinalizeDespawn(nob, DespawnType.Destroy); base.NetworkObjectDestroyed(nob, asServer); } /// /// Finalizes the despawn process. By the time this is called the object is considered unaccessible. /// /// private void FinalizeDespawn(NetworkObject nob, DespawnType despawnType) { List dirtiedNbs = _dirtySyncTypeBehaviours; if (nob != null && nob.ObjectId != NetworkObject.UNSET_OBJECTID_VALUE) { // Write out any pending sync types and be sure to clear from the dirty list // to avoid trying to write out a despawned object later on. for (int i = 0, count = nob.NetworkBehaviours.Count; i < count; ++i) { NetworkBehaviour nb = nob.NetworkBehaviours[i]; if (nb.SyncTypeDirty && nb.WriteDirtySyncTypes((SyncTypeWriteFlag.ForceReliable | SyncTypeWriteFlag.IgnoreInterval))) dirtiedNbs.Remove(nb); } WriteDespawnAndSend(nob, despawnType); CacheObjectId(nob); } } /// /// Writes a despawn and sends it to clients. /// private void WriteDespawnAndSend(NetworkObject nob, DespawnType despawnType) { HashSet observers = nob.Observers; if (observers.Count == 0) return; PooledWriter everyoneWriter = WriterPool.Retrieve(); WriteDespawn(nob, despawnType, everyoneWriter); ArraySegment despawnSegment = everyoneWriter.GetArraySegment(); //Add observers to a list cache. List cache = CollectionCaches.RetrieveList(); /* Must be added into a new collection because the * user might try and modify the observers in the despawn, which * would cause a collection modified error. */ cache.AddRange(observers); int cacheCount = cache.Count; for (int i = 0; i < cacheCount; i++) { //Invoke ondespawn and send despawn. NetworkConnection conn = cache[i]; nob.InvokeOnServerDespawn(conn); NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, despawnSegment, conn); //Remove from observers. //nob.Observers.Remove(conn); } everyoneWriter.Store(); CollectionCaches.Store(cache); } /// /// Reads a predicted despawn. /// internal void ReadDespawn(Reader reader, NetworkConnection conn) { NetworkObject nob = reader.ReadNetworkObject(); //Maybe server destroyed the object so don't kick if null. if (nob == null) return; if (nob.IsDeinitializing) return; //Various predicted despawn checks. if (!base.CanPredictedDespawn(nob, conn, true)) return; //Despawn object. nob.Despawn(); } #endregion } }