using FishNet.Connection; using FishNet.Documenting; using FishNet.Managing.Logging; using FishNet.Managing.Predicting; using FishNet.Managing.Server; using FishNet.Managing.Timing; using FishNet.Object.Prediction; using FishNet.Object.Prediction.Delegating; using FishNet.Serializing; using FishNet.Serializing.Helping; using FishNet.Transporting; using FishNet.Utility.Constant; using FishNet.Utility.Extension; using FishNet.Utility.Performance; using GameKit.Utilities; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityScene = UnityEngine.SceneManagement.Scene; [assembly: InternalsVisibleTo(UtilityConstants.CODEGEN_ASSEMBLY_NAME)] namespace FishNet.Object { public abstract partial class NetworkBehaviour : MonoBehaviour { #region Public. /// /// True if the client has cached reconcile /// internal bool ClientHasReconcileData; /// /// Gets the last tick this NetworkBehaviour reconciled with. /// public uint GetLastReconcileTick() => _lastReconcileTick; /// /// Sets the last tick this NetworkBehaviour reconciled with. /// internal void SetLastReconcileTick(uint value, bool updateGlobals = true) { _lastReconcileTick = value; if (updateGlobals) PredictionManager.LastReconcileTick = value; } /// /// /// private uint _lastReplicateTick; /// /// Gets the last tick this NetworkBehaviour replicated with. /// public uint GetLastReplicateTick() => _lastReplicateTick; /// /// Sets the last tick this NetworkBehaviour replicated with. /// For internal use only. /// private void SetLastReplicateTick(uint value, bool updateGlobals = true) { _lastReplicateTick = value; if (updateGlobals) { Owner.LocalReplicateTick = TimeManager.LocalTick; PredictionManager.LastReplicateTick = value; } } /// /// True if this object is reconciling. /// public bool IsReconciling { get; internal set; } #endregion #region Private. /// /// Registered Replicate methods. /// private readonly Dictionary _replicateRpcDelegates = new Dictionary(); /// /// Registered Reconcile methods. /// private readonly Dictionary _reconcileRpcDelegates = new Dictionary(); /// /// True if initialized compnents for prediction. /// private bool _predictionInitialized; /// /// Rigidbody found on this object. This is used for prediction. /// private Rigidbody _predictionRigidbody; /// /// Rigidbody2D found on this object. This is used for prediction. /// private Rigidbody2D _predictionRigidbody2d; /// /// Last position for TransformMayChange. /// private Vector3 _lastMayChangePosition; /// /// Last rotation for TransformMayChange. /// private Quaternion _lastMayChangeRotation; /// /// Last scale for TransformMayChange. /// private Vector3 _lastMayChangeScale; /// /// Number of resends which may occur. This could be for client resending replicates to the server or the server resending reconciles to the client. /// private int _remainingResends; /// /// Last sent replicate by owning client or server to non-owners. /// private uint _lastSentReplicateTick; /// /// Last enqueued replicate tick on the server. /// private uint _lastReceivedReplicateTick; /// /// Last tick of a reconcile received from the server. /// private uint _lastReceivedReconcileTick; /// /// Last tick a reconcile occured. /// private uint _lastReconcileTick; #endregion /// /// Registers a RPC method. /// Internal use. /// /// /// [CodegenMakePublic] [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void RegisterReplicateRpc(uint hash, ReplicateRpcDelegate del) { _replicateRpcDelegates[hash] = del; } /// /// Registers a RPC method. /// Internal use. /// /// /// [CodegenMakePublic] [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void RegisterReconcileRpc(uint hash, ReconcileRpcDelegate del) { _reconcileRpcDelegates[hash] = del; } /// /// Called when a replicate is received. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void OnReplicateRpc(uint? methodHash, PooledReader reader, NetworkConnection sendingClient, Channel channel) { if (methodHash == null) methodHash = ReadRpcHash(reader); if (sendingClient == null) { _networkObjectCache.NetworkManager.LogError($"NetworkConnection is null. Replicate {methodHash.Value} on {gameObject.name}, behaviour {GetType().Name} will not complete. Remainder of packet may become corrupt."); return; } if (_replicateRpcDelegates.TryGetValueIL2CPP(methodHash.Value, out ReplicateRpcDelegate del)) del.Invoke(reader, sendingClient, channel); else _networkObjectCache.NetworkManager.LogWarning($"Replicate not found for hash {methodHash.Value} on {gameObject.name}, behaviour {GetType().Name}. Remainder of packet may become corrupt."); } /// /// Called when a reconcile is received. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void OnReconcileRpc(uint? methodHash, PooledReader reader, Channel channel) { if (methodHash == null) methodHash = ReadRpcHash(reader); if (_reconcileRpcDelegates.TryGetValueIL2CPP(methodHash.Value, out ReconcileRpcDelegate del)) del.Invoke(reader, channel); else _networkObjectCache.NetworkManager.LogWarning($"Reconcile not found for hash {methodHash.Value}. Remainder of packet may become corrupt."); } /// /// Clears cached replicates. This can be useful to call on server and client after teleporting. /// /// True to reset values for server, false to reset values for client. public void ClearReplicateCache(bool asServer) { ResetLastPredictionTicks(); ClearReplicateCache_Virtual(asServer); } /// /// Clears cached replicates for server and client. This can be useful to call on server and client after teleporting. /// public void ClearReplicateCache() { ResetLastPredictionTicks(); ClearReplicateCache_Virtual(true); ClearReplicateCache_Virtual(false); } /// /// Clears cached replicates. /// For internal use only. /// /// [CodegenMakePublic] internal virtual void ClearReplicateCache_Virtual(bool asServer) { } /// /// Resets last predirection tick values. /// private void ResetLastPredictionTicks() { _lastSentReplicateTick = 0; _lastReceivedReplicateTick = 0; _lastReceivedReconcileTick = 0; SetLastReconcileTick(0, false); SetLastReplicateTick(0, false); } /// /// Writes number of past inputs from buffer to writer and sends it to the server. /// Internal use. /// private void Owner_SendReplicateRpc(uint hash, List replicates, Channel channel) where T : IReplicateData { if (!IsSpawnedWithWarning()) return; int bufferCount = replicates.Count; int lastBufferIndex = (bufferCount - 1); //Nothing to send; should never be possible. if (lastBufferIndex < 0) return; //Number of past inputs to send. int pastInputs = Mathf.Min(PredictionManager.RedundancyCount, bufferCount); /* Where to start writing from. When passed * into the writer values from this offset * and forward will be written. */ int offset = bufferCount - pastInputs; if (offset < 0) offset = 0; uint lastReplicateTick = _lastSentReplicateTick; if (lastReplicateTick > 0) { uint diff = TimeManager.LocalTick - GetLastReplicateTick(); offset += (int)diff - 1; if (offset >= replicates.Count) return; } _lastSentReplicateTick = TimeManager.LocalTick; //Write history to methodWriter. PooledWriter methodWriter = WriterPool.Retrieve(WriterPool.LENGTH_BRACKET); methodWriter.WriteReplicate(replicates, offset); PooledWriter writer; //if (_rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) //writer = CreateLinkedRpc(link, methodWriter, Channel.Unreliable); //else //todo add support for -> server rpc links. _transportManagerCache.CheckSetReliableChannel(methodWriter.Length + MAXIMUM_RPC_HEADER_SIZE, ref channel); writer = CreateRpc(hash, methodWriter, PacketId.Replicate, channel); NetworkManager.TransportManager.SendToServer((byte)channel, writer.GetArraySegment(), false); /* If being sent as reliable then clear buffer * since we know it will get there. * Also reset remaining resends. */ if (channel == Channel.Reliable) { replicates.Clear(); _remainingResends = 0; } methodWriter.StoreLength(); writer.StoreLength(); } /// /// Sends a RPC to target. /// Internal use. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Server_SendReconcileRpc(uint hash, T reconcileData, Channel channel) { if (!IsSpawned) return; if (!Owner.IsActive) return; PooledWriter methodWriter = WriterPool.Retrieve(); methodWriter.WriteUInt32(GetLastReplicateTick()); methodWriter.Write(reconcileData); PooledWriter writer; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (NetworkManager.DebugManager.ReconcileRpcLinks && _rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #else if (_rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #endif writer = CreateLinkedRpc(link, methodWriter, channel); else writer = CreateRpc(hash, methodWriter, PacketId.Reconcile, channel); _networkObjectCache.NetworkManager.TransportManager.SendToClient((byte)channel, writer.GetArraySegment(), Owner); methodWriter.Store(); writer.Store(); } /// /// Returns if there is a chance the transform may change after the tick. /// /// protected internal bool PredictedTransformMayChange() { if (TimeManager.PhysicsMode == PhysicsMode.Disabled) return false; if (!_predictionInitialized) { _predictionInitialized = true; _predictionRigidbody = GetComponentInParent(); _predictionRigidbody2d = GetComponentInParent(); } /* Use distance when checking if changed because rigidbodies can twitch * or move an extremely small amount. These small moves are not worth * resending over because they often fix themselves each frame. */ float changeDistance = 0.000004f; bool positionChanged = (transform.position - _lastMayChangePosition).sqrMagnitude > changeDistance; bool rotationChanged = (transform.rotation.eulerAngles - _lastMayChangeRotation.eulerAngles).sqrMagnitude > changeDistance; bool scaleChanged = (transform.localScale - _lastMayChangeScale).sqrMagnitude > changeDistance; bool transformChanged = (positionChanged || rotationChanged || scaleChanged); /* Returns true if transform.hasChanged, or if either * of the rigidbodies have velocity. */ bool changed = ( transformChanged || (_predictionRigidbody != null && (_predictionRigidbody.velocity != Vector3.zero || _predictionRigidbody.angularVelocity != Vector3.zero)) || (_predictionRigidbody2d != null && (_predictionRigidbody2d.velocity != Vector2.zero || _predictionRigidbody2d.angularVelocity != 0f)) ); //If transform changed update last values. if (transformChanged) { _lastMayChangePosition = transform.position; _lastMayChangeRotation = transform.rotation; _lastMayChangeScale = transform.localScale; } return changed; } /// /// Checks conditions for a replicate. /// /// True if checking as server. /// Returns true if to exit the replicate early. [CodegenMakePublic] //internal internal bool Replicate_ExitEarly_A(bool asServer, bool replaying, bool allowServerControl) { bool isOwner = IsOwner; //Server. if (asServer) { //No owner, do not try to replicate 'owner' input. if (!Owner.IsActive && !allowServerControl) { ClearReplicateCache(true); return true; } //Is client host, no need to use CSP; trust client. if (isOwner) { ClearReplicateCache(); return true; } } //Client. else { //Server does not replay; this should never happen. if (replaying && IsServer) return true; //Spectators cannot replicate. if (!isOwner) { ClearReplicateCache(false); return true; } } //Checks pass. return false; } /// /// Gets the next replicate in perform when server or non-owning client. /// [CodegenMakePublic] //internal internal void Replicate_NonOwner(ReplicateUserLogicDelegate del, BasicQueue q, T serverControlData, bool allowServerControl, Channel channel) where T : IReplicateData { //If to allow server control make sure there is no owner. if (allowServerControl && !Owner.IsValid) { uint tick = TimeManager.LocalTick; serverControlData.SetTick(tick); SetLastReplicateTick(tick); del.Invoke(serverControlData, true, channel, false); } //Using client inputs. else { int count = q.Count; if (count > 0) { ReplicateData(q.Dequeue()); count--; PredictionManager pm = PredictionManager; bool consumeExcess = !pm.DropExcessiveReplicates; //Number of entries to leave in buffer when consuming. int leaveInBuffer = (int)pm.QueuedInputs; //Only consume if the queue count is over leaveInBuffer. if (consumeExcess && count > leaveInBuffer) { byte maximumAllowedConsumes = pm.MaximumReplicateConsumeCount; int maximumPossibleConsumes = (count - leaveInBuffer); int consumeAmount = Mathf.Min(maximumAllowedConsumes, maximumPossibleConsumes); for (int i = 0; i < consumeAmount; i++) ReplicateData(q.Dequeue()); } void ReplicateData(T data) { uint tick = data.GetTick(); SetLastReplicateTick(tick); del.Invoke(data, true, channel, false); } _remainingResends = pm.RedundancyCount; } else { del.Invoke(default, true, channel, false); } } } /// /// Returns if a replicates data changed and updates resends as well data tick. /// /// True to enqueue data for replaying. /// True if data has changed.. [CodegenMakePublic] //internal internal void Replicate_Owner(ReplicateUserLogicDelegate del, uint methodHash, List replicates, T data, Channel channel) where T : IReplicateData { //Only check to enque/send if not clientHost. if (!IsServer) { Func isDefaultDel = GeneratedComparer.IsDefault; if (isDefaultDel == null) { NetworkManager.LogError($"ReplicateComparers not found for type {typeof(T).FullName}"); return; } //If there's no datas then reset last replicate send tick. if (replicates.Count == 0) _lastSentReplicateTick = 0; PredictionManager pm = NetworkManager.PredictionManager; bool isDefault = isDefaultDel.Invoke(data); bool mayChange = PredictedTransformMayChange(); bool resetResends = (pm.UsingRigidbodies || mayChange || !isDefault); /* If there is going to be a resend then enqueue data no matter what. * Then ensures there are no data gaps for ticks. EG, input may * look like this... * Move - tick 0. * Idle - tick 1. * Move - tick 2. * * If there were no 'using rigidbodies' then resetResends may be false. * As result the queue would be filled like this... * Move - tick 0. * Move - tick 2. * * The ticks are not sent per data, just once and incremented once per data. * Due to this the results would actually be... * Move - tick 0. * Move - tick 1 (should be tick 2!). * * But by including data if there will be resends the defaults will become added. */ if (resetResends) _remainingResends = pm.RedundancyCount; bool enqueueData = (_remainingResends > 0); if (enqueueData) { /* Replicates will be limited to 1 second * worth on the client. That means the client * will only lose replays if they do not receive * a response back from the server for over a second. * When a client drops a replay it does not necessarily mean * they will be out of synchronization, but rather they * will not be able to reconcile that tick. */ /* Even though limit is 1 second only remove entries if over 2 seconds * to prevent constant remove calls to the collection. */ int maximumReplicates = (TimeManager.TickRate * 2); //If over then remove half the replicates. if (replicates.Count >= maximumReplicates) { int removeCount = (maximumReplicates / 2); //Dispose first. for (int i = 0; i < removeCount; i++) replicates[i].Dispose(); //Then remove. replicates.RemoveRange(0, removeCount); } uint localTick = TimeManager.LocalTick; //Update tick on the data to current. data.SetTick(localTick); //Add to collection. replicates.Add(data); } //If theres resends left. if (_remainingResends > 0) { _remainingResends--; Owner_SendReplicateRpc(methodHash, replicates, channel); //Update last replicate tick. SetLastReplicateTick(TimeManager.LocalTick); } } del.Invoke(data, false, channel, false); } /// /// Reads a replicate the client. /// [CodegenMakePublic] //Internal. internal void Replicate_Reader(PooledReader reader, NetworkConnection sender, T[] arrBuffer, BasicQueue replicates, Channel channel) where T : IReplicateData { PredictionManager pm = PredictionManager; /* Data can be read even if owner is not valid because user * may switch ownership on an object and recv a replicate from * the previous owner. */ int receivedReplicatesCount = reader.ReadReplicate(ref arrBuffer, TimeManager.LastPacketTick); /* Replicate rpc readers relay to this method and * do not have an owner check in the generated code. */ if (!OwnerMatches(sender)) return; if (receivedReplicatesCount > pm.RedundancyCount) { sender.Kick(reader, KickReason.ExploitAttempt, LoggingType.Common, $"Connection {sender.ToString()} sent too many past replicates. Connection will be kicked immediately."); return; } Replicate_HandleReceivedReplicate(receivedReplicatesCount, arrBuffer, replicates, channel); } private void Replicate_HandleReceivedReplicate(int receivedReplicatesCount, T[] arrBuffer, BasicQueue replicates, Channel channel) where T : IReplicateData { PredictionManager pm = PredictionManager; bool consumeExcess = !pm.DropExcessiveReplicates; //Maximum number of replicates allowed to be queued at once. int replicatesCountLimit = (consumeExcess) ? (TimeManager.TickRate * 2) : pm.GetMaximumServerReplicates(); for (int i = 0; i < receivedReplicatesCount; i++) { uint tick = arrBuffer[i].GetTick(); if (tick > _lastReceivedReplicateTick) { //Cannot queue anymore, discard oldest. if (replicates.Count >= replicatesCountLimit) { T data = replicates.Dequeue(); data.Dispose(); } replicates.Enqueue(arrBuffer[i]); _lastReceivedReplicateTick = tick; } } if (IsServer && Owner.IsValid) Owner.AddAverageQueueCount((ushort)replicates.Count, TimeManager.LocalTick); } /// /// Checks conditions for a reconcile. /// /// True if checking as server. /// Returns true if able to continue. [CodegenMakePublic] //internal internal bool Reconcile_ExitEarly_A(bool asServer, out Channel channel) { channel = Channel.Unreliable; //Server. if (asServer) { if (_remainingResends <= 0) return true; _remainingResends--; if (_remainingResends == 0) channel = Channel.Reliable; } //Client. else { if (!ClientHasReconcileData) return true; /* If clientHost then invoke reconciles but * don't actually reconcile. This is done * because certain user code may * rely on those events running even as host. */ if (IsServer) { PredictionManager.InvokeOnReconcile(this, true); PredictionManager.InvokeOnReconcile(this, false); return true; } } //Checks pass. return false; } /// /// Updates lastReconcileTick as though running asServer. /// /// Data to set tick on. [CodegenMakePublic] internal void Reconcile_Server(uint methodHash, T data, Channel channel) where T : IReconcileData { /* //todo * the codegen right now calls this if asServer * and Reconcile_Client if not. * They both need to be called and each handled appropriately, * like done for the replicate method. * Do not forget to make this into a new method using defines, * for for predictionv1 and v2. */ //Server always uses last replicate tick as reconcile tick. uint tick = _lastReplicateTick; data.SetTick(tick); SetLastReconcileTick(tick); PredictionManager.InvokeServerReconcile(this, true); Server_SendReconcileRpc(methodHash, data, channel); PredictionManager.InvokeServerReconcile(this, false); } /// /// Processes a reconcile for client. /// [CodegenMakePublic] internal void Reconcile_Client(ReconcileUserLogicDelegate reconcileDel, ReplicateUserLogicDelegate replicateULDel, List replicates, T data, Channel channel) where T : IReconcileData where T2 : IReplicateData { uint tick = data.GetTick(); /* If the first entry in cllection has a tick higher than * the received tick then something went wrong, do not reconcile. */ if (replicates.Count > 0 && replicates[0].GetTick() > tick) return; UnityScene scene = gameObject.scene; PhysicsScene ps = scene.GetPhysicsScene(); PhysicsScene2D ps2d = scene.GetPhysicsScene2D(); //This must be set before reconcile is invoked. SetLastReconcileTick(tick); //Invoke that reconcile is starting. PredictionManager.InvokeOnReconcile(this, true); //Call reconcile user logic. reconcileDel?.Invoke(data, false, channel); //True if the timemanager is handling physics simulations. bool tmPhysics = (TimeManager.PhysicsMode == PhysicsMode.TimeManager); //Sync transforms if using tm physics. if (tmPhysics) { Physics.SyncTransforms(); Physics2D.SyncTransforms(); } //Remove excess from buffered inputs. int queueIndex = -1; for (int i = 0; i < replicates.Count; i++) { if (replicates[i].GetTick() == tick) { queueIndex = i; break; } } //Now found, weird. if (queueIndex == -1) replicates.Clear(); //Remove up to found, including it. else replicates.RemoveRange(0, queueIndex + 1); //Number of replays which will be performed. int replays = replicates.Count; float tickDelta = (float)TimeManager.TickDelta; for (int i = 0; i < replays; i++) { T2 rData = replicates[i]; uint replayTick = rData.GetTick(); PredictionManager.InvokeOnReplicateReplay(scene, replayTick, ps, ps2d, true); //Replay the data using the replicate logic delegate. replicateULDel.Invoke(rData, false, channel, true); if (tmPhysics) { ps.Simulate(tickDelta); ps2d.Simulate(tickDelta); } PredictionManager.InvokeOnReplicateReplay(scene, replayTick, ps, ps2d, false); } //Reconcile ended. PredictionManager.InvokeOnReconcile(this, false); } /// /// Reads a reconcile the client. /// public void Reconcile_Reader(PooledReader reader, ref T data, Channel channel) where T : IReconcileData { uint tick = reader.ReadUInt32(); T newData = reader.Read(); //Tick is old or already processed. if (tick <= _lastReceivedReconcileTick) return; //Only owner reconciles. Maybe ownership changed then packet arrived out of order. if (!IsOwner) return; data = newData; data.SetTick(tick); ClientHasReconcileData = true; _lastReceivedReconcileTick = tick; } } }