diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs
new file mode 100644
index 000000000..c6806fa79
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs
@@ -0,0 +1,96 @@
+using UnityEngine;
+using System;
+
+namespace Mirror{
+ [DefaultExecutionOrder(-10)]
+ [DisallowMultipleComponent]
+ [AddComponentMenu("Network/Network Physics Controller")]
+ public class NetworkPhysicsController : MonoBehaviour{
+ ///
+ /// Callback action to handle tick-forwarding logic.
+ /// Allows external classes to define custom behavior when the tick advances.
+ ///
+ public Action TickForwardCallback;
+
+ private static int _reconcileStartTick = 0;
+
+ ///
+ /// Advances the game state by a specified number of ticks.
+ /// Invokes the TickForwardCallback to allow external classes to handle tick-forwarding logic.
+ /// Typically called with `deltaTicks` = 1 from RunSimulate.
+ ///
+ /// The number of ticks to forward.
+ public virtual void TickForward(int deltaTicks) {
+ TickForwardCallback?.Invoke(deltaTicks);
+ }
+
+ ///
+ /// Executes a single physics simulation step for the given delta time.
+ /// Uses Unity's Physics.Simulate to perform the physics tick.
+ /// Typically called with Time.fixedDeltaTime.
+ ///
+ /// The time interval to simulate physics for.
+ public virtual void PhysicsTick(float deltaTime) {
+ Physics.Simulate(deltaTime); // Using Unity's built-in physics engine.
+ }
+
+ ///
+ /// Runs the simulation for the specified number of delta ticks.
+ /// This method performs multiple steps of entity updates and physics ticks
+ /// to bring the simulation in sync with the latest tick count.
+ ///
+ /// The number of ticks to simulate forward.
+ public void RunSimulate(int deltaTicks) {
+ var deltaTime = Time.fixedDeltaTime;
+ for (var step = 0; step < deltaTicks; step++) {
+ TickForward(1);
+ NetworkPhysicsEntity.RunBeforeNetworkUpdates(1, deltaTime);
+ NetworkPhysicsEntity.RunNetworkUpdates(1, deltaTime);
+ PhysicsTick(deltaTime);
+ NetworkPhysicsEntity.RunAfterNetworkUpdates(1, deltaTime);
+ }
+ }
+
+ ///
+ /// Runs the simulation for the specified number of delta ticks as a single batch.
+ /// This method performs a single set of entity updates and a single physics tick
+ /// scaled to account for the total number of ticks, useful for batching simulations.
+ ///
+ /// The number of ticks to simulate forward in one batch.
+ public void RunBatchSimulate(int deltaTicks) {
+ var deltaTime = Time.fixedDeltaTime * deltaTicks;
+ TickForward(deltaTicks);
+ NetworkPhysicsEntity.RunBeforeNetworkUpdates(deltaTicks, deltaTime);
+ NetworkPhysicsEntity.RunNetworkUpdates(deltaTicks, deltaTime);
+ PhysicsTick(deltaTime); // Uses scaled deltaTime for batch processing
+ NetworkPhysicsEntity.RunAfterNetworkUpdates(deltaTicks, deltaTime);
+ }
+
+ ///
+ /// Requests the reconciliation process to start from a specific tick.
+ /// Stores the earliest requested tick before reconciliation is executed.
+ ///
+ /// The tick from which to start reconciliation.
+ public static void RequestReconcileFromTick(int reconcileStartTick) {
+ if (_reconcileStartTick > reconcileStartTick || _reconcileStartTick == 0) {
+ _reconcileStartTick = reconcileStartTick; // the +1 is important to include the faulty tick
+ }
+ }
+
+ ///
+ /// Retrieves the tick number from which reconciliation should start.
+ ///
+ /// The tick number from which to start reconciliation.
+ public int GetReconcileStartTick() {
+ return _reconcileStartTick;
+ }
+
+ ///
+ /// Resets the reconciliation counter, marking the reconciliation process as complete.
+ /// Sets _ticksToReconcile to 0, indicating no further reconciliation is required.
+ ///
+ public void ResetReconcile() {
+ _reconcileStartTick = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta
new file mode 100644
index 000000000..9e6d11c0c
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b36faa4b9565404b95ba6539a10fc47f
+timeCreated: 1730317284
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs
new file mode 100644
index 000000000..bef492b3f
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+
+namespace Mirror{
+ ///
+ /// Interface representing a network item that requires updates at various stages of the network tick cycle.
+ /// Each method in this interface is intended to handle specific stages of the update process.
+ ///
+ public interface INetworkedItem{
+ ///
+ /// Called before the main network update, allowing the item to perform any necessary preparation or pre-update logic.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ void BeforeNetworkUpdate(int deltaTicks, float deltaTime);
+
+ ///
+ /// Called during the main network update, allowing the item to handle core updates related to network state, physics, or entity positioning.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ void OnNetworkUpdate(int deltaTicks, float deltaTime);
+
+ ///
+ /// Called after the main network update, allowing the item to perform any necessary cleanup or post-update logic.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ void AfterNetworkUpdate(int deltaTicks, float deltaTime);
+ }
+
+ ///
+ /// Manages network update sequences for entities requiring tick-based adjustments.
+ ///
+ public class NetworkPhysicsEntity{
+ /// Stores items requiring updates on each tick, as a list of tuples with priority and item.
+ private static readonly List<(int priority, INetworkedItem item)> NetworkItems = new List<(int, INetworkedItem)>();
+
+ /// Adds a network entity to the collection for updates and sorts by priority.
+ /// The network item implementing that requires tick updates.
+ /// The priority for the entity, with lower numbers indicating higher priority.
+ public static void AddNetworkEntity(INetworkedItem item, int priority = 0) {
+ NetworkItems.Add((priority, item));
+ NetworkItems.Sort((x, y) => y.priority.CompareTo(x.priority));
+ // Fortunately, List.Sort() in C# uses a stable sorting algorithm so same priority remains in the same order and new items are added to the end
+ // [2-a, 1-a, 1-b, 0-a, 0-b, 0-c] + [1-c] => [2-a, 1-a, 1-b, 1-c, 0-a, 0-b, 0-c]
+ }
+
+ /// Removes a network entity from the collection based on the item reference only.
+ /// The network item to remove.
+ public static void RemoveNetworkEntity(INetworkedItem item) {
+ NetworkItems.RemoveAll(entry => entry.item.Equals(item));
+ }
+
+
+ ///
+ /// Runs the BeforeNetworkUpdate method on each network item in priority order.
+ /// This method is intended to perform any necessary setup or pre-update logic before the main network updates are processed.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ public static void RunBeforeNetworkUpdates(int deltaTicks, float deltaTime) {
+ foreach (var (priority, item) in NetworkItems) {
+ item.BeforeNetworkUpdate(deltaTicks, deltaTime);
+ }
+ }
+
+
+ ///
+ /// Runs the OnNetworkUpdate method on each network item in priority order.
+ /// This method executes the main network update logic for each item, handling any core updates needed for the network state or entity positions.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ public static void RunNetworkUpdates(int deltaTicks, float deltaTime) {
+ foreach (var (priority, item) in NetworkItems) {
+ item.OnNetworkUpdate(deltaTicks, deltaTime);
+ }
+ }
+
+
+ ///
+ /// Runs the AfterNetworkUpdate method on each network item in priority order.
+ /// This method is intended for any necessary cleanup or post-update logic following the main network updates.
+ ///
+ /// The number of ticks since the last update.
+ /// The time elapsed since the last update in seconds.
+ public static void RunAfterNetworkUpdates(int deltaTicks, float deltaTime) {
+ foreach (var (priority, item) in NetworkItems) {
+ item.AfterNetworkUpdate(deltaTicks, deltaTime);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta
new file mode 100644
index 000000000..7d5484a56
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8b08c8ad7b0840fdb96784accdc66787
+timeCreated: 1730400815
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkTick.cs b/Assets/Mirror/Core/TickManager/NetworkTick.cs
new file mode 100644
index 000000000..f8d9722c4
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkTick.cs
@@ -0,0 +1,245 @@
+using System;
+
+namespace Mirror{
+ public class NetworkTick{
+ /*** Private Definitions ***/
+
+ #region Private Definitions
+
+ // Current state flags
+ private static bool _isServer = false;
+ private static bool _isSynchronizing = false;
+ private static bool _isSynchronized = false;
+ private static bool _isReconciling = false;
+
+ // Internal tick counters
+ private static int _clientTick = 0;
+ private static int _serverTick = 0;
+ private static int _absoluteClientTick = 0;
+ private static int _absoluteServerTick = 0;
+
+ // Packet loss compensation ticks
+ private static int _clientToServerPacketLossCompensation = 0;
+ private static int _serverToClientPacketLossCompensation = 0;
+
+ #endregion
+
+ /*** CLIENT ONLY METHODS ***/
+
+ #region CLIENT ONLY METHODS
+
+ /// Gets the client-to-server packet loss compensation ticks. Client-only: This cant be accessed on the server.
+ /// Thrown if accessed on the server.
+ public static int ClientToServerPacketLossCompensation {
+ get {
+ if (_isServer) throw new InvalidOperationException("ClientToServerPacketLossCompensation is client-only and cannot be accessed on the server.");
+ return _clientToServerPacketLossCompensation;
+ }
+ }
+
+ /// Gets the server-to-client packet loss compensation ticks. Client-only: This cant be accessed on the server.
+ /// Thrown if accessed on the server.
+ public static int ServerToClientPacketLossCompensation {
+ get {
+ if (_isServer) throw new InvalidOperationException("ServerToClientPacketLossCompensation is client-only and cannot be accessed on the server.");
+ return _serverToClientPacketLossCompensation;
+ }
+ }
+
+ ///
+ /// Sets the client-to-server packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss.
+ /// Client-only: This method should not be called on the server.
+ ///
+ /// The number of compensation ticks to set.
+ public void SetClientToServerPacketLossCompensation(int compensationTicks) {
+ if (_isServer) throw new InvalidOperationException("SetClientToServerPacketLossCompensation is client-only and cannot be accessed on the server.");
+ _clientToServerPacketLossCompensation = compensationTicks;
+ }
+
+ ///
+ /// Sets the server-to-client packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss.
+ /// Client-only: This method should not be called on the server.
+ ///
+ /// The number of compensation ticks to set.
+ public void SetServerToClientPacketLossCompensation(int compensationTicks) {
+ if (_isServer) throw new InvalidOperationException("SetServerToClientPacketLossCompensation is client-only and cannot be accessed on the server.");
+ _serverToClientPacketLossCompensation = compensationTicks;
+ }
+
+ #endregion
+
+ /*** Static Status Getters ***/
+
+ #region Static Status Getters
+
+ /// Gets a value indicating whether the current instance is a server.
+ public static bool IsServer => _isServer;
+
+ /// Gets a value indicating whether the client is synchronizing with the server.
+ public static bool IsSynchronizing => _isSynchronizing;
+
+ /// Gets a value indicating whether the client is synchronized with the server.
+ public static bool IsSynchronized => _isSynchronized;
+
+ /// Gets a value indicating whether the system is reconciling ticks.
+ public static bool IsReconciling => _isReconciling;
+
+ #endregion
+
+ /*** Static Tick Getters ***/
+
+ #region Static Tick Getters
+
+ /// Gets the current tick count based on whether the instance is a server or client.
+ public static int CurrentTick => _isServer ? _serverTick : _clientTick;
+
+ /// Gets the current absolute tick count based on whether the instance is a server or client.
+ public static int CurrentAbsoluteTick => _isServer ? _absoluteServerTick : _absoluteClientTick;
+
+ /// Gets the client tick count.
+ public static int ClientTick => _clientTick;
+
+ /// Gets the client absolute tick count.
+ public static int ClientAbsoluteTick => _absoluteClientTick;
+
+ /// Gets the server tick count.
+ public static int ServerTick => _serverTick;
+
+ /// Gets the server tick count.
+ public static int ServerAbsoluteTick => _absoluteServerTick;
+
+ #endregion
+
+ /*** Instance Getters ***/
+
+ #region Instance Getters
+
+ /// Checks if the client is in the process of synchronizing with the server.
+ public bool GetIsSynchronizing() => _isSynchronizing;
+
+ /// Checks if the client is currently synchronized with the server.
+ public bool GetIsSynchronized() => _isSynchronized;
+
+ /// Gets the current client tick value.
+ public int GetClientTick() => _clientTick;
+
+ /// Gets the absolute tick value for the client.
+ public int GetClientAbsoluteTick() => _absoluteClientTick;
+
+ /// Gets the current client tick value.
+ public int GetServerTick() => _serverTick;
+
+ /// Gets the absolute tick value for the server.
+ public int GetServerAbsoluteTick() => _absoluteServerTick;
+
+ #endregion
+
+ /*** Instance Status Setters ***/
+
+ #region Instance Status Setters
+
+ /// Sets the server status of the current instance.
+ public void SetIsServer(bool isServer) => _isServer = isServer;
+
+ /// Sets the synchronization status between client and server.
+ public void SetSynchronized(bool isSynchronized) => _isSynchronized = isSynchronized;
+
+ /// Sets the synchronization status between client and server.
+ public void SetSynchronizing(bool isSynchronizing) => _isSynchronizing = isSynchronizing;
+
+ /// Sets the reconciling status.
+ public void SetReconciling(bool reconciling) => _isReconciling = reconciling;
+
+ #endregion
+
+ /*** Instance Tick Setters ***/
+
+ #region Instance Tick Setters
+
+ /// Sets a new tick value for the client.
+ public void SetClientTick(int newTick) => _clientTick = newTick;
+
+ /// Sets a new absolute tick value for the client.
+ public void SetClientAbsoluteTick(int newAbsoluteTick) => _absoluteClientTick = newAbsoluteTick;
+
+ /// Sets a new tick value for the server.
+ public void SetServerTick(int newTick) => _serverTick = newTick;
+
+ /// Sets a new absolute tick value for the server.
+ public void SetServerAbsoluteTick(int newAbsoluteTick) => _absoluteServerTick = newAbsoluteTick;
+
+ #endregion
+
+ /*** Instance Tick Modifiers ***/
+
+ #region Instance Tick Modifiers
+
+ /// Increments the client tick by a specified amount, wrapping to 11 bits.
+ public void IncrementClientTick(int increment) => _clientTick = (_clientTick + increment) & 0b11111111111;
+
+ /// Increments the client's absolute tick by a specified amount.
+ public void IncrementClientAbsoluteTick(int increment) => _absoluteClientTick += increment;
+
+ /// Increments the server tick by a specified amount, wrapping to 11 bits.
+ public void IncrementServerTick(int increment) => _serverTick = (_serverTick + increment) & 0b11111111111;
+
+ /// Increments the server's absolute tick by a specified amount.
+ public void IncrementServerAbsoluteTick(int increment) => _absoluteServerTick += increment;
+
+ #endregion
+
+ /*** Useful Bitwise Functions ***/
+
+ #region Useful Bitwise Functions
+
+ ///
+ /// Combines a fiveBits and tick counter into a single value. This is used to optimize network traffic by packing two values into one.
+ ///
+ /// The fiveBits value (should be within 5 bits).
+ /// The tick counter value (should be within 11 bits).
+ /// A combined containing both the fiveBits and tick counter.
+ public static ushort CombineBitsTick(int fiveBits, int tick) {
+ // Ensure the fiveBits is within 5 bits and tickCounter within 11 bits
+ fiveBits &= 0x1F; // Mask to keep only the lowest 5 bits
+ tick &= 0x7FF; // Mask to keep only the lowest 11 bits
+ return (ushort)((fiveBits << 11) | tick); // Shift fiveBits left by 11 bits and combine with tickCounter
+ }
+
+ ///
+ /// Splits a combined fiveBits and tick counter value back into its individual components.
+ ///
+ /// The combined value.
+ /// A tuple containing the fiveBits and tick counter.
+ public static (int fiveBits, int tickCounter) SplitCombinedBitsTick(ushort combined) {
+ var fiveBits = (combined >> 11) & 0x1F; // Extract the 5-bit fiveBits by shifting right and masking
+ var tickCounter = combined & 0x7FF; // Extract the 11-bit tick counter by masking the lower 11 bits
+ return (fiveBits, tickCounter);
+ }
+
+ ///
+ /// Calculates the minimal difference between two ticks, accounting for wraparound (ex: SubtractTicks(2040, 2) => 10).
+ /// This helps in correctly comparing tick counts in a circular tick range.
+ ///
+ /// The first tick value.
+ /// The second tick value.
+ /// The minimal difference between the two ticks.
+ public static int SubtractTicks(int tickOne, int tickTwo) {
+ var delta = (tickOne - tickTwo + 2048) % 2048;
+ if (delta >= 1024) delta -= 2048;
+ return delta;
+ }
+
+ ///
+ /// Increments a tick value by a specified amount, wrapping around within a 2048 tick range.
+ /// This function ensures that tick values stay within a defined range by handling wraparound correctly.
+ ///
+ /// The initial tick value.
+ /// The amount to increment the tick by.
+ /// The incremented tick value, wrapped within the 2048 range.
+ public static int IncrementTick(int tick, int increment) {
+ return (tick + increment) & 0b11111111111;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta b/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta
new file mode 100644
index 000000000..402d11bc2
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e4b45d54032d4018af1450222e8e7eef
+timeCreated: 1730317658
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkTickManager.cs b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs
new file mode 100644
index 000000000..ddf53bda8
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs
@@ -0,0 +1,698 @@
+using System;
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace Mirror{
+ ///
+ /// Represents data sent from the client to track its current state.
+ ///
+ public struct ClientData{
+ public int ClientNonce;
+ public int ClientTick;
+ public PacketLossTracker RemoteClientLoss;
+ public int SentPackets;
+ }
+
+ ///
+ /// Represents a server response that includes synchronization data for the client tick and packet loss information.
+ ///
+ public struct ServerPong{
+ public ushort ServerTickWithNonce; // 5 bits for nonce + 11 bits for the tick
+ public ushort ClientTickWithLoss; // 5 bits for packet loss value + 11 bits for the tick
+ }
+
+ ///
+ /// Represents a server response with an absolute tick count to help the client synchronize with the server more accurately.
+ ///
+ public struct AbsoluteServerPong{
+ public ushort ServerTickWithNonce; // 5 bits for nonce + 11 bits for the tick
+ public ushort ClientTickWithLoss; // 5 bits for packet loss value + 11 bits for the tick
+ public int AbsoluteServerTick; // Absolute server tick count to sync the client
+ }
+
+ ///
+ /// Represents a client request sent to the server, including the client tick and a unique nonce for tracking.
+ ///
+ public struct ClientPing{
+ public ushort ClientTickWithNonce; // 5 bits for nonce + 11 bits for the tick
+ }
+
+ // Ensure we run first and that there is only one instance present.
+ [DefaultExecutionOrder(-10)]
+ [DisallowMultipleComponent]
+ [AddComponentMenu("Network/Network Tick Manager")]
+ public class NetworkTickManager : NetworkBehaviour{
+ /*** Public Definitions ***/
+
+ #region Public Definitions
+
+ [Header("Client Prediction Settings")]
+ [Min(1)]
+ [Tooltip("The minimum tick difference required between the client and server when the client's tick data arrives on the server." +
+ "If the difference is less than this value, the client must adjust to stay synchronized.")]
+ public int minClientRunaway = 1;
+
+ [Min(1)]
+ [Tooltip(
+ "The allowable tick difference range added on top of the minimum client runaway." +
+ "This defines how much further ahead the client can be from the server beyond the minimum before needing adjustment.")]
+ public int acceptableClientRunawayRange = 1;
+
+ [Header("Server State Replay Settings")]
+ [Min(1)]
+ [Tooltip(
+ "The minimum tick difference required between the server and client when the client replays server states." +
+ "If the difference is less than this value, the client must adjust the server replay tick.")]
+ public int minServerRunaway = 0;
+
+ [Min(1)]
+ [Tooltip("The allowable tick difference range added on top of the minimum server runaway." +
+ "Defines how much further ahead the received server state can be from the server replay beyond the minimum before needing adjustment.")]
+ public int acceptableServerRunawayRange = 1;
+
+ [Header("Deviation measurements and timings:")] [Tooltip("The amount of samples to use for packet loss calculation")] [Min(25)]
+ public int packetLossSamples = 100;
+
+ [Min(1)]
+ [Tooltip("The duration in seconds over which deviation data is collected before adjusting the client or server states to acceptable runaway values.")]
+ public int calculationSeconds = 2;
+
+ [Min(10)]
+ [Tooltip("The longer duration in seconds over which deviation data is collected before adjusting the client or server states to minimum runaway values.")]
+ public int longCalculationSeconds = 30;
+
+ [Tooltip("Tick compensation when packet loss is present: \ncompensation = loss percent / factor")] [Range(1, 30)]
+ public int packetLossCompensationFactor = 10;
+
+ [Header("Absolute tick sync settings:")]
+ [Min(1)]
+ [Tooltip("How often to verify absolute tick between the server and the client (this can happen if desync is larger than 1024 ticks)")]
+ public int absoluteTickSyncIntervalSeconds = 10;
+
+ [Min(10)] [Tooltip("How many ticks to send the absolute server tick for the clients to sync. Cant be 1 because this packet can get lost in transit.")]
+ public int absoluteTickSyncHandshakeTicks = 10;
+
+ [Header("Physics Controller:")] public NetworkPhysicsController physicsController;
+
+ #endregion
+
+ /*** Private Definitions ***/
+
+ #region Private Definitions
+
+ // Local clients list on the server for efficient communication
+ private readonly Dictionary _clients = new Dictionary();
+
+ // Instance of NetworkTick used for managing and changing the tick counters
+ private readonly NetworkTick _networkTick = new NetworkTick();
+
+ // Actual runaway metrics - adjusted based on network conditions ( aka packet losses )
+ private int _internalClientRunaway = 0;
+ private int _internalServerRunaway = 0;
+ private int _internalMinClientRunaway = 0;
+
+ private int _internalMinServerRunaway = 0;
+
+ // Running minimum counters used to avoid adjusting too fast and oscillating back and forth
+ private RunningMin _clientRunningMin;
+ private RunningMin _serverRunningMin;
+ private RunningMin _clientLongRunningMin;
+ private RunningMin _serverLongRunningMin;
+
+ // Client side packet loss tracker for packets from the server
+ private PacketLossTracker _receivePacketLoss;
+
+ // Server and Client last nonce values - used to dettect packet losses
+ private int _serverNonce = 0;
+ private int _clientNonce = 0;
+
+ // Client side last client tick received from the server to avoid lengthy adjustments
+ private int _lastRemoteClientTick = 0;
+
+ // Absolute tick synch status
+ private bool _isAbsoluteTickSynced = false;
+
+ // Modulus ( % modulus == 0 -> send absolute tick to clients ) based on absoluteTickSyncIntervalSeconds and tick rate
+ private int _absoluteServerTickModulus = 0;
+
+ // Adjustment flags and variables used to ensure only one adjustment is taking place at a time.
+ private bool _capturedOffsets = false;
+ private bool _isAdjusting = false;
+ private int _adjustmentEndTick = 0;
+
+ // Make sure NetworkPhysicsController is attached to the tick manager
+ protected new virtual void OnValidate() {
+ base.OnValidate();
+ physicsController = GetComponent();
+ if (physicsController == null) {
+ throw new ArgumentException("Missing NetworkPhysicsController! please attach it to this entity");
+ }
+ }
+
+ public static NetworkTickManager singleton;
+
+ void Awake() {
+ if (singleton != null && singleton != this) {
+ Destroy(gameObject);
+ return;
+ }
+ singleton = this;
+ }
+
+ #endregion
+
+ /*** Server Startup and Setup ***/
+
+ #region Server Startup and Setup
+
+ ///
+ /// Called when the server starts. Registers callbacks for client connection and disconnection events,
+ /// allowing the server to handle these events appropriately.
+ ///
+ [Server]
+ public override void OnStartServer() {
+ // Register callback for when clients connect/disconnect
+ NetworkServer.OnConnectedEvent += OnClientConnected;
+ NetworkServer.OnDisconnectedEvent += OnClientDisconnected;
+ base.OnStartServer();
+ }
+
+ ///
+ /// Called when the server stops. Unregisters callbacks for client connection and disconnection events
+ /// to ensure cleanup and avoid unintended event handling after the server has stopped.
+ ///
+ [Server]
+ public override void OnStopServer() {
+ // Unregister callbacks when server stops
+ NetworkServer.OnConnectedEvent -= OnClientConnected;
+ NetworkServer.OnDisconnectedEvent -= OnClientDisconnected;
+ base.OnStopServer();
+ }
+
+ ///
+ /// Called when a client connects to the server. Initializes a new entry in the _clients dictionary for the connected client,
+ /// storing initial client data such as nonce, tick count, packet loss tracker, and sent packets.
+ ///
+ /// The network connection for the connected client.
+ [Server]
+ private void OnClientConnected(NetworkConnection conn) => _clients[conn] = new ClientData()
+ { ClientNonce = 0, ClientTick = 0, RemoteClientLoss = new PacketLossTracker(packetLossSamples), SentPackets = 0 };
+
+ ///
+ /// Called when a client disconnects from the server. Removes the client entry from the _clients dictionary
+ /// to free up resources and maintain an accurate list of active clients.
+ ///
+ /// The network connection for the disconnected client.
+ [Server]
+ private void OnClientDisconnected(NetworkConnection conn) => _clients.Remove(conn);
+
+ #endregion
+
+ /*** Network Tick Start and Tick Initialization ***/
+
+ #region Network Tick Start and Tick Initialization
+
+ /// Initializes server-specific settings when the server starts.
+ [Server]
+ private void StartServer() {
+ _absoluteServerTickModulus = Mathf.RoundToInt(absoluteTickSyncIntervalSeconds / Time.fixedDeltaTime);
+ _networkTick.SetSynchronized(true);
+ _networkTick.SetSynchronizing(false);
+ }
+
+ /// Initializes client-specific settings when the client starts.
+ [Client]
+ private void StartClient() {
+ _internalMinClientRunaway = minClientRunaway;
+ _internalMinServerRunaway = minServerRunaway;
+ _internalClientRunaway = acceptableClientRunawayRange;
+ _internalServerRunaway = acceptableServerRunawayRange;
+ _clientRunningMin = new RunningMin(Mathf.RoundToInt(calculationSeconds / Time.fixedDeltaTime));
+ _serverRunningMin = new RunningMin(Mathf.RoundToInt(calculationSeconds / Time.fixedDeltaTime));
+ _clientLongRunningMin = new RunningMin(Mathf.RoundToInt(longCalculationSeconds / Time.fixedDeltaTime));
+ _serverLongRunningMin = new RunningMin(Mathf.RoundToInt(longCalculationSeconds / Time.fixedDeltaTime));
+ _receivePacketLoss = new PacketLossTracker(packetLossSamples);
+ }
+
+ /// Advances the client's tick counters by the specified number of ticks.
+ /// Number of ticks to advance.
+ [Client]
+ private void OnTickForwardClient(int deltaTicks) {
+ _networkTick.IncrementClientTick(deltaTicks);
+ _networkTick.IncrementClientAbsoluteTick(deltaTicks);
+ _networkTick.IncrementServerTick(deltaTicks);
+ _networkTick.IncrementServerAbsoluteTick(deltaTicks);
+ }
+
+ /// Advances the server's tick counters by the specified number of ticks.
+ /// Number of ticks to advance.
+ [Server]
+ private void OnTickForwardServer(int deltaTicks) {
+ _networkTick.IncrementServerTick(deltaTicks);
+ _networkTick.IncrementServerAbsoluteTick(deltaTicks);
+ }
+
+ /// Initializes the tick manager and sets up the physics controller based on whether it is running on the server or client.
+ private void Start() {
+ _networkTick.SetIsServer(isServer);
+ if (isServer)
+ StartServer();
+ else
+ StartClient();
+
+ // Allow the physics to position all the items in the scene - we run 1 tick for this then wait for sync
+ physicsController.TickForwardCallback = isServer ? OnTickForwardServer : OnTickForwardClient;
+ physicsController.RunSimulate(1);
+ }
+
+ #endregion
+
+ /*** Packet Loss Calculations ***/
+
+ #region Packet Loss Calculations
+
+ /// Updates the client's packet loss compensation values based on the server's nonce and reported packet loss.
+ /// The nonce received from the server to detect packet loss.
+ /// The packet loss percentage reported by the server.
+ [Client]
+ private void UpdatePacketLossCompensation(int serverNonce, int sendPacketLoss) {
+ _receivePacketLoss.AddPacket(_serverNonce > 0 && NextNonce(_serverNonce) != serverNonce);
+ _serverNonce = serverNonce;
+
+ // Calculate adjustments based on server and client packet loss factors
+ var sendCompensationTicks = CalculateTickCompensation(sendPacketLoss);
+ var receiveCompensationTicks = CalculateTickCompensation(_receivePacketLoss.Loss);
+
+ // Update NetworkTick with the compensation values for users to integrate compensations if needed
+ _networkTick.SetClientToServerPacketLossCompensation(sendCompensationTicks);
+ _networkTick.SetServerToClientPacketLossCompensation(receiveCompensationTicks);
+
+ // Adjust internal tick min and max runaway values to compensate for packet losses
+ _internalMinClientRunaway = minClientRunaway + sendCompensationTicks;
+ _internalClientRunaway = acceptableClientRunawayRange + _internalMinClientRunaway + sendCompensationTicks;
+ _internalMinServerRunaway = minServerRunaway + receiveCompensationTicks;
+ _internalServerRunaway = acceptableServerRunawayRange + _internalMinServerRunaway + receiveCompensationTicks;
+ }
+
+ #endregion
+
+ /*** Synchronization functions ***/
+
+ #region Synchronization functions
+
+ /// Sets or adjusts the client's absolute server tick values to maintain synchronization with the server.
+ /// The absolute tick count provided by the server.
+ /// The server's current tick value.
+ [Client]
+ private void SetAbsoluteTicks(int absoluteServerTick, int serverTick) {
+ if (!_isAbsoluteTickSynced) {
+ _isAbsoluteTickSynced = true;
+ _networkTick.SetServerTick(serverTick);
+ _networkTick.SetServerAbsoluteTick(absoluteServerTick);
+ return;
+ }
+
+ var proposedServerAbsoluteTick = absoluteServerTick - NetworkTick.SubtractTicks(serverTick, _networkTick.GetServerTick());
+ var absoluteTickDiff = proposedServerAbsoluteTick - _networkTick.GetServerAbsoluteTick();
+ if (absoluteTickDiff != 0) {
+ _networkTick.IncrementServerAbsoluteTick(absoluteTickDiff);
+ _networkTick.IncrementClientAbsoluteTick(absoluteTickDiff);
+ }
+ }
+
+ /// Initiates the synchronization process by aligning the client's and server's ticks.
+ /// The current tick value from the server.
+ /// The client's tick value as received by the server.
+ [Client]
+ private void SynchronizeStart(int serverTick, int clientTick) {
+ var oldServerTick = _networkTick.GetServerTick();
+ _networkTick.IncrementServerTick(-_internalServerRunaway);
+ var newServerTick = _networkTick.GetServerTick();
+ _networkTick.IncrementServerAbsoluteTick(NetworkTick.SubtractTicks(newServerTick, oldServerTick));
+ _networkTick.SetClientTick(NetworkTick.IncrementTick(serverTick,
+ NetworkTick.SubtractTicks(_networkTick.GetClientTick(), clientTick) + _internalClientRunaway));
+ _networkTick.SetClientAbsoluteTick(_networkTick.GetServerAbsoluteTick() + NetworkTick.SubtractTicks(_networkTick.GetClientTick(), newServerTick));
+ _networkTick.SetSynchronizing(true);
+ SetAdjusting(_networkTick.GetClientTick());
+ }
+
+ /// Synchronizes the client's and server's ticks by applying necessary adjustments.
+ [Client]
+ private void Synchronize() {
+ var serverTickAdjustment = GetServerAdjustment(true);
+ var clientTickAdjustment = GetServerAdjustment(true);
+ // Apply adjustments on current tick counters
+ _networkTick.IncrementClientTick(clientTickAdjustment);
+ _networkTick.IncrementClientAbsoluteTick(clientTickAdjustment);
+ _networkTick.IncrementServerTick(-serverTickAdjustment);
+ _networkTick.IncrementServerAbsoluteTick(-serverTickAdjustment);
+ // Set status to synchronized
+ _networkTick.SetSynchronized(true);
+ _networkTick.SetSynchronizing(false);
+
+ SetAdjusting(_networkTick.GetClientTick());
+ }
+
+ #endregion
+
+ /*** Handling Message from the Server ***/
+
+ #region Handling Message from the Server
+
+ ///
+ /// Handles the server's pong response to maintain and adjust tick synchronization between the client and server.
+ /// This method updates packet loss compensation, ensures packets are processed in order,
+ /// and manages the client's synchronization state based on the received ticks.
+ /// Depending on whether the client is synchronized, in the process of synchronizing, or not yet synchronized,
+ /// it calculates necessary offsets and adjusts ticks to align with the server.
+ ///
+ /// Nonce value from the server for packet loss detection.
+ /// Current tick count from the server.
+ /// Packet loss percentage reported by the server.
+ /// Client's tick count as received by the server.
+ [Client]
+ private void HandleServerPong(int serverNonce, int serverTick, int sendLoss, int clientTick) {
+ _capturedOffsets = false;
+ UpdatePacketLossCompensation(serverNonce, sendLoss);
+
+ // We want to avoid handling the same client tick from the server to improve accuracy otherwise we risk of repeating adjustments
+ if (!IsValidPacket(clientTick)) return;
+
+ if (_networkTick.GetIsSynchronized()) {
+ if (_isAdjusting && NetworkTick.SubtractTicks(clientTick, _adjustmentEndTick) > 0) {
+ _isAdjusting = false;
+ ResetRunningMins();
+ }
+
+ // Calculate and deviations using the server info
+ CalculateOffsets(serverTick, clientTick);
+ return;
+ }
+
+ if (_networkTick.GetIsSynchronizing()) {
+ // Since client tick is not yet synchronized and the server sends old tick we need to compare to server tick rather than client tick
+ if (NetworkTick.SubtractTicks(serverTick, _adjustmentEndTick) > 0) {
+ // We are not worried about being too much ahead at this point, we only care about being behind the execution on client or server
+ // So we calculate the minimum values and run the adjustment again
+ CalculateOffsets(serverTick, clientTick);
+ Synchronize();
+ ResetRunningMins();
+ }
+
+ return;
+ }
+
+ // Wait until we receive positive tick from server before starting the initial 2 step sync
+ if (clientTick > 0) {
+ SynchronizeStart(serverTick, clientTick);
+ ResetRunningMins();
+ }
+ }
+
+ #endregion
+
+ /*** Tick Adjustment Calculations ***/
+
+ #region Tick Adjustment Calculations
+
+ /// Calculates the tick offsets between client and server, updating running minimums for synchronization adjustments.
+ /// The current tick value from the server.
+ /// The client's tick value as received by the server.
+ [Client]
+ private void CalculateOffsets(int serverTick, int clientTick) {
+ _capturedOffsets = true;
+ var clientTickOffset = NetworkTick.SubtractTicks(clientTick, serverTick);
+ var serverTickOffset = NetworkTick.SubtractTicks(serverTick, _networkTick.GetServerTick());
+ _clientRunningMin.Add(clientTickOffset);
+ _clientLongRunningMin.Add(clientTickOffset);
+ _serverRunningMin.Add(serverTickOffset);
+ _serverLongRunningMin.Add(serverTickOffset);
+ }
+
+ /// Determines the necessary tick adjustment for the client to maintain synchronization with the server.
+ /// If true, applies the full adjustment needed; otherwise, applies a minimal step.
+ /// The number of ticks to adjust the client's tick by.
+ [Client]
+ private int GetClientAdjustment(bool absolute = false) {
+ // If the server received client predicted tick bellow min thresh hold we need to adjust ourselves forward otherwise risking server not receiving inputs
+ if (_clientRunningMin.CurrentMin < _internalMinClientRunaway)
+ return -(_clientRunningMin.CurrentMin - _internalMinClientRunaway);
+
+ // If the server received client predicted tick is too far into the future we want to slow down the client to reduce perceived latency
+ if (_clientRunningMin.IsFull && _clientRunningMin.CurrentMin > _internalClientRunaway)
+ return absolute ? -_clientRunningMin.CurrentMin : -1;
+
+ // If the server received client predicted tick is stable but above the min requirement we can slow down the client to reduce perceived latency
+ if (_clientLongRunningMin.IsFull && _clientLongRunningMin.CurrentMin > _internalMinClientRunaway)
+ return absolute ? -_clientLongRunningMin.CurrentMin : -1;
+ return 0;
+ }
+
+ /// Determines the necessary tick adjustment for the server to maintain synchronization with the client.
+ /// If true, applies the full adjustment needed; otherwise, applies a minimal step.
+ /// The number of ticks to adjust the server's tick by.
+ [Client]
+ private int GetServerAdjustment(bool absolute = false) {
+ // If the received server tick is behind the expected minimum we need to adjust our tick backwards
+ if (_serverRunningMin.CurrentMin < _internalMinServerRunaway)
+ return _internalMinServerRunaway - _serverRunningMin.CurrentMin;
+
+ // If the received server tick is too far forward we need to reduce it to reduce latency
+ if (_serverRunningMin.IsFull && _serverRunningMin.CurrentMin > _internalServerRunaway)
+ return absolute ? -_serverRunningMin.CurrentMin : -1;
+
+ // If the received server tick is more than the minimum for an extended period of time its safe to reduce it to reduce latency
+ if (_serverLongRunningMin.IsFull && _serverLongRunningMin.CurrentMin > _internalMinServerRunaway)
+ return absolute ? -_serverLongRunningMin.CurrentMin : -1;
+ return 0;
+ }
+
+ /// Calculates adjusted tick values for synchronization, applying any necessary client or server tick adjustments.
+ /// The base number of ticks to advance.
+ /// The adjusted number of ticks to use for simulation.
+ [Client]
+ private int GetAdjustedTicks(int deltaTicks) {
+ int clientAdjustment = GetClientAdjustment();
+ int serverAdjustment = GetServerAdjustment();
+ if (serverAdjustment != 0) {
+ _networkTick.IncrementServerTick(-serverAdjustment);
+ _networkTick.IncrementServerAbsoluteTick(-serverAdjustment);
+ }
+
+ // If client or server are adjusting we need to wait for confirmation to avoid oscillating adjusments
+ if (clientAdjustment != 0 || serverAdjustment != 0)
+ SetAdjusting(NetworkTick.IncrementTick(_networkTick.GetClientTick(), deltaTicks + clientAdjustment));
+
+ return deltaTicks + clientAdjustment;
+ }
+
+ #endregion
+
+ /*** Tick Simulation Functions ***/
+
+ #region Tick Simulation Functions
+
+ /// Checks for any required reconciliation due to state discrepancies and resimulates physics accordingly.
+ /// The number of ticks advanced since the last update.
+ [Client]
+ private void CheckReconcile(int deltaTicks) {
+ var reconcileStartTick = physicsController.GetReconcileStartTick();
+ if (reconcileStartTick > 0) {
+ var reconcileTicks = _networkTick.GetClientTick() - reconcileStartTick + deltaTicks;
+ OnTickForwardClient(-reconcileTicks);
+ _networkTick.SetReconciling(true);
+ physicsController.RunSimulate(reconcileTicks);
+ physicsController.ResetReconcile();
+ _networkTick.SetReconciling(false);
+ }
+ }
+
+ /// Updates the client's state each tick, handling synchronization, reconciliation, and physics simulation.
+ /// The number of ticks to advance.
+ [Client]
+ private void UpdateClient(int deltaTicks) {
+ // Check if need reconciling - if yes reconcile before executing the next ticks
+ CheckReconcile(deltaTicks);
+
+ // Adjust the delta ticks if not waiting for adjustment confirmation
+ var adjustedTicks = _capturedOffsets && !_isAdjusting ? GetAdjustedTicks(deltaTicks) : deltaTicks;
+
+ // fix discrepancies cause by client tick adjustment
+ _networkTick.IncrementServerTick(deltaTicks - adjustedTicks);
+ _networkTick.IncrementServerAbsoluteTick(deltaTicks - adjustedTicks);
+
+ // Simulate ticks or skip if pause was requested
+ if (adjustedTicks > 0)
+ physicsController.RunSimulate(adjustedTicks);
+ }
+
+ /// Handles physics simulation and synchronization updates on both the server and client each fixed frame.
+ public void FixedUpdate() {
+ // Handle FixedUpdate for deltaTicks
+ if (isServer) {
+ physicsController.RunSimulate(1);
+ SendUpdatesToAllClients();
+ }
+ else {
+ // Keep pushing the tick counters forward until the client is synced with the server
+ if (!_networkTick.GetIsSynchronized())
+ OnTickForwardClient(1);
+ else
+ UpdateClient(1);
+ ClientSendPing();
+ }
+ }
+
+ #endregion
+
+ /*** Communication Functions ***/
+
+ #region Communication Functions
+
+ /// Sends a ping to the server with the client's current tick and a nonce for packet loss detection.
+ [Client]
+ private void ClientSendPing() {
+ // Increase nonce by 1 but keep withing 5 bits of data [0-31]
+ _clientNonce = NextNonce(_clientNonce);
+ CmdPingServer(new ClientPing() { ClientTickWithNonce = NetworkTick.CombineBitsTick(_clientNonce, _networkTick.GetClientTick()) });
+ }
+
+ /// Sends synchronization updates to all connected clients, including tick counts and packet loss information.
+ [Server]
+ private void SendUpdatesToAllClients() {
+ // Increase nonce by 1 but keep withing 5 bits of data [0-31]
+ _serverNonce = NextNonce(_serverNonce);
+ var absoluteServerTick = _networkTick.GetServerAbsoluteTick();
+ var isSendAbsolute = absoluteServerTick % _absoluteServerTickModulus == 0;
+ var serverTickWithNonce = NetworkTick.CombineBitsTick(_serverNonce, _networkTick.GetServerTick());
+ foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) {
+ // If connection is on the same machine as the server we skip it.
+ if (conn == NetworkServer.localConnection) continue;
+
+ if (_clients.TryGetValue(conn, out ClientData clientData)) {
+ // 0-30 % are reported regularly but 31 or higher are aggregated as just 31 ( mor ethan 30% packet loss is extreme! )
+ int compressedLoss = Math.Min(31, (int)Math.Ceiling(clientData.RemoteClientLoss.Loss));
+
+ if (isSendAbsolute || clientData.SentPackets < absoluteTickSyncHandshakeTicks)
+ // If requested by interval or during hand shake send absolute tick alongside tick information
+ RpcAbsoluteServerPong(conn, new AbsoluteServerPong() {
+ AbsoluteServerTick = absoluteServerTick,
+ ServerTickWithNonce = serverTickWithNonce,
+ ClientTickWithLoss = NetworkTick.CombineBitsTick(
+ compressedLoss,
+ clientData.ClientTick)
+ });
+ else
+ // Send tick information with nonce and loss
+ RpcServerPong(conn, new ServerPong() {
+ ServerTickWithNonce = serverTickWithNonce,
+ ClientTickWithLoss = NetworkTick.CombineBitsTick(
+ compressedLoss,
+ clientData.ClientTick)
+ });
+
+ // Count how many packets were sent
+ clientData.SentPackets += 1;
+ _clients[conn] = clientData;
+ }
+ }
+ }
+
+ #endregion
+
+ /*** Target RPC and Command callbacks ***/
+
+ #region Target RPC and Command callbacks
+
+ /// Handles the server's response containing absolute tick synchronization data.
+ /// The client connection receiving the response.
+ /// The server's pong message with tick and nonce data.
+ [TargetRpc(channel = Channels.Unreliable)]
+ private void RpcAbsoluteServerPong(NetworkConnectionToClient target, AbsoluteServerPong serverPong) {
+ var (serverNonce, serverTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ServerTickWithNonce);
+ var (sendLoss, clientTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ClientTickWithLoss);
+ SetAbsoluteTicks(serverPong.AbsoluteServerTick, serverTick);
+ HandleServerPong(serverNonce, serverTick, sendLoss, clientTick);
+ }
+
+ /// Handles the server's standard response containing synchronization data.
+ /// The client connection receiving the response.
+ /// The server's pong message with tick and nonce data.
+ [TargetRpc(channel = Channels.Unreliable)]
+ private void RpcServerPong(NetworkConnectionToClient target, ServerPong serverPong) {
+ var (serverNonce, serverTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ServerTickWithNonce);
+ var (sendLoss, clientTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ClientTickWithLoss);
+ HandleServerPong(serverNonce, serverTick, sendLoss, clientTick);
+ }
+
+ /// Receives ping messages from clients and updates their data on the server.
+ /// The ping message containing the client's tick and nonce.
+ /// The connection to the client sending the ping.
+ [Command(requiresAuthority = false, channel = Channels.Unreliable)]
+ private void CmdPingServer(ClientPing clientPing, NetworkConnectionToClient connectionToClient = null) {
+ if (connectionToClient == null) return;
+ var (nonce, clientTick) = NetworkTick.SplitCombinedBitsTick(clientPing.ClientTickWithNonce);
+ _clients[connectionToClient].RemoteClientLoss.AddPacket(NextNonce(_clients[connectionToClient].ClientNonce) != nonce);
+ _clients[connectionToClient] = new ClientData() {
+ ClientTick = clientTick,
+ ClientNonce = nonce,
+ RemoteClientLoss = _clients[connectionToClient].RemoteClientLoss,
+ SentPackets = _clients[connectionToClient].SentPackets,
+ };
+ }
+
+ #endregion
+
+ /*** Helper Functions ***/
+
+ #region Helper Functions
+
+ /// Validates whether the incoming packet is newer than the last processed one to prevent out-of-order processing.
+ /// The tick value from the incoming packet.
+ /// True if the packet is valid and should be processed; otherwise, false.
+ [Client]
+ private bool IsValidPacket(int clientTick) {
+ var isValid = NetworkTick.SubtractTicks(clientTick, _lastRemoteClientTick) > 0;
+ _lastRemoteClientTick = clientTick;
+ return isValid;
+ }
+
+ /// Marks the start of an adjustment period, during which tick synchronization adjustments are applied.
+ /// The client tick value when adjustments should stop.
+ [Client]
+ private void SetAdjusting(int adjustmentEndTick) {
+ _capturedOffsets = false;
+ _isAdjusting = true;
+ _adjustmentEndTick = adjustmentEndTick;
+ }
+
+ /// Resets the running minimums used for calculating tick adjustments, clearing any accumulated data.
+ [Client]
+ private void ResetRunningMins() {
+ _capturedOffsets = false;
+ _clientRunningMin.Reset();
+ _clientLongRunningMin.Reset();
+ _serverRunningMin.Reset();
+ _serverLongRunningMin.Reset();
+ }
+
+ ///
+ /// Generates the next nonce value by incrementing the current nonce.
+ /// The nonce is a looping 5-bit variable (0-31), ensuring it wraps around correctly when reaching 31.
+ ///
+ /// The starting nonce value to increment.
+ /// The next nonce value, wrapped to stay within the 5-bit range.
+ private static int NextNonce(int startNonce) => (startNonce + 1) & 0b11111;
+
+ ///
+ /// Calculates the tick compensation based on packet loss and a predefined compensation factor.
+ /// This is used to adjust for lost packets, smoothing gameplay experience based on the
+ /// `packetLossCompensationFactor`.
+ ///
+ /// The packet loss percentage used to calculate compensation ticks.
+ /// The number of ticks to compensate based on the provided packet loss.
+ private int CalculateTickCompensation(float loss) => Mathf.FloorToInt((loss + packetLossCompensationFactor - 0.01f) / packetLossCompensationFactor);
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta
new file mode 100644
index 000000000..953b434a8
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: eb2e49dca4a74e7baa4b2fc290e20730
+timeCreated: 1730317270
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/PacketLossTracker.cs b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs
new file mode 100644
index 000000000..65f20be36
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+
+namespace Mirror{
+ ///
+ /// A class for tracking packet loss over a sliding window of packets.
+ /// It calculates the packet loss rate as a fraction based on a fixed sample size,
+ /// and provides both a floating-point representation and a byte-scaled value for the loss.
+ ///
+ public class PacketLossTracker{
+ // A queue to store recent packet loss values (1 for loss, 0 for received)
+ private Queue _packetLossWindow;
+
+ // The current packet loss rate as a float (between 0 and 1)
+ private float _loss = 0;
+
+ // Sum of packet loss values in the current window
+ private int _packetLossSum = 0;
+
+ // Number of packets in the sample window for calculating loss
+ private readonly int _samplePackets;
+
+ /// Initializes a new instance of the PacketLossTracker class with a specified sample size.
+ /// The number of packets to track for calculating the loss rate.
+ /// Thrown when samplePackets is zero or negative.
+ public PacketLossTracker(int samplePackets) {
+ _samplePackets = samplePackets > 0 ? samplePackets : throw new ArgumentException("Sample packets must be greater than zero.");
+ _packetLossWindow = new Queue(_samplePackets);
+ }
+
+ ///
+ /// Adds a packet result to the tracker, indicating whether the packet was lost or received.
+ /// Updates the loss rate based on the latest window of packet results.
+ ///
+ /// True if the packet was lost; false if the packet was received.
+ public void AddPacket(bool isLost) {
+ int lossValue = isLost ? 1 : 0;
+ _packetLossWindow.Enqueue(lossValue);
+ _packetLossSum += lossValue;
+
+ // Remove the oldest packet when the queue exceeds the max size
+ if (_packetLossWindow.Count > _samplePackets)
+ _packetLossSum -= _packetLossWindow.Dequeue();
+
+ _loss = (float)_packetLossSum / _samplePackets;
+ }
+
+ /// Gets the current packet loss rate as a floating-point value between 0 and 100.
+ public float Loss => _loss * 100;
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta
new file mode 100644
index 000000000..e5fae0278
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 44fd6008a95941a1997ed153caf41f5e
+timeCreated: 1730577723
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/RunningMin.cs b/Assets/Mirror/Core/TickManager/RunningMin.cs
new file mode 100644
index 000000000..1df2c3933
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/RunningMin.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+
+namespace Mirror{
+ ///
+ /// A class that maintains a running minimum over a fixed-size sliding window of integers.
+ /// Provides efficient tracking of the minimum value as elements are added and removed from the window.
+ ///
+ public class RunningMin{
+ // The fixed size of the sliding window
+ private readonly int _windowSize;
+
+ // Queue to store the values in the sliding window
+ private readonly Queue _values;
+
+ // Stores the current minimum value in the window
+ private int _currentMin;
+
+ /// Gets the current minimum value in the sliding window.
+ public int CurrentMin => _currentMin;
+
+ /// Gets the current count of elements in the sliding window.
+ public int Count => _values.Count;
+
+ /// Checks if the sliding window is full.
+ public bool IsFull => _values.Count == _windowSize;
+
+ /// Returns last added value.
+ public int Last => _values.ToArray()[_values.Count - 1];
+
+ /// Initializes a new instance of the class with a specified window size.
+ /// The maximum number of elements in the sliding window.
+ public RunningMin(int windowSize = 100) {
+ _windowSize = windowSize > 0 ? windowSize : throw new ArgumentException("Sample packets must be greater than zero.");
+ _values = new Queue(windowSize);
+ _currentMin = int.MaxValue;
+ }
+
+ /// Resets the values and current minimum.
+ public void Reset() {
+ _currentMin = int.MaxValue;
+ _values.Clear();
+ }
+
+ /// Recalculates the current minimum by iterating through the queue. Only called when necessary to avoid performance overhead.
+ private void UpdateCurrentMin() {
+ _currentMin = int.MaxValue;
+ foreach (int value in _values)
+ if (value < _currentMin)
+ _currentMin = value;
+ }
+
+ /// Adds a value to the sliding window. Updates the current minimum as needed.
+ /// The new value to add to the window.
+ public void Add(int value) {
+ _values.Enqueue(value);
+ if (value < _currentMin)
+ _currentMin = value;
+ // Check if exceeding the window size, if so then remove oldest item
+ if (_values.Count > _windowSize) {
+ int removedValue = _values.Dequeue();
+ // Check oldest value is equal to minimum and is not equal to the new value we need to calculate the current minimum
+ if (removedValue == _currentMin && removedValue != value)
+ UpdateCurrentMin();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Core/TickManager/RunningMin.cs.meta b/Assets/Mirror/Core/TickManager/RunningMin.cs.meta
new file mode 100644
index 000000000..3d86e20d5
--- /dev/null
+++ b/Assets/Mirror/Core/TickManager/RunningMin.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9671059f9b45471abcf30dd42fa2018e
+timeCreated: 1730577873
\ No newline at end of file