using FishNet.Connection; using FishNet.Managing.Logging; using FishNet.Managing.Object; using FishNet.Managing.Timing; using FishNet.Object; using FishNet.Serializing; using FishNet.Transporting; using FishNet.Utility; using FishNet.Utility.Extension; using FishNet.Utility.Performance; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; 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 Dictionary(); #endregion #region Private. /// /// Cached ObjectIds which may be used when exceeding available ObjectIds. /// private Queue _objectIdCache = new Queue(); /// /// Returns the ObjectId cache. /// /// internal Queue GetObjectIdCache() => _objectIdCache; /// /// NetworkBehaviours which have dirty SyncVars. /// private List _dirtySyncVarBehaviours = new List(20); /// /// NetworkBehaviours which have dirty SyncObjects. /// private List _dirtySyncObjectBehaviours = new List(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 Dictionary _pendingDestroy = new Dictionary(); /// /// Scenes which were loaded that need to be setup. /// private List<(int, Scene)> _loadedScenes = new List<(int frame, Scene scene)>(); /// /// Cache of spawning objects, used for recursively spawning nested NetworkObjects. /// private ListCache _spawnCache = new ListCache(); /// /// 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.IsServer) { _scenesLoading = false; _loadedScenes.Clear(); return; } CleanRecentlyDespawned(); if (!_scenesLoading) IterateLoadedScenes(false); Observers_OnUpdate(); } #region Checking dirty SyncTypes. /// /// Iterates NetworkBehaviours with dirty SyncTypes. /// internal void WriteDirtySyncTypes() { /* Tells networkbehaviours to check their * dirty synctypes. */ IterateCollection(_dirtySyncVarBehaviours, false); IterateCollection(_dirtySyncObjectBehaviours, true); void IterateCollection(List collection, bool isSyncObject) { for (int i = 0; i < collection.Count; i++) { bool dirtyCleared = collection[i].WriteDirtySyncTypes(isSyncObject); if (dirtyCleared) { collection.RemoveAt(i); i--; } } } } /// /// Sets that a NetworkBehaviour has a dirty syncVars. /// /// internal void SetDirtySyncType(NetworkBehaviour nb, bool isSyncObject) { if (isSyncObject) _dirtySyncObjectBehaviours.Add(nb); else _dirtySyncVarBehaviours.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.OneServerStarted()) { BuildObjectIdCache(); SetupSceneObjects(); } } //Server in anything but started state. else { //If no servers are started then reset. if (!base.NetworkManager.ServerManager.AnyServerStarted()) { base.DespawnWithoutSynchronization(true); base.SceneObjects.Clear(); _objectIdCache.Clear(); base.NetworkManager.ClearClientsCollection(base.NetworkManager.ServerManager.Clients); } } } /// /// Called when a client disconnects. /// /// internal void ClientDisconnected(NetworkConnection connection) { RemoveFromObserversWithoutSynchronization(connection); OnPreDestroyClientObjects?.Invoke(connection); /* A cache is made because the Objects * collection would end up modified during * iteration from removing ownership and despawning. */ ListCache cache = ListCaches.GetNetworkObjectCache(); foreach (NetworkObject nob in connection.Objects) cache.AddValue(nob); int written = cache.Written; List collection = cache.Collection; for (int i = 0; i < written; 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 (!collection[i].IsDeinitializing) base.NetworkManager.ServerManager.Despawn(collection[i]); } ListCaches.StoreCache(cache); } #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 List(); //Ignore ushort.maxvalue as that indicates null. for (int i = 0; i < (ushort.MaxValue - 1); i++) shuffledCache.Add(i); /* Only shuffle when NOT in editor and not * development build. * Debugging could be easier when Ids are ordered. */ #if !UNITY_EDITOR && !DEVELOPMENT_BUILD 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] 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) { _objectIdCache.Enqueue(id); } /// /// Gets the next ObjectId to use for NetworkObjects. /// /// protected internal override int GetNextNetworkObjectId(bool errorCheck = true) { //Either something went wrong or user actually managed to spawn ~64K networked objects. if (_objectIdCache.Count == 0) { base.NetworkManager.LogError($"No more available ObjectIds. How the heck did you manage to have {ushort.MaxValue} objects spawned at once?"); return -1; } else { return _objectIdCache.Dequeue(); } } #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() { _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) _loadedScenes.Clear(); for (int i = 0; i < _loadedScenes.Count; i++) { (int frame, Scene scene) value = _loadedScenes[i]; if (ignoreFrameRestriction || (Time.frameCount > value.frame)) { SetupSceneObjects(value.scene); _loadedScenes.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; //Add to loaded scenes so that they are setup next frame. _loadedScenes.Add((Time.frameCount, s)); } /// /// Setup all NetworkObjects in scenes. Should only be called when server is active. /// protected internal void SetupSceneObjects() { for (int i = 0; i < SceneManager.sceneCount; i++) SetupSceneObjects(SceneManager.GetSceneAt(i)); Scene ddolScene = DDOLFinder.GetDDOL().gameObject.scene; if (ddolScene.isLoaded) SetupSceneObjects(ddolScene); } /// /// Setup NetworkObjects in a scene. Should only be called when server is active. /// /// private void SetupSceneObjects(Scene s) { ListCache nobs; SceneFN.GetSceneNetworkObjects(s, false, out nobs); bool isHost = base.NetworkManager.IsHost; for (int i = 0; i < nobs.Written; i++) { NetworkObject nob = nobs.Collection[i]; //Only setup if a scene object and not initialzied. if (nob.IsNetworked && nob.IsSceneObject && nob.IsDeinitializing) { base.UpdateNetworkBehavioursForSceneObject(nob, true); 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.ActiveDuringEdit || 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); } } } ListCaches.StoreCache(nobs); } /// /// Performs setup on a NetworkObject without synchronizing the actions to clients. /// /// Override ObjectId to use. private void SetupWithoutSynchronization(NetworkObject nob, NetworkConnection ownerConnection = null, int? objectId = null) { if (nob.IsNetworked) { if (objectId == null) objectId = GetNextNetworkObjectId(); nob.Preinitialize_Internal(NetworkManager, objectId.Value, ownerConnection, true); base.AddToSpawned(nob, true); nob.gameObject.SetActive(true); nob.Initialize(true, true); } } #endregion #region Spawning. /// /// Spawns an object over the network. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Spawn(NetworkObject networkObject, NetworkConnection ownerConnection = null) { //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.PredictionManager.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, ownerConnection, false)) 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; } if (networkObject.ParentNetworkObject != null && !networkObject.ParentNetworkObject.IsSpawned) { base.NetworkManager.LogError($"{networkObject.name} cannot be spawned because it has a parent NetworkObject {networkObject.ParentNetworkObject} which is not spawned."); return; } if (predictedSpawn) base.NetworkManager.ClientManager.Objects.PredictedSpawn(networkObject, ownerConnection); else SpawnWithoutChecks(networkObject, ownerConnection); } /// /// Spawns networkObject without any checks. /// private void SpawnWithoutChecks(NetworkObject networkObject, NetworkConnection ownerConnection = null, int? objectId = null) { /* Setup locally without sending to clients. * When observers are built for the network object * during initialization spawn messages will * be sent. */ networkObject.SetIsNetworked(true); _spawnCache.AddValue(networkObject); SetupWithoutSynchronization(networkObject, ownerConnection, objectId); foreach (NetworkObject item in networkObject.ChildNetworkObjects) { /* Only spawn recursively if the nob state is unset. * Unset indicates that the nob has not been */ if (item.gameObject.activeInHierarchy || item.State == NetworkObjectState.Spawned) SpawnWithoutChecks(item, ownerConnection); } /* 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. */ ListCache spawnCacheCopy = ListCaches.GetNetworkObjectCache(); spawnCacheCopy.AddValues(_spawnCache); _spawnCache.Reset(); //Also rebuild observers for the object so it spawns for others. RebuildObservers(spawnCacheCopy); /* 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.IsClient) { int count = spawnCacheCopy.Written; List collection = spawnCacheCopy.Collection; for (int i = 0; i < count; i++) collection[i].SetRenderersVisible(networkObject.Observers.Contains(NetworkManager.ClientManager.Connection)); } ListCaches.StoreCache(spawnCacheCopy); } /// /// Reads a predicted spawn. /// internal void ReadPredictedSpawn(PooledReader reader, NetworkConnection conn) { sbyte initializeOrder; ushort collectionId; int prefabId; int objectId = reader.ReadNetworkObjectForSpawn(out initializeOrder, out collectionId, out _); //If objectId is not within predicted ids for conn. if (!conn.PredictedObjectIds.Contains(objectId)) { reader.Clear(); conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection {conn.ClientId} used predicted spawning with a non-reserved objectId of {objectId}."); return; } NetworkConnection owner = reader.ReadNetworkConnection(); SpawnType st = (SpawnType)reader.ReadByte(); //Not used at the moment. byte componentIndex = reader.ReadByte(); //Read transform values which differ from serialized values. Vector3? localPosition; Quaternion? localRotation; Vector3? localScale; base.ReadTransformProperties(reader, out localPosition, out localRotation, out localScale); NetworkObject nob; bool isGlobal = false; if (SpawnTypeEnum.Contains(st, SpawnType.Scene)) { ulong sceneId = reader.ReadUInt64(AutoPackType.Unpacked); nob = base.GetSceneNetworkObject(sceneId); if (!base.CanPredictedSpawn(nob, conn, owner, true)) return; } else { //Not used right now. SpawnParentType spt = (SpawnParentType)reader.ReadByte(); prefabId = reader.ReadNetworkObjectId(); //Invalid prefabId. if (prefabId == NetworkObject.UNSET_PREFABID_VALUE) { reader.Clear(); conn.Kick(KickReason.UnusualActivity, LoggingType.Common, $"Spawned object has an invalid prefabId of {prefabId}. Make sure all objects which are being spawned over the network are within SpawnableObjects on the NetworkManager. Connection {conn.ClientId} will be kicked immediately."); return; } PrefabObjects prefabObjects = NetworkManager.GetPrefabObjects(collectionId, false); //PrefabObjects not found. if (prefabObjects == null) { reader.Clear(); conn.Kick(KickReason.UnusualActivity, LoggingType.Common, $"PrefabObjects collection is not found for CollectionId {collectionId}. Be sure to add your addressables NetworkObject prefabs to the collection on server and client before attempting to spawn them over the network. Connection {conn.ClientId} will be kicked immediately."); return; } //Check if prefab allows predicted spawning. NetworkObject nPrefab = prefabObjects.GetObject(true, prefabId); if (!base.CanPredictedSpawn(nPrefab, conn, owner, true)) return; nob = NetworkManager.GetPooledInstantiated(prefabId, false); isGlobal = SpawnTypeEnum.Contains(st, SpawnType.InstantiatedGlobal); } Transform t = nob.transform; //Parenting predicted spawns is not supported yet. t.SetParent(null, true); base.GetTransformProperties(localPosition, localRotation, localScale, t, out Vector3 pos, out Quaternion rot, out Vector3 scale); t.SetLocalPositionRotationAndScale(pos, rot, scale); nob.SetIsGlobal(isGlobal); //Initialize for prediction. nob.InitializePredictedObject_Server(base.NetworkManager, conn); /* Only read sync types if allowed for the object. * If the client did happen to send synctypes while not allowed * this will create a parse error on the server, * resulting in the client being kicked. */ if (nob.AllowPredictedSyncTypes) { ArraySegment syncValues = reader.ReadArraySegmentAndSize(); PooledReader syncTypeReader = ReaderPool.GetReader(syncValues, base.NetworkManager); foreach (NetworkBehaviour nb in nob.NetworkBehaviours) { //SyncVars. int length = syncTypeReader.ReadInt32(); nb.OnSyncType(syncTypeReader, length, false, true); //SyncObjects length = syncTypeReader.ReadInt32(); nb.OnSyncType(syncTypeReader, length, true, true); } syncTypeReader.Dispose(); } SpawnWithoutChecks(nob, owner, objectId); //Send the spawner a new reservedId. WriteResponse(true); //Writes a predicted spawn result to a client. void WriteResponse(bool success) { PooledWriter writer = WriterPool.GetWriter(); writer.WritePacketId(PacketId.PredictedSpawnResult); writer.WriteNetworkObjectId(nob.ObjectId); writer.WriteBoolean(success); if (success) { Queue objectIdCache = NetworkManager.ServerManager.Objects.GetObjectIdCache(); //Write next objectId to use. int invalidId = NetworkObject.UNSET_OBJECTID_VALUE; int nextId = (objectIdCache.Count > 0) ? objectIdCache.Dequeue() : invalidId; writer.WriteNetworkObjectId(nextId); //If nextId is valid then also add it to spawners local cache. if (nextId != invalidId) conn.PredictedObjectIds.Enqueue(nextId); ////Update RPC links. //foreach (NetworkBehaviour nb in nob.NetworkBehaviours) // nb.WriteRpcLinks(writer); } conn.SendToClient((byte)Channel.Reliable, writer.GetArraySegment()); } } #endregion #region Despawning. /// /// Cleans recently despawned objects. /// private void CleanRecentlyDespawned() { //Only iterate if frame ticked to save perf. if (!base.NetworkManager.TimeManager.FrameTicked) return; ListCache intCache = ListCaches.GetIntCache(); 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.AddValue(kvp.Key); count++; if (count == iterations) break; } //Remove cached entries. List collection = intCache.Collection; int cCount = collection.Count; for (int i = 0; i < cCount; i++) RecentlyDespawnedIds.Remove(collection[i]); ListCaches.StoreCache(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[nob.ObjectId] = nob; } /// /// Tries to removes objectId from PendingDestroy and returns if successful. /// internal bool RemoveFromPending(int objectId) { return _pendingDestroy.Remove(objectId); } /// /// Returns a NetworkObject in PendingDestroy. /// internal NetworkObject GetFromPending(int objectId) { NetworkObject nob; _pendingDestroy.TryGetValue(objectId, out nob); return nob; } /// /// Destroys NetworkObjects pending for destruction. /// internal void DestroyPending() { foreach (NetworkObject item in _pendingDestroy.Values) { if (item != null) MonoBehaviour.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.PredictionManager.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 NetworkObjectUnexpectedlyDestroyed(NetworkObject nob, bool asServer) { FinalizeDespawn(nob, DespawnType.Destroy); base.NetworkObjectUnexpectedlyDestroyed(nob, asServer); } /// /// Finalizes the despawn process. By the time this is called the object is considered unaccessible. /// /// private void FinalizeDespawn(NetworkObject nob, DespawnType despawnType) { if (nob != null && nob.ObjectId != NetworkObject.UNSET_OBJECTID_VALUE) { nob.WriteDirtySyncTypes(); WriteDespawnAndSend(nob, despawnType); CacheObjectId(nob); } } /// /// Writes a despawn and sends it to clients. /// /// private void WriteDespawnAndSend(NetworkObject nob, DespawnType despawnType) { PooledWriter everyoneWriter = WriterPool.GetWriter(); WriteDespawn(nob, despawnType, everyoneWriter); ArraySegment despawnSegment = everyoneWriter.GetArraySegment(); //Add observers to a list cache. ListCache cache = ListCaches.GetNetworkConnectionCache(); cache.Reset(); cache.AddValues(nob.Observers); int written = cache.Written; for (int i = 0; i < written; i++) { //Invoke ondespawn and send despawn. NetworkConnection conn = cache.Collection[i]; nob.InvokeOnServerDespawn(conn); NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, despawnSegment, conn); //Remove from observers. //nob.Observers.Remove(conn); } everyoneWriter.Dispose(); ListCaches.StoreCache(cache); } /// /// Reads a predicted despawn. /// internal void ReadPredictedDespawn(Reader reader, NetworkConnection conn) { NetworkObject nob = reader.ReadNetworkObject(); //Maybe server destroyed the object so don't kick if null. if (nob == null) { reader.Clear(); return; } //Does not allow predicted despawning. if (!nob.AllowPredictedDespawning) { reader.Clear(); conn.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection {conn.ClientId} used predicted despawning for object {nob.name} when it does not support predicted despawning."); } //Despawn object. nob.Despawn(); } #endregion } }