377 lines
13 KiB
C#
377 lines
13 KiB
C#
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.
|
|
/// <summary>
|
|
/// A message affected by latency.
|
|
/// </summary>
|
|
private struct Message
|
|
{
|
|
public readonly int ConnectionId;
|
|
public readonly byte[] Data;
|
|
public readonly int Length;
|
|
public readonly float SendTime;
|
|
|
|
public Message(int connectionId, ArraySegment<byte> 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<byte> GetSegment()
|
|
{
|
|
return new ArraySegment<byte>(Data, 0, Length);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Internal.
|
|
/// <summary>
|
|
/// True if latency can be simulated.
|
|
/// </summary>
|
|
internal bool CanSimulate => (GetEnabled() && (GetLatency() > 0 || GetPacketLost() > 0 || GetOutOfOrder() > 0));
|
|
#endregion
|
|
|
|
#region Serialized
|
|
[Header("Settings")]
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
[Tooltip("True if latency simulator is enabled.")]
|
|
[SerializeField]
|
|
private bool _enabled;
|
|
/// <summary>
|
|
/// Gets the enabled value of simulator.
|
|
/// </summary>
|
|
public bool GetEnabled() => _enabled;
|
|
/// <summary>
|
|
/// Sets the enabled value of simulator.
|
|
/// </summary>
|
|
/// <param name="value">New value.</param>
|
|
public void SetEnabled(bool value)
|
|
{
|
|
if (value == _enabled)
|
|
return;
|
|
|
|
_enabled = value;
|
|
Reset();
|
|
}
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
[Tooltip("True to add latency on clientHost as well.")]
|
|
[SerializeField]
|
|
private bool _simulateHost = true;
|
|
/// <summary>
|
|
/// Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.
|
|
/// </summary>
|
|
[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;
|
|
/// <summary>
|
|
/// Gets the latency value.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public long GetLatency() => _latency;
|
|
/// <summary>
|
|
/// Sets a new latency value.
|
|
/// </summary>
|
|
/// <param name="value">Latency as milliseconds.</param>
|
|
public void SetLatency(long value) => _latency = value;
|
|
|
|
[Header("Unreliable")]
|
|
/// <summary>
|
|
/// Percentage of unreliable packets which should arrive out of order.
|
|
/// </summary>
|
|
[Tooltip("Percentage of unreliable packets which should arrive out of order.")]
|
|
[Range(0f, 1f)]
|
|
[SerializeField]
|
|
private double _outOfOrder = 0;
|
|
/// <summary>
|
|
/// Out of order chance, 1f is a 100% chance to occur.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public double GetOutOfOrder() => _outOfOrder;
|
|
/// <summary>
|
|
/// Sets out of order chance. 1f is a 100% chance to occur.
|
|
/// </summary>
|
|
/// <param name="value">New Value.</param>
|
|
public void SetOutOfOrder(double value) => _outOfOrder = value;
|
|
/// <summary>
|
|
/// Percentage of packets which should drop.
|
|
/// </summary>
|
|
[Tooltip("Percentage of packets which should drop.")]
|
|
[Range(0, 1)]
|
|
[SerializeField]
|
|
private double _packetLoss = 0;
|
|
/// <summary>
|
|
/// Gets packet loss chance. 1f is a 100% chance to occur.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public double GetPacketLost() => _packetLoss;
|
|
/// <summary>
|
|
/// Sets packet loss chance. 1f is a 100% chance to occur.
|
|
/// </summary>
|
|
/// <param name="value">New Value.</param>
|
|
public void SetPacketLoss(double value) => _packetLoss = value;
|
|
#endregion
|
|
|
|
#region Private
|
|
/// <summary>
|
|
/// Transport to send data on.
|
|
/// </summary>
|
|
private Transport _transport;
|
|
/// <summary>
|
|
/// Reliable messages to the server.
|
|
/// </summary>
|
|
private List<Message> _toServerReliable = new List<Message>();
|
|
/// <summary>
|
|
/// Unreliable messages to the server.
|
|
/// </summary>
|
|
private List<Message> _toServerUnreliable = new List<Message>();
|
|
/// <summary>
|
|
/// Reliable messages to clients.
|
|
/// </summary>
|
|
private List<Message> _toClientReliable = new List<Message>();
|
|
/// <summary>
|
|
/// Unreliable messages to clients.
|
|
/// </summary>
|
|
private List<Message> _toClientUnreliable = new List<Message>();
|
|
/// <summary>
|
|
/// NetworkManager for this instance.
|
|
/// </summary>
|
|
private NetworkManager _networkManager;
|
|
/// <summary>
|
|
/// Used to generate chances of latency.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Stops both client and server.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
bool enabled = GetEnabled();
|
|
if (_transport != null && enabled)
|
|
{
|
|
IterateAndStore(_toServerReliable);
|
|
IterateAndStore(_toServerUnreliable);
|
|
IterateAndStore(_toClientReliable);
|
|
IterateAndStore(_toClientUnreliable);
|
|
}
|
|
|
|
void IterateAndStore(List<Message> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes pending or held packets for a connection.
|
|
/// </summary>
|
|
/// <param name="conn">Connection to remove pending packets for.</param>
|
|
public void RemovePendingForConnection(int connectionId)
|
|
{
|
|
RemoveFromCollection(_toServerUnreliable);
|
|
RemoveFromCollection(_toServerUnreliable);
|
|
RemoveFromCollection(_toClientReliable);
|
|
RemoveFromCollection(_toClientUnreliable);
|
|
|
|
void RemoveFromCollection(List<Message> c)
|
|
{
|
|
for (int i = 0; i < c.Count; i++)
|
|
{
|
|
if (c[i].ConnectionId == connectionId)
|
|
{
|
|
c.RemoveAt(i);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Simulation
|
|
/// <summary>
|
|
/// Returns long latency as a float.
|
|
/// </summary>
|
|
/// <param name="ms"></param>
|
|
/// <returns></returns>
|
|
private float GetLatencyAsFloat()
|
|
{
|
|
return (float)(_latency / 1000f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a packet for simulation.
|
|
/// </summary>
|
|
public void AddOutgoing(byte channelId, ArraySegment<byte> 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<Message> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulates pending outgoing packets.
|
|
/// </summary>
|
|
/// <param name="toServer">True if sending to the server.</param>
|
|
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<Message> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if a packet should drop.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private bool DropPacket()
|
|
{
|
|
return (_packetLoss > 0d && (_random.NextDouble() < _packetLoss));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if a packet should be out of order.
|
|
/// </summary>
|
|
/// <param name="c"></param>
|
|
/// <returns></returns>
|
|
private bool OutOfOrderPacket(Channel c)
|
|
{
|
|
if (c == Channel.Reliable)
|
|
return false;
|
|
|
|
return (_outOfOrder > 0d && (_random.NextDouble() < _outOfOrder));
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
|