using FishNet.Connection; using FishNet.Transporting; using FishNet.Utility.Performance; using System; using System.Collections.Generic; using UnityEngine; //Thanks to TiToMoskito originally creating this as a Transport. //https://github.com/TiToMoskito/FishyLatency namespace FishNet.Managing.Transporting { [System.Serializable] public class LatencySimulator { #region Types. /// /// A message affected by latency. /// private struct Message { public readonly int ConnectionId; public readonly byte[] Data; public readonly int Length; public readonly float SendTime; public Message(int connectionId, ArraySegment segment, float latency) { this.ConnectionId = connectionId; this.SendTime = (Time.unscaledTime + latency); this.Length = segment.Count; this.Data = ByteArrayPool.Retrieve(this.Length); Buffer.BlockCopy(segment.Array, segment.Offset, this.Data, 0, this.Length); } public ArraySegment GetSegment() { return new ArraySegment(Data, 0, Length); } } #endregion #region Internal. /// /// True if latency can be simulated. /// internal bool CanSimulate => (GetEnabled() && (GetLatency() > 0 || GetPacketLost() > 0 || GetOutOfOrder() > 0)); #endregion #region Serialized [Header("Settings")] /// /// /// [Tooltip("True if latency simulator is enabled.")] [SerializeField] private bool _enabled; /// /// Gets the enabled value of simulator. /// public bool GetEnabled() => _enabled; /// /// Sets the enabled value of simulator. /// /// New value. public void SetEnabled(bool value) { if (value == _enabled) return; _enabled = value; Reset(); } /// /// /// [Tooltip("True to add latency on clientHost as well.")] [SerializeField] private bool _simulateHost = true; /// /// Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate. /// [Tooltip("Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.")] [Range(0, 60000)] [SerializeField] private long _latency = 0; /// /// Gets the latency value. /// /// public long GetLatency() => _latency; /// /// Sets a new latency value. /// /// Latency as milliseconds. public void SetLatency(long value) => _latency = value; [Header("Unreliable")] /// /// Percentage of unreliable packets which should arrive out of order. /// [Tooltip("Percentage of unreliable packets which should arrive out of order.")] [Range(0f, 1f)] [SerializeField] private double _outOfOrder = 0; /// /// Out of order chance, 1f is a 100% chance to occur. /// /// public double GetOutOfOrder() => _outOfOrder; /// /// Sets out of order chance. 1f is a 100% chance to occur. /// /// New Value. public void SetOutOfOrder(double value) => _outOfOrder = value; /// /// Percentage of packets which should drop. /// [Tooltip("Percentage of packets which should drop.")] [Range(0, 1)] [SerializeField] private double _packetLoss = 0; /// /// Gets packet loss chance. 1f is a 100% chance to occur. /// /// public double GetPacketLost() => _packetLoss; /// /// Sets packet loss chance. 1f is a 100% chance to occur. /// /// New Value. public void SetPacketLoss(double value) => _packetLoss = value; #endregion #region Private /// /// Transport to send data on. /// private Transport _transport; /// /// Reliable messages to the server. /// private List _toServerReliable = new List(); /// /// Unreliable messages to the server. /// private List _toServerUnreliable = new List(); /// /// Reliable messages to clients. /// private List _toClientReliable = new List(); /// /// Unreliable messages to clients. /// private List _toClientUnreliable = new List(); /// /// NetworkManager for this instance. /// private NetworkManager _networkManager; /// /// Used to generate chances of latency. /// private readonly System.Random _random = new System.Random(); #endregion #region Initialization and Unity public void Initialize(NetworkManager manager, Transport transport) { _networkManager = manager; _transport = transport; } #endregion /// /// Stops both client and server. /// public void Reset() { bool enabled = GetEnabled(); if (_transport != null && enabled) { IterateAndStore(_toServerReliable); IterateAndStore(_toServerUnreliable); IterateAndStore(_toClientReliable); IterateAndStore(_toClientUnreliable); } void IterateAndStore(List messages) { foreach (Message m in messages) { _transport.SendToServer((byte)Channel.Reliable, m.GetSegment()); ByteArrayPool.Store(m.Data); } } _toServerReliable.Clear(); _toServerUnreliable.Clear(); _toClientReliable.Clear(); _toClientUnreliable.Clear(); } /// /// Removes pending or held packets for a connection. /// /// Connection to remove pending packets for. public void RemovePendingForConnection(int connectionId) { RemoveFromCollection(_toServerUnreliable); RemoveFromCollection(_toServerUnreliable); RemoveFromCollection(_toClientReliable); RemoveFromCollection(_toClientUnreliable); void RemoveFromCollection(List c) { for (int i = 0; i < c.Count; i++) { if (c[i].ConnectionId == connectionId) { c.RemoveAt(i); i--; } } } } #region Simulation /// /// Returns long latency as a float. /// /// /// private float GetLatencyAsFloat() { return (float)(_latency / 1000f); } /// /// Adds a packet for simulation. /// public void AddOutgoing(byte channelId, ArraySegment segment, bool toServer = true, int connectionId = -1) { /* If to not simulate for host see if this packet * should be sent normally. */ if (!_simulateHost && _networkManager != null && _networkManager.IsHost) { /* If going to the server and is host then * it must be sent from clientHost. */ if (toServer) { _transport.SendToServer(channelId, segment); return; } //Not to server, see if going to clientHost. else { //If connId is the same as clientHost id. if (_networkManager.ClientManager.Connection.ClientId == connectionId) { _transport.SendToClient(channelId, segment, connectionId); return; } } } List collection; Channel c = (Channel)channelId; if (toServer) collection = (c == Channel.Reliable) ? _toServerReliable : _toServerUnreliable; else collection = (c == Channel.Reliable) ? _toClientReliable : _toClientUnreliable; float latency = GetLatencyAsFloat(); //If dropping check to add extra latency if reliable, or discard if not. if (DropPacket()) { if (c == Channel.Reliable) { latency += (latency * 0.3f); //add extra for resend. } //If not reliable then return the segment array to pool. else { return; } } Message msg = new Message(connectionId, segment, latency); int count = collection.Count; if (c == Channel.Unreliable && count > 0 && OutOfOrderPacket(c)) collection.Insert(count - 1, msg); else collection.Add(msg); } /// /// Simulates pending outgoing packets. /// /// True if sending to the server. public void IterateOutgoing(bool toServer) { if (_transport == null) { Reset(); return; } if (toServer) { IterateCollection(_toServerReliable, Channel.Reliable); IterateCollection(_toServerUnreliable, Channel.Unreliable); } else { IterateCollection(_toClientReliable, Channel.Reliable); IterateCollection(_toClientUnreliable, Channel.Unreliable); } void IterateCollection(List collection, Channel channel) { byte cByte = (byte)channel; float unscaledTime = Time.unscaledTime; int count = collection.Count; int iterations = 0; for (int i = 0; i < count; i++) { Message msg = collection[i]; //Not enough time has passed. if (unscaledTime < msg.SendTime) break; if (toServer) _transport.SendToServer(cByte, msg.GetSegment()); else _transport.SendToClient(cByte, msg.GetSegment(), msg.ConnectionId); iterations++; } if (iterations > 0) { for (int i = 0; i < iterations; i++) ByteArrayPool.Store(collection[i].Data); collection.RemoveRange(0, iterations); } } _transport.IterateOutgoing(toServer); } /// /// Returns if a packet should drop. /// /// private bool DropPacket() { return (_packetLoss > 0d && (_random.NextDouble() < _packetLoss)); } /// /// Returns if a packet should be out of order. /// /// /// private bool OutOfOrderPacket(Channel c) { if (c == Channel.Reliable) return false; return (_outOfOrder > 0d && (_random.NextDouble() < _outOfOrder)); } #endregion } }