#if UNITY_EDITOR || DEVELOPMENT_BUILD #define DEVELOPMENT #endif using FishNet.CodeGenerating; using FishNet.Connection; using FishNet.Documenting; using FishNet.Managing; 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; using GameKit.Dependencies.Utilities; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using GameKit.Dependencies.Utilities.Types; using UnityEngine; [assembly: InternalsVisibleTo(UtilityConstants.CODEGEN_ASSEMBLY_NAME)] namespace FishNet.Object { #region Types. internal static class ReplicateTickFinder { public enum DataPlacementResult { /// /// Something went wrong; this should never be returned. /// Error, /// /// Tick was found on an index. /// Exact, /// /// Tick was not found because it is lower than any of the replicates. /// This is also used when there are no datas. /// InsertBeginning, /// /// Tick was not found but can be inserted in the middle of the collection. /// InsertMiddle, /// /// Tick was not found because it is larger than any of the replicates. /// InsertEnd, } /// /// Gets the index in replicates where the tick matches. /// public static int GetReplicateHistoryIndex(uint tick, RingBuffer> replicatesHistory, out DataPlacementResult findResult) where T : IReplicateData, new() { int replicatesCount = replicatesHistory.Count; if (replicatesCount == 0) { findResult = DataPlacementResult.InsertBeginning; return 0; } uint firstTick = replicatesHistory[0].Data.GetTick(); //Try to find by skipping ahead the difference between tick and start. int diff = (int)(tick - firstTick); /* If the difference is larger than replicatesCount * then that means the replicates collection is missing * entries. EG if replicates values were 4, 7, 10 and tick were * 10 the difference would be 6. While replicates does contain the value * there is no way it could be found by pulling index 'diff' since that * would be out of bounds. This should never happen under normal conditions, return * missing if it does. */ //Do not need to check less than 0 since we know if here tick is larger than first entry. if (diff >= replicatesCount) { //Try to return value using brute force. int index = FindIndexBruteForce(out findResult); return index; } else if (diff < 0) { findResult = DataPlacementResult.InsertBeginning; return 0; } else { /* If replicatesHistory contained the ticks * of 1 2 3 4 5, and the tick is 3, then the difference * would be 2 (because 3 - 1 = 2). As we can see index * 2 of replicatesHistory does indeed return the proper tick. */ //Expected diff to be result but was not. if (replicatesHistory[diff].Data.GetTick() != tick) { //Try to return value using brute force. int index = FindIndexBruteForce(out findResult); return index; } //Exact was found, this is the most ideal situation. else { findResult = DataPlacementResult.Exact; return diff; } } //Tries to find the index by brute forcing the collection. int FindIndexBruteForce(out DataPlacementResult result) { /* Some quick exits to save perf. */ //If tick is lower than first then it must be inserted at the beginning. if (tick < firstTick) { result = DataPlacementResult.InsertBeginning; return 0; } //If tick is larger the last then it must be inserted at the end. else if (tick > replicatesHistory[replicatesCount - 1].Data.GetTick()) { result = DataPlacementResult.InsertEnd; return replicatesCount; } else { //Brute check. for (int i = 0; i < replicatesCount; i++) { uint lTick = replicatesHistory[i].Data.GetTick(); //Exact match found. if (lTick == tick) { result = DataPlacementResult.Exact; return i; } /* The checked data is greater than * what was being searched. This means * to insert right before it. */ else if (lTick > tick) { result = DataPlacementResult.InsertMiddle; return i; } } //Should be impossible to get here. result = DataPlacementResult.Error; return -1; } } } } //See todo below. /* Update codegen to remove arrBuffer from replicate method calls. * Update codegen to remove channel from replicate method calls where applicable. * Convert BasicQueue/RingBuffer to BasicQueue/RingBuffer>. * */ #endregion public abstract partial class NetworkBehaviour : MonoBehaviour { #region Public. /// /// True if this NetworkBehaviour is reconciling. /// If this NetworkBehaviour does not implemnent prediction methods this value will always be false. /// Value will be false if there is no data to reconcile to, even if the PredictionManager IsReconciling. /// Data may be missing if it were intentionally not sent, or due to packet loss. /// public bool IsBehaviourReconciling { get; internal set; } #endregion #region Private. /// /// Registered Replicate methods. /// private Dictionary _replicateRpcDelegates; /// /// Registered Reconcile methods. /// private Dictionary _reconcileRpcDelegates; /// /// Number of replicate resends which may occur. /// private int _remainingReplicateResends; /// /// Number of reconcile resends which may occur. /// private int _remainingReconcileResends; /// /// Last replicate tick read from remote. This can be the server reading a client or the other way around. /// private uint _lastReplicateReadRemoteTick = TimeManager.UNSET_TICK; /// /// Tick when replicates should begun to run. This is set and used when inputs are just received and need to queue to create a buffer. /// private uint _replicateStartTick = TimeManager.UNSET_TICK; /// /// Last tick to replicate which was not replayed. /// private uint _lastOrderedReplicatedTick = TimeManager.UNSET_TICK; /// /// Last tick read for a replicate. /// private uint _lastReadReplicateTick = TimeManager.UNSET_TICK; /// /// Last tick read for a reconcile. This is only set on the client. /// private uint _lastReadReconcileRemoteTick = TimeManager.UNSET_TICK; /// /// Last tick this object reconciled on. /// private uint _lastReconcileTick = TimeManager.UNSET_TICK; /// /// Last values when checking for transform changes since previous tick. /// private Vector3 _lastTransformPosition; /// /// Last values when checking for transform changes since previous tick. /// private Quaternion _lastTransformRotation; /// /// Last values when checking for transform changes since previous tick. /// private Vector3 _lastTransformScale; /// /// True if this Networkbehaviour implements prediction methods. /// [APIExclude] private bool _usesPrediction; #endregion /// /// Initializes the NetworkBehaviour for prediction. /// internal void Preinitialize_Prediction(bool asServer) { } /// /// Deinitializes the NetworkBehaviour for prediction. /// internal void Deinitialize_Prediction(bool asServer) { } /// /// Called when the object is destroyed. /// internal void OnDestroy_Prediction() { CollectionCaches.StoreAndDefault(ref _replicateRpcDelegates); CollectionCaches.StoreAndDefault(ref _reconcileRpcDelegates); } /// /// Registers a RPC method. /// Internal use. /// /// /// [MakePublic] internal void RegisterReplicateRpc(uint hash, ReplicateRpcDelegate del) { _usesPrediction = true; if (_replicateRpcDelegates == null) _replicateRpcDelegates = CollectionCaches.RetrieveDictionary(); _replicateRpcDelegates[hash] = del; } /// /// Registers a RPC method. /// Internal use. /// /// /// [MakePublic] internal void RegisterReconcileRpc(uint hash, ReconcileRpcDelegate del) { if (_reconcileRpcDelegates == null) _reconcileRpcDelegates = CollectionCaches.RetrieveDictionary(); _reconcileRpcDelegates[hash] = del; } /// /// Called when a replicate is received. /// internal void OnReplicateRpc(uint? methodHash, PooledReader reader, NetworkConnection sendingClient, Channel channel) { if (methodHash == null) methodHash = ReadRpcHash(reader); reader.NetworkManager = _networkObjectCache.NetworkManager; 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. /// internal void OnReconcileRpc(uint? methodHash, PooledReader reader, Channel channel) { if (methodHash == null) methodHash = ReadRpcHash(reader); reader.NetworkManager = _networkObjectCache.NetworkManager; 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."); } /// /// Resets cached ticks used by prediction, such as last read and replicate tick. /// This is generally used when the ticks will be different then what was previously used; eg: when ownership changes. /// private void ResetState_Prediction(bool asServer) { if (!asServer) { _lastReadReconcileRemoteTick = TimeManager.UNSET_TICK; _lastReconcileTick = TimeManager.UNSET_TICK; } _lastOrderedReplicatedTick = TimeManager.UNSET_TICK; _lastReplicateReadRemoteTick = TimeManager.UNSET_TICK; _lastReadReplicateTick = TimeManager.UNSET_TICK; ClearReplicateCache(); } /// /// Clears cached replicates for server and client. This can be useful to call on server and client after teleporting. /// public virtual void ClearReplicateCache() { } /// /// Clears cached replicates and histories. /// [MakePublic] internal void ClearReplicateCache_Internal(BasicQueue> replicatesQueue, RingBuffer> replicatesHistory, RingBuffer> reconcilesHistory, ref T lastReadReplicate, ref T2 lastReadReconcile) where T : IReplicateData, new() where T2 : IReconcileData, new() { while (replicatesQueue.Count > 0) { ReplicateDataContainer dataContainer = replicatesQueue.Dequeue(); dataContainer.Dispose(); } if (lastReadReplicate != null) lastReadReplicate.Dispose(); lastReadReplicate = default; if (lastReadReconcile != null) lastReadReconcile.Dispose(); lastReadReconcile = default; for (int i = 0; i < replicatesHistory.Count; i++) { ReplicateDataContainer dataContainer = replicatesHistory[i]; dataContainer.Dispose(); } replicatesHistory.Clear(); ClearReconcileHistory(reconcilesHistory); } /// /// Sends a RPC to target. /// Internal use. /// [MakePublic] private void Server_SendReconcileRpc(uint hash, ref T lastReconcileData, T reconcileData, Channel channel) where T : IReconcileData { if (!IsSpawned) return; //If channel is reliable set remaining resends to 1. if (channel == Channel.Reliable) _remainingReconcileResends = 1; if (_remainingReconcileResends == 0) return; _remainingReconcileResends--; //No owner and no state forwarding, nothing to do. bool stateForwarding = _networkObjectCache.EnableStateForwarding; if (!Owner.IsValid && !stateForwarding) return; /* Set the channel for Rpcs to reliable to that the length * is written. The data does not actually send reliable, unless * the channel is of course that to start. */ /* This is a temporary solution to resolve an issue which was * causing parsing problems due to states sending unreliable and reliable * headers being written, or sending reliably and unreliable headers being written. * Using an extra byte to write length is more preferred than always forcing reliable * until properly resolved. */ const Channel rpcChannel = Channel.Reliable; PooledWriter methodWriter = WriterPool.Retrieve(); /* Tick does not need to be written because it will always * be the localTick of the server. For the clients, this will * be the LastRemoteTick of the packet. * * The exception is for the owner, which we send the last replicate * tick so the owner knows which to roll back to. */ //#if !FISHNET_STABLE_SYNCTYPES #if DO_NOT_USE methodWriter.WriteDeltaReconcile(lastReconcileData, reconcileData, GetDeltaSerializeOption()); #else methodWriter.WriteReconcile(reconcileData); #endif lastReconcileData = reconcileData; PooledWriter writer; #if DEVELOPMENT if (!NetworkManager.DebugManager.DisableReconcileRpcLinks && _rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #else if (_rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #endif writer = CreateLinkedRpc(link, methodWriter, rpcChannel); else writer = CreateRpc(hash, methodWriter, PacketId.Reconcile, rpcChannel); //If state forwarding is not enabled then only send to owner. if (!stateForwarding) { Owner.WriteState(writer); } //State forwarding, send to all. else { foreach (NetworkConnection nc in Observers) nc.WriteState(writer); } methodWriter.Store(); writer.Store(); } /// /// Returns if there is a chance the transform may change after the tick. /// /// private bool TransformChanged() { if (TimeManager.PhysicsMode == PhysicsMode.Disabled) return false; /* 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 anyChanged = false; anyChanged |= (transform.position - _lastTransformPosition).sqrMagnitude > changeDistance; if (!anyChanged) anyChanged |= (transform.rotation.eulerAngles - _lastTransformRotation.eulerAngles).sqrMagnitude > changeDistance; if (!anyChanged) anyChanged |= (transform.localScale - _lastTransformScale).sqrMagnitude > changeDistance; //If transform changed update last values. if (anyChanged) { _lastTransformPosition = transform.position; _lastTransformRotation = transform.rotation; _lastTransformScale = transform.localScale; } return anyChanged; } /// /// Performs a replicate for current tick. /// [MakePublic] internal void Replicate_Current(ReplicateUserLogicDelegate del, uint methodHash, BasicQueue> replicatesQueue, RingBuffer> replicatesHistory, ReplicateDataContainer dataContainer) where T : IReplicateData, new() { /* Do not run if currently reconciling. * This change allows devs to call inherited replicates * from replays to only run the method logic without * prompting for network action. */ if (_networkObjectCache.PredictionManager.IsReconciling) return; if (_networkObjectCache.IsController) Replicate_Authoritative(del, methodHash, replicatesHistory, dataContainer); else Replicate_NonAuthoritative(del, replicatesQueue, replicatesHistory); } /// /// Returns if a replicates data changed and updates resends as well data tick. /// /// True to enqueue data for replaying. /// True if data has changed.. private void Replicate_Authoritative(ReplicateUserLogicDelegate del, uint methodHash, RingBuffer> replicatesHistory, ReplicateDataContainer dataContainer) where T : IReplicateData, new() { bool ownerlessAndServer = (!Owner.IsValid && IsServerStarted); if (!IsOwner && !ownerlessAndServer) return; Func isDefaultDel = PublicPropertyComparer.IsDefault; if (isDefaultDel == null) { NetworkManager.LogError($"{nameof(PublicPropertyComparer)} not found for type {typeof(T).FullName}"); return; } PredictionManager pm = NetworkManager.PredictionManager; uint dataTick = TimeManager.LocalTick; /* The following code is to remove replicates from replicatesHistory * which exceed the buffer allowance. Replicates are kept for up to * x seconds to clients can re-run them during a reconcile. The reconcile * method removes old histories but given the server does not reconcile, * it will never perform that operation. * The server would not actually need to keep replicates history except * when it is also client(clientHost). This is because the clientHost must * send redundancies to other clients still, therefor that redundancyCount * must be the allowance when clientHost. */ if (IsHostStarted) { int replicatesHistoryCount = replicatesHistory.Count; int maxCount = pm.RedundancyCount; //Number to remove which is over max count. int removeCount = (replicatesHistoryCount - maxCount); //If there are any to remove. if (removeCount > 0) { //Dispose first. for (int i = 0; i < removeCount; i++) replicatesHistory[i].Dispose(); //Then remove range. replicatesHistory.RemoveRange(true, removeCount); } } dataContainer.SetDataTick(dataTick); AddReplicatesHistory(replicatesHistory, dataContainer); //Check to reset resends. bool isDefault = isDefaultDel.Invoke(dataContainer.Data); bool resetResends = (!isDefault || TransformChanged()); byte redundancyCount = PredictionManager.RedundancyCount; //Standard delta serialize option. //+1 to redundancy so lastFirstRead is pushed out to the last actual input when server reads. if (resetResends) { _remainingReplicateResends = redundancyCount; _remainingReconcileResends = redundancyCount; } bool sendData = (_remainingReplicateResends > 0); if (sendData) { /* If not server then send to server. * If server then send to clients. */ bool toServer = !IsServerStarted; Replicate_SendAuthoritative(toServer, methodHash, redundancyCount, replicatesHistory, dataTick, dataContainer.Channel, GetDeltaSerializeOption()); _remainingReplicateResends--; } SetReplicateTick(dataTick, createdReplicate: true); #if !FISHNET_STABLE_REPLICATESTATES //Owner always replicates with new data. del.Invoke(dataContainer.Data, (ReplicateState.Ticked | ReplicateState.Created), dataContainer.Channel); #else del.Invoke(dataContainer.Data, ReplicateState.CurrentCreated, dataContainer.Channel); #endif } /// /// Gets the next replicate in perform when server or non-owning client. /// /// private void Replicate_NonAuthoritative(ReplicateUserLogicDelegate del, BasicQueue> replicatesQueue, RingBuffer> replicatesHistory) where T : IReplicateData, new() { bool serverStarted = _networkObjectCache.IsServerStarted; bool ownerlessAndServer = (!Owner.IsValid && serverStarted); if (IsOwner || ownerlessAndServer) return; /* Still need to run inputs if server, even if forwarding * is not enabled.*/ if (!_networkObjectCache.EnableStateForwarding && !serverStarted) return; TimeManager tm = _networkObjectCache.TimeManager; PredictionManager pm = _networkObjectCache.PredictionManager; uint localTick = tm.LocalTick; bool isServer = _networkObjectCache.IsServerStarted; bool isAppendedOrder = pm.IsAppendedStateOrder; //Server is initialized or appended state order. if (isServer || isAppendedOrder) { int count = replicatesQueue.Count; /* If count is 0 then data must be set default * and as predicted. */ if (count == 0) { ReplicateDefaultData(); } //Not predicted, is user created. else { //Check to unset start tick, which essentially voids it resulting in inputs being run immediately. /* As said above, if start tick is unset then replicates * can run. When still set that means the start condition has * not been met yet. */ if (localTick >= _replicateStartTick) { _replicateStartTick = TimeManager.UNSET_TICK; ReplicateDataContainer queueEntry; bool queueEntryValid = false; while (replicatesQueue.TryDequeue(out queueEntry)) { if (queueEntry.Data.GetTick() > _lastReconcileTick) { queueEntryValid = true; break; } } if (queueEntryValid) { _remainingReconcileResends = pm.RedundancyCount; #if !FISHNET_STABLE_REPLICATESTATES ReplicateData(queueEntry, (ReplicateState.Ticked | ReplicateState.Created)); #else ReplicateData(queueEntry, ReplicateState.CurrentCreated); #endif //Update count since old entries were dropped and one replicate run. count = replicatesQueue.Count; bool consumeExcess = (!pm.DropExcessiveReplicates || IsClientOnlyStarted); int leaveInBuffer = _networkObjectCache.PredictionManager.StateInterpolation; //Only consume if the queue count is over leaveInBuffer. if (consumeExcess && count > leaveInBuffer) { const byte maximumAllowedConsumes = 1; int maximumPossibleConsumes = (count - leaveInBuffer); int consumeAmount = Mathf.Min(maximumAllowedConsumes, maximumPossibleConsumes); for (int i = 0; i < consumeAmount; i++) #if !FISHNET_STABLE_REPLICATESTATES ReplicateData(replicatesQueue.Dequeue(), (ReplicateState.Ticked | ReplicateState.Created)); #else ReplicateData(replicatesQueue.Dequeue(), ReplicateState.CurrentCreated); #endif } } } //Not enough ticks passed yet to run actually data. else { ReplicateDefaultData(); } } } //Is client only and not using future state order. else { ReplicateDefaultData(); } //Performs a replicate using default data. void ReplicateDefaultData() { uint tick = (GetDefaultedLastReplicateTick() + 1); ReplicateDataContainer dataContainer = ReplicateDataContainer.GetDefault(tick); #if !FISHNET_STABLE_REPLICATESTATES ReplicateData(dataContainer, ReplicateState.Ticked); #else ReplicateData(dataContainer, ReplicateState.CurrentFuture); #endif } void ReplicateData(ReplicateDataContainer data, ReplicateState state) { uint tick = data.Data.GetTick(); #if !FISHNET_STABLE_REPLICATESTATES SetReplicateTick(tick, state.ContainsCreated()); #else SetReplicateTick(tick, (state == ReplicateState.CurrentCreated)); #endif /* If server or appended state order then insert/add to history when run * within this method. * Whether data is inserted/added into the past (replicatesHistory) depends on * if client only && and state order. * * Server only adds onto the history after running the inputs. This is so * the server can send past inputs with redundancy. * * Client inserts into the history under two scenarios: * - If state order is using inserted. This is done when the data is read so it * can be iterated during the next reconcile, since the data is not added to * a queue otherwise. This is what causes the requirement to reconcile to run * datas. * - If the state order if using append, and the state just ran. This is so that * the reconcile does not replay data which hasn't yet run. But, the data should still * be inserted at point of run so reconciles can correct to the state at the right * point in history.*/ //Server always adds. if (isServer) AddReplicatesHistory(replicatesHistory, data); //If client insert value into history. else InsertIntoReplicateHistory(data, replicatesHistory); del.Invoke(data.Data, state, data.Channel); } //Returns a replicate tick for when data is not created. uint GetDefaultedLastReplicateTick() { if (_lastOrderedReplicatedTick == TimeManager.UNSET_TICK) _lastOrderedReplicatedTick = (tm.LastPacketTick.Value() + pm.StateInterpolation); return _lastOrderedReplicatedTick; } } /// /// Called internally when an input from localTick should be replayed. /// internal virtual void Replicate_Replay_Start(uint replayTick) { } /// /// Replays inputs from replicates. /// /// The server calls this from codegen but it never completes as IsBehaviourReconciling will always be false on server. [MakePublic] internal void Replicate_Replay(uint replayTick, ReplicateUserLogicDelegate del, RingBuffer> replicatesHistory) where T : IReplicateData, new() { //Reconcile data was not received so cannot replay. if (!IsBehaviourReconciling) return; if (_networkObjectCache.IsController) Replicate_Replay_Authoritative(replayTick, del, replicatesHistory); else Replicate_Replay_NonAuthoritative(replayTick, del, replicatesHistory); } /// /// Replays an input for authoritative entity. /// private void Replicate_Replay_Authoritative(uint replayTick, ReplicateUserLogicDelegate del, RingBuffer> replicatesHistory) where T : IReplicateData, new() { ReplicateTickFinder.DataPlacementResult findResult; int replicateIndex = ReplicateTickFinder.GetReplicateHistoryIndex(replayTick, replicatesHistory, out findResult); ReplicateDataContainer dataContainer; ReplicateState state; //If found then the replicate has been received by the server. if (findResult == ReplicateTickFinder.DataPlacementResult.Exact) { dataContainer = replicatesHistory[replicateIndex]; #if !FISHNET_STABLE_REPLICATESTATES state = (ReplicateState.Replayed | ReplicateState.Ticked | ReplicateState.Created); #else state = ReplicateState.ReplayedCreated; #endif //SetReplicateTick(data.GetTick(), true); del.Invoke(dataContainer.Data, state, dataContainer.Channel); } } /// /// Replays an input for non authoritative entity. /// [MakePublic] private void Replicate_Replay_NonAuthoritative(uint replayTick, ReplicateUserLogicDelegate del, RingBuffer> replicatesHistory) where T : IReplicateData, new() { ReplicateDataContainer dataContainer; ReplicateState state; bool isAppendedOrder = _networkObjectCache.PredictionManager.IsAppendedStateOrder; //If the first replay. if (isAppendedOrder || replayTick == (_networkObjectCache.PredictionManager.ServerStateTick + 1)) { ReplicateTickFinder.DataPlacementResult findResult; int replicateIndex = ReplicateTickFinder.GetReplicateHistoryIndex(replayTick, replicatesHistory, out findResult); //If not found then something went wrong. if (findResult == ReplicateTickFinder.DataPlacementResult.Exact) { dataContainer = replicatesHistory[replicateIndex]; #if !FISHNET_STABLE_REPLICATESTATES state = ReplicateState.Replayed; bool isCreated = dataContainer.IsCreated; //Set if created. if (isCreated) state |= ReplicateState.Created; /* Ticked will be true if value had ticked outside of reconcile, * or if data is created. It's possible for data to be created * and not yet ticked if state order is inserted rather than append. */ if (replayTick <= _lastOrderedReplicatedTick || isCreated) state |= ReplicateState.Ticked; #else //state = ReplicateState.ReplayedCreated; state = (dataContainer.IsCreated) ? ReplicateState.ReplayedCreated : ReplicateState.ReplayedFuture; #endif } else { SetDataToDefault(); } } //Not the first replay tick. else { SetDataToDefault(); } void SetDataToDefault() { dataContainer = ReplicateDataContainer.GetDefault(replayTick); #if !FISHNET_STABLE_REPLICATESTATES state = ReplicateState.Replayed; #else state = ReplicateState.ReplayedFuture; #endif } del.Invoke(dataContainer.Data, state, dataContainer.Channel); } /// /// This is overriden by codegen to call EmptyReplicatesQueueIntoHistory(). /// This should only be called when client only. /// [MakePublic] internal virtual void EmptyReplicatesQueueIntoHistory_Start() { } /// /// Replicates which are enqueued will be removed from the queue and put into replicatesHistory. /// This should only be called when client only. /// [MakePublic] internal void EmptyReplicatesQueueIntoHistory(BasicQueue> replicatesQueue, RingBuffer> replicatesHistory) where T : IReplicateData, new() { while (replicatesQueue.TryDequeue(out ReplicateDataContainer data)) InsertIntoReplicateHistory(data, replicatesHistory); } /// /// Returns the DeltaSerializeOption to use for the tick. /// /// /// private DeltaSerializerOption GetDeltaSerializeOption() { uint localTick = _networkObjectCache.TimeManager.LocalTick; ushort tickRate = _networkObjectCache.TimeManager.TickRate; /* New observers so send a full serialize next replicate. * This could go out to only the newly added observers, but it * would generate a lot more complexity to save presumably * a small amount of occasional bandwidth. */ if (_networkObjectCache.ObserverAddedTick == localTick) return DeltaSerializerOption.FullSerialize; //Send full every half a second. //else if (localTick % tickRate == 0 || localTick % (tickRate / 2) == 0) // return DeltaSerializerOption.FullSerialize; //Send full every second. else if (localTick % tickRate == 0) return DeltaSerializerOption.FullSerialize; //Otherwise return rootSerialize, the default for sending the child most data. else return DeltaSerializerOption.RootSerialize; } /// /// Sends a Replicate to server or clients. /// private void Replicate_SendAuthoritative(bool toServer, uint hash, int pastInputs, RingBuffer> replicatesHistory, uint queuedTick, Channel channel, DeltaSerializerOption deltaOption) where T : IReplicateData, new() { /* Do not use IsSpawnedWithWarning because the server * will still call this a tick or two as clientHost when * an owner disconnects. This comes from calling Replicate(default) * for the server-side processing in NetworkBehaviours. */ if (!IsSpawned) return; int historyCount = replicatesHistory.Count; //Nothing to send; should never be possible. if (historyCount <= 0) return; //Number of past inputs to send. if (historyCount < pastInputs) pastInputs = historyCount; /* Where to start writing from. When passed * into the writer values from this offset * and forward will be written. * Always write up to past inputs. */ int offset = (historyCount - pastInputs); //Write history to methodWriter. PooledWriter methodWriter = WriterPool.Retrieve(WriterPool.LENGTH_BRACKET); /* If going to clients from the server then * write the queueTick. */ if (!toServer) methodWriter.WriteTickUnpacked(queuedTick); //#if !FISHNET_STABLE_SYNCTYPES #if DO_NOT_USE methodWriter.WriteDeltaReplicate(replicatesHistory, offset, deltaOption); #else methodWriter.WriteReplicate(replicatesHistory, offset); #endif _transportManagerCache.CheckSetReliableChannel(methodWriter.Length + MAXIMUM_RPC_HEADER_SIZE, ref channel); PooledWriter writer = CreateRpc(hash, methodWriter, PacketId.Replicate, channel); /* toServer will never be true if clientHost. * When clientHost and here replicates will * always just send to clients, while * excluding clientHost. */ if (toServer) { NetworkManager.TransportManager.SendToServer((byte)channel, writer.GetArraySegment(), splitLargeMessages: true); } else { /* If going to clients from server, then only send * if state forwarding is enabled. */ if (_networkObjectCache.EnableStateForwarding) { //Exclude owner and if clientHost, also localClient. _networkConnectionCache.Clear(); _networkConnectionCache.Add(Owner); if (IsClientStarted) _networkConnectionCache.Add(ClientManager.Connection); NetworkManager.TransportManager.SendToClients((byte)channel, writer.GetArraySegment(), Observers, _networkConnectionCache, splitLargeMessages: true); } } /* If sending as reliable there is no reason * to perform resends, so clear remaining resends. */ if (channel == Channel.Reliable) _remainingReplicateResends = 0; methodWriter.StoreLength(); writer.StoreLength(); } /// /// Reads a replicate the client. /// [MakePublic] internal void Replicate_Reader(uint hash, PooledReader reader, NetworkConnection sender, ref ReplicateDataContainer lastReadReplicate, BasicQueue> replicatesQueue, RingBuffer> replicatesHistory, Channel channel) where T : IReplicateData, new() { /* This will never be received on owner, except in the condition * the server is the owner and also a client. In such condition * the method is exited after data is parsed. */ PredictionManager pm = _networkObjectCache.PredictionManager; TimeManager tm = _networkObjectCache.TimeManager; bool fromServer = (reader.Source == Reader.DataSource.Server); uint tick; /* If coming from the server then read the tick. Server sends tick * if authority or if relaying from another client. The tick which * arrives will be the tick the replicate will run on the server. */ if (fromServer) tick = reader.ReadTickUnpacked(); /* When coming from a client it will always be owner. * Client sends out replicates soon as they are run. * It's safe to use the LastRemoteTick from the client * in addition to QueuedInputs. */ else tick = (tm.LastPacketTick.LastRemoteTick); //#if !FISHNET_STABLE_SYNCTYPES #if DO_NOT_USE receivedReplicatesCount = reader.ReadDeltaReplicate(lastReadReplicate, ref arrBuffer, tick); #else List> readReplicates = reader.ReadReplicate(tick); #endif //Update first read if able. if (readReplicates.Count > 0) { lastReadReplicate.Dispose(); lastReadReplicate = readReplicates[^1]; } //If received on clientHost simply ignore after parsing data. if (fromServer && IsHostStarted) return; /* Replicate rpc readers relay to this method and * do not have an owner check in the generated code. * Only server needs to check for owners. Clients * should accept the servers data regardless. * * If coming from a client and that client is not owner then exit. */ if (!fromServer && !OwnerMatches(sender)) return; //Early exit if old data. if (TimeManager.LastPacketTick.LastRemoteTick < _lastReplicateReadRemoteTick) return; _lastReplicateReadRemoteTick = TimeManager.LastPacketTick.LastRemoteTick; //If from a client that is not clientHost do some safety checks. if (!fromServer && !Owner.IsLocalClient) { if (readReplicates.Count > pm.RedundancyCount) { sender.Kick(reader, KickReason.ExploitAttempt, LoggingType.Common, $"Connection {sender.ToString()} sent too many past replicates. Connection will be kicked immediately."); return; } } Replicate_EnqueueReceivedReplicate(readReplicates, replicatesQueue, replicatesHistory); Replicate_SendNonAuthoritative(hash, replicatesQueue, channel); CollectionCaches>.Store(readReplicates); } /// /// Sends data from a reader which only contains the replicate packet. /// [MakePublic] internal void Replicate_SendNonAuthoritative(uint hash, BasicQueue> replicatesQueue, Channel channel) where T : IReplicateData, new() { if (!IsServerStarted) return; if (!_networkObjectCache.EnableStateForwarding) return; int queueCount = replicatesQueue.Count; //None to send. if (queueCount == 0) return; //If the only observer is the owner then there is no need to write. int observersCount = Observers.Count; //Quick exit for no observers other than owner. if (observersCount == 0 || (Owner.IsValid && observersCount == 1)) return; PooledWriter methodWriter = WriterPool.Retrieve(WriterPool.LENGTH_BRACKET); uint localTick = _networkObjectCache.TimeManager.LocalTick; /* Write when the last entry will run. * * Typically, the last entry will run on localTick + (queueCount - 1). * 1 is subtracted from queueCount because in most cases the first entry * is going to run same tick. * An exception is when the replicateStartTick is set, then there is going * to be a delayed based on start tick difference. */ uint runTickOflastEntry = localTick + ((uint)queueCount - 1); //If start tick is set then add on the delay. if (_replicateStartTick != TimeManager.UNSET_TICK) runTickOflastEntry += (_replicateStartTick - TimeManager.LocalTick); //Write the run tick now. methodWriter.WriteTickUnpacked(runTickOflastEntry); //Write the replicates. int redundancyCount = (int)Mathf.Min(_networkObjectCache.PredictionManager.RedundancyCount, queueCount); //#if !FISHNET_STABLE_SYNCTYPES #if DO_NOT_USE methodWriter.WriteDeltaReplicate(replicatesQueue, redundancyCount, GetDeltaSerializeOption()); #else methodWriter.WriteReplicate(replicatesQueue, redundancyCount); #endif PooledWriter writer = CreateRpc(hash, methodWriter, PacketId.Replicate, channel); //Exclude owner and if clientHost, also localClient. _networkConnectionCache.Clear(); if (Owner.IsValid) _networkConnectionCache.Add(Owner); if (IsClientStarted && !Owner.IsLocalClient) _networkConnectionCache.Add(ClientManager.Connection); NetworkManager.TransportManager.SendToClients((byte)channel, writer.GetArraySegment(), Observers, _networkConnectionCache, false); methodWriter.StoreLength(); writer.StoreLength(); } /// /// Handles a received replicate packet. /// private void Replicate_EnqueueReceivedReplicate(List> readDatas, BasicQueue> replicatesQueue, RingBuffer> replicatesHistory) where T : IReplicateData, new() { int startQueueCount = replicatesQueue.Count; /* Owner never gets this for their own object so * this can be processed under the assumption data is only * handled on unowned objects. */ PredictionManager pm = PredictionManager; bool isServer = _networkObjectCache.IsServerStarted; bool isAppendedOrder = pm.IsAppendedStateOrder; //Maximum number of replicates allowed to be queued at once. int maximmumReplicates = (IsServerStarted) ? pm.GetMaximumServerReplicates() : pm.MaximumPastReplicates; for (int i = 0; i < readDatas.Count; i++) { ReplicateDataContainer dataContainer = readDatas[i]; dataContainer.IsCreated = true; uint tick = dataContainer.Data.GetTick(); //Skip if old data. if (tick <= _lastReadReplicateTick) { dataContainer.Dispose(); continue; } _lastReadReplicateTick = tick; //Cannot queue anymore, discard oldest. if (replicatesQueue.Count > maximmumReplicates) { ReplicateDataContainer disposableDataContainer = replicatesQueue.Dequeue(); disposableDataContainer.Dispose(); } /* Check if replicate is already in history. * This can occur when the replicate method has a predicted * state for the tick, but a user created replicate comes * through afterward. * * Only perform this check if not the server, since server * does not reconcile it will never use replicatesHistory. * * When clients are also using ReplicateStateOrder.Future the replicates * do not need to be put into the past, as they're always added onto * the end of the queue. * * The server also does not predict replicates in the same way * a client does. When an owner sends a replicate to the server * the server only uses the owner tick to check if it's an old replicate. * But when running the replicate, the server applies it's local tick and * sends that to spectators. */ //Add automatically if server or future order. if (isServer || isAppendedOrder) replicatesQueue.Enqueue(dataContainer); //Run checks to replace data if not server. else InsertIntoReplicateHistory(dataContainer, replicatesHistory); } /* If entries are being added after nothing then * start the queued inputs delay. Only the server needs * to do this since clients implement the queue delay * by holding reconcile x ticks rather than not running received * x ticks. */ if ((isServer || isAppendedOrder) && startQueueCount == 0 && replicatesQueue.Count > 0) _replicateStartTick = (_networkObjectCache.TimeManager.LocalTick + pm.StateInterpolation); } /// /// Inserts data into the replicatesHistory collection. /// This should only be called when client only. /// private void InsertIntoReplicateHistory(ReplicateDataContainer dataContainer, RingBuffer> replicatesHistory) where T : IReplicateData, new() { /* See if replicate tick is in history. Keep in mind * this is the localTick from the server, not the localTick of * the client which is having their replicate relayed. */ ReplicateTickFinder.DataPlacementResult findResult; int index = ReplicateTickFinder.GetReplicateHistoryIndex(dataContainer.Data.GetTick(), replicatesHistory, out findResult); /* Exact entry found. This is the most likely * scenario. Client would have already run the tick * in the future, and it's now being replaced with * the proper data. */ if (findResult == ReplicateTickFinder.DataPlacementResult.Exact) { ReplicateDataContainer prevEntry = replicatesHistory[index]; prevEntry.Dispose(); replicatesHistory[index] = dataContainer; } else if (findResult == ReplicateTickFinder.DataPlacementResult.InsertMiddle) { InsertReplicatesHistory(replicatesHistory, dataContainer, index); } else if (findResult == ReplicateTickFinder.DataPlacementResult.InsertEnd) { AddReplicatesHistory(replicatesHistory, dataContainer); } /* Insert beginning should not happen unless the data is REALLY old. * This would mean the network was in an unplayable state. Discard the * data. */ if (findResult == ReplicateTickFinder.DataPlacementResult.InsertBeginning) InsertReplicatesHistory(replicatesHistory, dataContainer, 0); } /// /// Adds to replicate history disposing of old entries if needed. /// private void AddReplicatesHistory(RingBuffer> replicatesHistory, ReplicateDataContainer value) where T : IReplicateData, new() { ReplicateDataContainer prev = replicatesHistory.Add(value); if (prev.Data != null) prev.Dispose(); } /// /// Inserts to replicate history disposing of old entries if needed. /// private void InsertReplicatesHistory(RingBuffer> replicatesHistory, ReplicateDataContainer value, int index) where T : IReplicateData, new() { ReplicateDataContainer prev = replicatesHistory.Insert(index, value); if (prev.Data != null) prev.Dispose(); } /// /// Override this method to create your reconcile data, and call your reconcile method. /// public virtual void CreateReconcile() { } /// /// Sends a reconcile to clients. /// [MakePublic] internal void Reconcile_Server(uint methodHash, ref T lastReconcileData, T data, Channel channel) where T : IReconcileData { //Tick does not need to be set for reconciles since they come in as state updates, which have the tick included globally. if (IsServerInitialized) Server_SendReconcileRpc(methodHash, ref lastReconcileData, data, channel); } /// /// This is called when the NetworkBehaviour should perform a reconcile. /// Codegen overrides this calling Reconcile_Client with the needed data. /// [MakePublic] internal virtual void Reconcile_Client_Start() { } /// /// Adds a reconcile to local reconcile history. /// [MakePublic] internal void Reconcile_Client_AddToLocalHistory(RingBuffer> reconcilesHistory, T data) where T : IReconcileData { //Server does not need to store these locally. if (_networkObjectCache.IsServerStarted) return; if (!_networkObjectCache.PredictionManager.CreateLocalStates) return; /* This is called by the local client when creating * a local reconcile state. These states should always * be in order, so we will add data to the end * of the collection. */ /* These datas are used to fill missing reconciles * be it the packet dropped, server doesnt need to send, * or if the player is throttling reconciles. */ uint tick = _networkObjectCache.PredictionManager.GetCreateReconcileTick(_networkObjectCache.IsOwner); //Tick couldn't be retrieved. if (tick == TimeManager.UNSET_TICK) return; data.SetTick(tick); //Build LocalReconcile. LocalReconcile lr = new(); lr.Initialize(tick, data); reconcilesHistory.Add(lr); } /// /// Called by codegen with data provided by user, such as from overriding CreateReconcile. /// [MakePublic] internal void Reconcile_Current(uint hash, ref T lastReconcileData, RingBuffer> reconcilesHistory, T data, Channel channel) where T : IReconcileData, new() { if (_networkObjectCache.PredictionManager.IsReconciling) return; if (_networkObjectCache.IsServerInitialized) Reconcile_Server(hash, ref lastReconcileData, data, channel); else Reconcile_Client_AddToLocalHistory(reconcilesHistory, data); } /// /// Runs a reconcile. Prefers server data if available, otherwise uses local history data. /// [MakePublic] internal void Reconcile_Client(ReconcileUserLogicDelegate reconcileDel, RingBuffer> replicatesHistory, RingBuffer> reconcilesHistory, T data) where T : IReconcileData where T2 : IReplicateData, new() { bool isBehaviourReconciling = IsBehaviourReconciling; const long unsetHistoryIndex = -1; long historyIndex = unsetHistoryIndex; /* There should always be entries, except when the object * first spawns. * * Find the history index associated with the reconcile tick. */ if (reconcilesHistory.Count > 0) { //If reconcile data received then use that tick, otherwise get estimated tick for this reconcile. uint reconcileTick = (isBehaviourReconciling) ? data.GetTick() : _networkObjectCache.PredictionManager.GetReconcileStateTick(_networkObjectCache.IsOwner); uint firstHistoryTick = reconcilesHistory[0].Tick; historyIndex = ((long)reconcileTick - (long)firstHistoryTick); /* If difference is negative then * the first history is beyond the tick being reconciled. * EG: if history index 0 is 100 and reconcile tick is 90 then * (90 - 100) = -10. * This should only happen when first connecting and data hasn't been made yet. */ if (!IsHistoryIndexValid((int)historyIndex)) { historyIndex = unsetHistoryIndex; ClearReconcileHistory(reconcilesHistory); } //Valid history index. else { //Get the tick at the index. uint lrTick = reconcilesHistory[(int)historyIndex].Tick; /* Since we store reconcile data every tick moving ahead a set number of ticks * should usually match up to the reconcile tick. There are exceptions where the tick * used to locally create the reconcile was for non owner, so using the server tick, * and there is a slight misalignment in the server tick. This is not unusual as the * client corrects it's tick timing regularly, but such an alignment could make this not line up. */ /* If the history tick does not match the reconcile tick try to find * the correct history tick. This should rarely happen but since these reconciles * are created locally and client timing can vary slightly it's still possible. */ if (lrTick != reconcileTick) { /* Get the difference between what tick is stored vs reconcile tick. * Adjust the index based on this difference. */ long tickDifference = ((long)reconcileTick - (long)lrTick); /* Add difference onto history index and again validate that it * is in range of the collection. */ historyIndex += tickDifference; //Invalid. if (!IsHistoryIndexValid((int)historyIndex)) { /* This shouldn't ever happen. Something went very wrong if here. * When this does happen clear out the entire history collection * and start over. */ ClearReconcileHistory(reconcilesHistory); //Unset index. historyIndex = unsetHistoryIndex; } } //If index is set and behaviour is not reconciling then apply data. if (!isBehaviourReconciling && historyIndex != unsetHistoryIndex) { LocalReconcile localReconcile = reconcilesHistory[(int)historyIndex]; //Before disposing get the writer and call reconcile reader so it's parsed. PooledWriter reconcileWritten = localReconcile.Writer; /* Although this is actually from the local client the datasource is being set to server since server * is what typically sends reconciles. */ PooledReader reader = ReaderPool.Retrieve(reconcileWritten.GetArraySegment(), _networkObjectCache.NetworkManager, Reader.DataSource.Server); data = Reconcile_Reader_Local(localReconcile.Tick, reader); ReaderPool.Store(reader); } } } //Returns if a history index can be within history collection. bool IsHistoryIndexValid(int index) => (index >= 0 && (index < reconcilesHistory.Count)); //Dispose of old reconcile histories. if (historyIndex != unsetHistoryIndex) { int index = (int)historyIndex; //If here everything is good, remove up to used index. for (int i = 0; i < index; i++) reconcilesHistory[i].Dispose(); reconcilesHistory.RemoveRange(true, (int)historyIndex); } //If does not have data still then exit method. if (!IsBehaviourReconciling) return; //Set on the networkObject that a reconcile can now occur. _networkObjectCache.IsObjectReconciling = true; uint dataTick = data.GetTick(); _lastReconcileTick = dataTick; if (replicatesHistory.Count > 0) { /* Remove replicates up to reconcile. Since the reconcile * is the state after a replicate for it's tick we no longer * need any replicates prior. */ //Find the closest entry which can be removed. int removeCount = 0; //A few quick tests. if (replicatesHistory.Count > 0) { /* If the last entry in history is less or equal * to datatick then all histories need to be removed * as reconcile is beyond them. */ if (replicatesHistory[^1].Data.GetTick() <= dataTick) { removeCount = replicatesHistory.Count; } //Somewhere in between. Find what to remove up to. else { for (int i = 0; i < replicatesHistory.Count; i++) { uint entryTick = replicatesHistory[i].Data.GetTick(); /* Soon as an entry beyond dataTick is * found remove up to that entry. */ if (entryTick > dataTick) { removeCount = i; break; } } } } for (int i = 0; i < removeCount; i++) replicatesHistory[i].Dispose(); replicatesHistory.RemoveRange(true, removeCount); } //Call reconcile user logic. reconcileDel?.Invoke(data, Channel.Reliable); } internal void Reconcile_Client_End() { IsBehaviourReconciling = false; } /// /// Disposes and clears LocalReconciles. /// private void ClearReconcileHistory(RingBuffer> reconcilesHistory) where T : IReconcileData { foreach (LocalReconcile localReconcile in reconcilesHistory) localReconcile.Dispose(); reconcilesHistory.Clear(); } /// /// Reads a reconcile from the server. /// public void Reconcile_Reader(PooledReader reader, ref T lastReconcileData) where T : IReconcileData { uint tick = (IsOwner) ? PredictionManager.ClientStateTick : PredictionManager.ServerStateTick; //#if !FISHNET_STABLE_SYNCTYPES #if DO_NOT_USE T newData = reader.ReadDeltaReconcile(lastReconciledata); #else T newData = reader.ReadReconcile(); #endif //Do not process if an old state. if (tick < _lastReadReconcileRemoteTick) return; lastReconcileData = newData; lastReconcileData.SetTick(tick); IsBehaviourReconciling = true; _networkObjectCache.IsObjectReconciling = true; _lastReadReconcileRemoteTick = tick; } /// /// Reads a local reconcile from the client. /// public T Reconcile_Reader_Local(uint tick, PooledReader reader) where T : IReconcileData { reader.NetworkManager = _networkObjectCache.NetworkManager; T newData = reader.ReadReconcile(); newData.SetTick(tick); IsBehaviourReconciling = true; return newData; } /// /// Sets the last tick this NetworkBehaviour replicated with. /// /// True to set unordered value, false to set ordered. private void SetReplicateTick(uint value, bool createdReplicate) { _lastOrderedReplicatedTick = value; _networkObjectCache.SetReplicateTick(value, createdReplicate); } } }