save
This commit is contained in:
@@ -1,418 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>A client that can connect to a <see cref="Server"/>.</summary>
|
||||
public class Client : Peer
|
||||
{
|
||||
/// <summary>Invoked when a connection to the server is established.</summary>
|
||||
public event EventHandler Connected;
|
||||
/// <summary>Invoked when a connection to the server fails to be established.</summary>
|
||||
public event EventHandler<ConnectionFailedEventArgs> ConnectionFailed;
|
||||
/// <summary>Invoked when a message is received.</summary>
|
||||
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
||||
/// <summary>Invoked when disconnected from the server.</summary>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
/// <summary>Invoked when another <i>non-local</i> client connects.</summary>
|
||||
public event EventHandler<ClientConnectedEventArgs> ClientConnected;
|
||||
/// <summary>Invoked when another <i>non-local</i> client disconnects.</summary>
|
||||
public event EventHandler<ClientDisconnectedEventArgs> ClientDisconnected;
|
||||
|
||||
/// <summary>The client's numeric ID.</summary>
|
||||
public ushort Id => connection.Id;
|
||||
/// <inheritdoc cref="Connection.RTT"/>
|
||||
public short RTT => connection.RTT;
|
||||
/// <inheritdoc cref="Connection.SmoothRTT"/>
|
||||
/// <remarks>This value is slower to accurately represent lasting changes in latency than <see cref="RTT"/>, but it is less susceptible to changing drastically due to significant—but temporary—jumps in latency.</remarks>
|
||||
public short SmoothRTT => connection.SmoothRTT;
|
||||
/// <summary>Sets the client's <see cref="Connection.TimeoutTime"/>.</summary>
|
||||
public override int TimeoutTime
|
||||
{
|
||||
set
|
||||
{
|
||||
defaultTimeout = value;
|
||||
connection.TimeoutTime = defaultTimeout;
|
||||
}
|
||||
}
|
||||
/// <summary>Whether or not the client is currently <i>not</i> trying to connect, pending, nor actively connected.</summary>
|
||||
public bool IsNotConnected => connection is null || connection.IsNotConnected;
|
||||
/// <summary>Whether or not the client is currently in the process of connecting.</summary>
|
||||
public bool IsConnecting => !(connection is null) && connection.IsConnecting;
|
||||
/// <summary>Whether or not the client's connection is currently pending (waiting to be accepted/rejected by the server).</summary>
|
||||
public bool IsPending => !(connection is null) && connection.IsPending;
|
||||
/// <summary>Whether or not the client is currently connected.</summary>
|
||||
public bool IsConnected => !(connection is null) && connection.IsConnected;
|
||||
/// <summary>The client's connection to a server.</summary>
|
||||
// Not an auto property because properties can't be passed as ref/out parameters. Could
|
||||
// use a local variable in the Connect method, but that's arguably not any cleaner. This
|
||||
// property will also probably only be used rarely from outside the class/library.
|
||||
public Connection Connection => connection;
|
||||
/// <summary>Encapsulates a method that handles a message from a server.</summary>
|
||||
/// <param name="message">The message that was received.</param>
|
||||
public delegate void MessageHandler(Message message);
|
||||
|
||||
/// <inheritdoc cref="Connection"/>
|
||||
private Connection connection;
|
||||
/// <summary>How many connection attempts have been made so far.</summary>
|
||||
private int connectionAttempts;
|
||||
/// <summary>How many connection attempts to make before giving up.</summary>
|
||||
private int maxConnectionAttempts;
|
||||
/// <inheritdoc cref="Server.messageHandlers"/>
|
||||
private Dictionary<ushort, MessageHandler> messageHandlers;
|
||||
/// <summary>The underlying transport's client that is used for sending and receiving data.</summary>
|
||||
private IClient transport;
|
||||
/// <summary>The message sent when connecting. May include custom data.</summary>
|
||||
private Message connectMessage;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
/// <param name="transport">The transport to use for sending and receiving data.</param>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Client(IClient transport, string logName = "CLIENT") : base(logName)
|
||||
{
|
||||
this.transport = transport;
|
||||
}
|
||||
/// <summary>Handles initial setup using the built-in UDP transport.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Client(string logName = "CLIENT") : this(new Transports.Udp.UdpClient(), logName) { }
|
||||
|
||||
/// <summary>Disconnects the client if it's connected and swaps out the transport it's using.</summary>
|
||||
/// <param name="newTransport">The new transport to use for sending and receiving data.</param>
|
||||
/// <remarks>This method does not automatically reconnect to the server. To continue communicating with the server, <see cref="Connect(string, int, byte, Message, bool)"/> must be called again.</remarks>
|
||||
public void ChangeTransport(IClient newTransport)
|
||||
{
|
||||
Disconnect();
|
||||
transport = newTransport;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to connect to a server at the given host address.</summary>
|
||||
/// <param name="hostAddress">The host address to connect to.</param>
|
||||
/// <param name="maxConnectionAttempts">How many connection attempts to make before giving up.</param>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to use when building <see cref="messageHandlers"/>.</param>
|
||||
/// <param name="message">Data that should be sent to the server with the connection attempt. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
/// <param name="useMessageHandlers">Whether or not the client should use the built-in message handler system.</param>
|
||||
/// <remarks>
|
||||
/// <para>Riptide's default transport expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>. If you are using a different transport, check the relevant documentation for what information it requires in the host address.</para>
|
||||
/// <para>Setting <paramref name="useMessageHandlers"/> to <see langword="false"/> will disable the automatic detection and execution of methods with the <see cref="MessageHandlerAttribute"/>, which is beneficial if you prefer to handle messages via the <see cref="MessageReceived"/> event.</para>
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> if a connection attempt will be made. <see langword="false"/> if an issue occurred (such as <paramref name="hostAddress"/> being in an invalid format) and a connection attempt will <i>not</i> be made.</returns>
|
||||
public bool Connect(string hostAddress, int maxConnectionAttempts = 5, byte messageHandlerGroupId = 0, Message message = null, bool useMessageHandlers = true)
|
||||
{
|
||||
Disconnect();
|
||||
|
||||
SubToTransportEvents();
|
||||
|
||||
if (!transport.Connect(hostAddress, out connection, out string connectError))
|
||||
{
|
||||
RiptideLogger.Log(LogType.Error, LogName, connectError);
|
||||
UnsubFromTransportEvents();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.maxConnectionAttempts = maxConnectionAttempts;
|
||||
connectionAttempts = 0;
|
||||
connection.Initialize(this, defaultTimeout);
|
||||
IncreaseActiveCount();
|
||||
this.useMessageHandlers = useMessageHandlers;
|
||||
if (useMessageHandlers)
|
||||
CreateMessageHandlersDictionary(messageHandlerGroupId);
|
||||
|
||||
connectMessage = Message.Create(MessageHeader.Connect);
|
||||
if (message != null)
|
||||
{
|
||||
if (message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting connection attempt data!");
|
||||
|
||||
connectMessage.AddMessage(message);
|
||||
message.Release();
|
||||
}
|
||||
|
||||
StartTime();
|
||||
Heartbeat();
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Connecting to {connection}...");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Subscribes appropriate methods to the transport's events.</summary>
|
||||
private void SubToTransportEvents()
|
||||
{
|
||||
transport.Connected += TransportConnected;
|
||||
transport.ConnectionFailed += TransportConnectionFailed;
|
||||
transport.DataReceived += HandleData;
|
||||
transport.Disconnected += TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes methods from all of the transport's events.</summary>
|
||||
private void UnsubFromTransportEvents()
|
||||
{
|
||||
transport.Connected -= TransportConnected;
|
||||
transport.ConnectionFailed -= TransportConnectionFailed;
|
||||
transport.DataReceived -= HandleData;
|
||||
transport.Disconnected -= TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreateMessageHandlersDictionary(byte messageHandlerGroupId)
|
||||
{
|
||||
MethodInfo[] methods = FindMessageHandlers();
|
||||
|
||||
messageHandlers = new Dictionary<ushort, MessageHandler>(methods.Length);
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>();
|
||||
if (attribute.GroupId != messageHandlerGroupId)
|
||||
continue;
|
||||
|
||||
if (!method.IsStatic)
|
||||
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
||||
|
||||
Delegate clientMessageHandler = Delegate.CreateDelegate(typeof(MessageHandler), method, false);
|
||||
if (clientMessageHandler != null)
|
||||
{
|
||||
// It's a message handler for Client instances
|
||||
if (messageHandlers.ContainsKey(attribute.MessageId))
|
||||
{
|
||||
MethodInfo otherMethodWithId = messageHandlers[attribute.MessageId].GetMethodInfo();
|
||||
throw new DuplicateHandlerException(attribute.MessageId, method, otherMethodWithId);
|
||||
}
|
||||
else
|
||||
messageHandlers.Add(attribute.MessageId, (MessageHandler)clientMessageHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's not a message handler for Client instances, but it might be one for Server instances
|
||||
if (Delegate.CreateDelegate(typeof(Server.MessageHandler), method, false) == null)
|
||||
throw new InvalidHandlerSignatureException(method.DeclaringType, method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Heartbeat()
|
||||
{
|
||||
if (IsConnecting)
|
||||
{
|
||||
// If still trying to connect, send connect messages instead of heartbeats
|
||||
if (connectionAttempts < maxConnectionAttempts)
|
||||
{
|
||||
Send(connectMessage, false);
|
||||
connectionAttempts++;
|
||||
}
|
||||
else
|
||||
LocalDisconnect(DisconnectReason.NeverConnected);
|
||||
}
|
||||
else if (IsPending)
|
||||
{
|
||||
// If waiting for the server to accept/reject the connection attempt
|
||||
if (connection.HasConnectAttemptTimedOut)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.TimedOut);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (IsConnected)
|
||||
{
|
||||
// If connected and not timed out, send heartbeats
|
||||
if (connection.HasTimedOut)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.TimedOut);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.SendHeartbeat();
|
||||
}
|
||||
|
||||
ExecuteLater(HeartbeatInterval, new HeartbeatEvent(this));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
transport.Poll();
|
||||
HandleMessages();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Handle(Message message, MessageHeader header, Connection connection)
|
||||
{
|
||||
switch (header)
|
||||
{
|
||||
// User messages
|
||||
case MessageHeader.Unreliable:
|
||||
case MessageHeader.Reliable:
|
||||
OnMessageReceived(message);
|
||||
break;
|
||||
|
||||
// Internal messages
|
||||
case MessageHeader.Ack:
|
||||
connection.HandleAck(message);
|
||||
break;
|
||||
case MessageHeader.Connect:
|
||||
connection.SetPending();
|
||||
break;
|
||||
case MessageHeader.Reject:
|
||||
if (!IsConnected) // Don't disconnect if we are connected
|
||||
LocalDisconnect(DisconnectReason.ConnectionRejected, message, (RejectReason)message.GetByte());
|
||||
break;
|
||||
case MessageHeader.Heartbeat:
|
||||
connection.HandleHeartbeatResponse(message);
|
||||
break;
|
||||
case MessageHeader.Disconnect:
|
||||
LocalDisconnect((DisconnectReason)message.GetByte(), message);
|
||||
break;
|
||||
case MessageHeader.Welcome:
|
||||
if (IsConnecting || IsPending)
|
||||
{
|
||||
connection.HandleWelcome(message);
|
||||
OnConnected();
|
||||
}
|
||||
break;
|
||||
case MessageHeader.ClientConnected:
|
||||
OnClientConnected(message.GetUShort());
|
||||
break;
|
||||
case MessageHeader.ClientDisconnected:
|
||||
OnClientDisconnected(message.GetUShort());
|
||||
break;
|
||||
default:
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Unexpected message header '{header}'! Discarding {message.BytesInUse} bytes.");
|
||||
break;
|
||||
}
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Sends a message to the server.</summary>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public ushort Send(Message message, bool shouldRelease = true) => connection.Send(message, shouldRelease);
|
||||
|
||||
/// <summary>Disconnects from the server.</summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (connection == null || IsNotConnected)
|
||||
return;
|
||||
|
||||
Send(Message.Create(MessageHeader.Disconnect));
|
||||
LocalDisconnect(DisconnectReason.Disconnected);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Disconnect(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
if (connection.IsConnected && connection.CanQualityDisconnect)
|
||||
LocalDisconnect(reason);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the connection.</summary>
|
||||
/// <param name="reason">The reason why the client has disconnected.</param>
|
||||
/// <param name="message">The disconnection or rejection message, potentially containing extra data to be handled externally.</param>
|
||||
/// <param name="rejectReason">The reason why the connection was rejected (<i>if</i> it was rejected).</param>
|
||||
private void LocalDisconnect(DisconnectReason reason, Message message = null, RejectReason rejectReason = RejectReason.NoConnection)
|
||||
{
|
||||
if (IsNotConnected)
|
||||
return;
|
||||
|
||||
UnsubFromTransportEvents();
|
||||
DecreaseActiveCount();
|
||||
|
||||
StopTime();
|
||||
transport.Disconnect();
|
||||
|
||||
connection.LocalDisconnect();
|
||||
|
||||
if (reason == DisconnectReason.NeverConnected)
|
||||
OnConnectionFailed(RejectReason.NoConnection);
|
||||
else if (reason == DisconnectReason.ConnectionRejected)
|
||||
OnConnectionFailed(rejectReason, message);
|
||||
else
|
||||
OnDisconnected(reason, message);
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport establishes a connection.</summary>
|
||||
private void TransportConnected(object sender, EventArgs e) { }
|
||||
|
||||
/// <summary>What to do when the transport fails to connect.</summary>
|
||||
private void TransportConnectionFailed(object sender, EventArgs e)
|
||||
{
|
||||
LocalDisconnect(DisconnectReason.NeverConnected);
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport disconnects.</summary>
|
||||
private void TransportDisconnected(object sender, Transports.DisconnectedEventArgs e)
|
||||
{
|
||||
if (connection == e.Connection)
|
||||
LocalDisconnect(e.Reason);
|
||||
}
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
connectMessage.Release();
|
||||
connectMessage = null;
|
||||
RiptideLogger.Log(LogType.Info, LogName, "Connected successfully!");
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
/// <param name="reason">The reason for the connection failure.</param>
|
||||
/// <param name="message">Additional data related to the failed connection attempt.</param>
|
||||
protected virtual void OnConnectionFailed(RejectReason reason, Message message = null)
|
||||
{
|
||||
connectMessage.Release();
|
||||
connectMessage = null;
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Connection to server failed: {Helper.GetReasonString(reason)}.");
|
||||
ConnectionFailed?.Invoke(this, new ConnectionFailedEventArgs(reason, message));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="MessageReceived"/> event and initiates handling of the received message.</summary>
|
||||
/// <param name="message">The received message.</param>
|
||||
protected virtual void OnMessageReceived(Message message)
|
||||
{
|
||||
ushort messageId = (ushort)message.GetVarULong();
|
||||
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(connection, messageId, message));
|
||||
|
||||
if (useMessageHandlers)
|
||||
{
|
||||
if (messageHandlers.TryGetValue(messageId, out MessageHandler messageHandler))
|
||||
messageHandler(message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"No message handler method found for message ID {messageId}!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
/// <param name="message">Additional data related to the disconnection.</param>
|
||||
protected virtual void OnDisconnected(DisconnectReason reason, Message message)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Disconnected from server: {Helper.GetReasonString(reason)}.");
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(reason, message));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientConnected"/> event.</summary>
|
||||
/// <param name="clientId">The numeric ID of the client that connected.</param>
|
||||
protected virtual void OnClientConnected(ushort clientId)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} connected.");
|
||||
ClientConnected?.Invoke(this, new ClientConnectedEventArgs(clientId));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientDisconnected"/> event.</summary>
|
||||
/// <param name="clientId">The numeric ID of the client that disconnected.</param>
|
||||
protected virtual void OnClientDisconnected(ushort clientId)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {clientId} disconnected.");
|
||||
ClientDisconnected?.Invoke(this, new ClientDisconnectedEventArgs(clientId));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,648 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The state of a connection.</summary>
|
||||
internal enum ConnectionState : byte
|
||||
{
|
||||
/// <summary>Not connected. No connection has been established or the connection has been closed.</summary>
|
||||
NotConnected,
|
||||
/// <summary>Connecting. Still trying to establish a connection.</summary>
|
||||
Connecting,
|
||||
/// <summary>Connection is pending. The server is still determining whether or not the connection should be allowed.</summary>
|
||||
Pending,
|
||||
/// <summary>Connected. A connection has been established successfully.</summary>
|
||||
Connected,
|
||||
}
|
||||
|
||||
/// <summary>Represents a connection to a <see cref="Server"/> or <see cref="Client"/>.</summary>
|
||||
public abstract class Connection
|
||||
{
|
||||
/// <summary>Invoked when the notify message with the given sequence ID is successfully delivered.</summary>
|
||||
public Action<ushort> NotifyDelivered;
|
||||
/// <summary>Invoked when the notify message with the given sequence ID is lost.</summary>
|
||||
public Action<ushort> NotifyLost;
|
||||
/// <summary>Invoked when a notify message is received.</summary>
|
||||
public Action<Message> NotifyReceived;
|
||||
/// <summary>Invoked when the reliable message with the given sequence ID is successfully delivered.</summary>
|
||||
public Action<ushort> ReliableDelivered;
|
||||
|
||||
/// <summary>The connection's numeric ID.</summary>
|
||||
public ushort Id { get; internal set; }
|
||||
/// <summary>Whether or not the connection is currently <i>not</i> trying to connect, pending, nor actively connected.</summary>
|
||||
public bool IsNotConnected => state == ConnectionState.NotConnected;
|
||||
/// <summary>Whether or not the connection is currently in the process of connecting.</summary>
|
||||
public bool IsConnecting => state == ConnectionState.Connecting;
|
||||
/// <summary>Whether or not the connection is currently pending (waiting to be accepted/rejected by the server).</summary>
|
||||
public bool IsPending => state == ConnectionState.Pending;
|
||||
/// <summary>Whether or not the connection is currently connected.</summary>
|
||||
public bool IsConnected => state == ConnectionState.Connected;
|
||||
/// <summary>The round trip time (ping) of the connection, in milliseconds. -1 if not calculated yet.</summary>
|
||||
public short RTT
|
||||
{
|
||||
get => _rtt;
|
||||
private set
|
||||
{
|
||||
SmoothRTT = _rtt == -1 ? value : (short)Math.Max(1f, SmoothRTT * 0.7f + value * 0.3f);
|
||||
_rtt = value;
|
||||
}
|
||||
}
|
||||
private short _rtt;
|
||||
/// <summary>The smoothed round trip time (ping) of the connection, in milliseconds. -1 if not calculated yet.</summary>
|
||||
/// <remarks>This value is slower to accurately represent lasting changes in latency than <see cref="RTT"/>, but it is less susceptible to changing drastically due to significant—but temporary—jumps in latency.</remarks>
|
||||
public short SmoothRTT { get; private set; }
|
||||
/// <summary>The time (in milliseconds) after which to disconnect if no heartbeats are received.</summary>
|
||||
public int TimeoutTime { get; set; }
|
||||
/// <summary>Whether or not the connection can time out.</summary>
|
||||
public bool CanTimeout
|
||||
{
|
||||
get => _canTimeout;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
ResetTimeout();
|
||||
|
||||
_canTimeout = value;
|
||||
}
|
||||
}
|
||||
private bool _canTimeout;
|
||||
/// <summary>Whether or not the connection can disconnect due to poor connection quality.</summary>
|
||||
/// <remarks>When this is set to <see langword="false"/>, <see cref="MaxAvgSendAttempts"/>, <see cref="MaxSendAttempts"/>,
|
||||
/// and <see cref="MaxNotifyLoss"/> are ignored and exceeding their values will not trigger a disconnection.</remarks>
|
||||
public bool CanQualityDisconnect;
|
||||
/// <summary>The connection's metrics.</summary>
|
||||
public readonly ConnectionMetrics Metrics;
|
||||
/// <summary>The maximum acceptable average number of send attempts it takes to deliver a reliable message. The connection
|
||||
/// will be closed if this is exceeded more than <see cref="AvgSendAttemptsResilience"/> times in a row.</summary>
|
||||
public int MaxAvgSendAttempts;
|
||||
/// <summary>How many consecutive times <see cref="MaxAvgSendAttempts"/> can be exceeded before triggering a disconnect.</summary>
|
||||
public int AvgSendAttemptsResilience;
|
||||
/// <summary>The absolute maximum number of times a reliable message may be sent. A single message reaching this threshold will cause a disconnection.</summary>
|
||||
public int MaxSendAttempts;
|
||||
/// <summary>The maximum acceptable loss rate of notify messages. The connection will be closed if this is exceeded more than <see cref="NotifyLossResilience"/> times in a row.</summary>
|
||||
public float MaxNotifyLoss;
|
||||
/// <summary>How many consecutive times <see cref="MaxNotifyLoss"/> can be exceeded before triggering a disconnect.</summary>
|
||||
public int NotifyLossResilience;
|
||||
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
internal Peer Peer { get; private set; }
|
||||
/// <summary>Whether or not the connection has timed out.</summary>
|
||||
internal bool HasTimedOut => _canTimeout && Peer.CurrentTime - lastHeartbeat > TimeoutTime;
|
||||
/// <summary>Whether or not the connection attempt has timed out.</summary>
|
||||
internal bool HasConnectAttemptTimedOut => _canTimeout && Peer.CurrentTime - lastHeartbeat > Peer.ConnectTimeoutTime;
|
||||
|
||||
/// <summary>The sequencer for notify messages.</summary>
|
||||
private readonly NotifySequencer notify;
|
||||
/// <summary>The sequencer for reliable messages.</summary>
|
||||
private readonly ReliableSequencer reliable;
|
||||
/// <summary>The currently pending reliably sent messages whose delivery has not been acknowledged yet. Stored by sequence ID.</summary>
|
||||
private readonly Dictionary<ushort, PendingMessage> pendingMessages;
|
||||
/// <summary>The connection's current state.</summary>
|
||||
private ConnectionState state;
|
||||
/// <summary>The number of consecutive times that the <see cref="MaxAvgSendAttempts"/> threshold was exceeded.</summary>
|
||||
private int sendAttemptsViolations;
|
||||
/// <summary>The number of consecutive times that the <see cref="MaxNotifyLoss"/> threshold was exceeded.</summary>
|
||||
private int lossRateViolations;
|
||||
/// <summary>The time at which the last heartbeat was received from the other end.</summary>
|
||||
private long lastHeartbeat;
|
||||
/// <summary>The ID of the last ping that was sent.</summary>
|
||||
private byte lastPingId;
|
||||
/// <summary>The ID of the currently pending ping.</summary>
|
||||
private byte pendingPingId;
|
||||
/// <summary>The time at which the currently pending ping was sent.</summary>
|
||||
private long pendingPingSendTime;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
protected Connection()
|
||||
{
|
||||
Metrics = new ConnectionMetrics();
|
||||
notify = new NotifySequencer(this);
|
||||
reliable = new ReliableSequencer(this);
|
||||
state = ConnectionState.Connecting;
|
||||
_rtt = -1;
|
||||
SmoothRTT = -1;
|
||||
_canTimeout = true;
|
||||
CanQualityDisconnect = true;
|
||||
MaxAvgSendAttempts = 5;
|
||||
AvgSendAttemptsResilience = 64;
|
||||
MaxSendAttempts = 15;
|
||||
MaxNotifyLoss = 0.05f; // 5%
|
||||
NotifyLossResilience = 64;
|
||||
pendingMessages = new Dictionary<ushort, PendingMessage>();
|
||||
}
|
||||
|
||||
/// <summary>Initializes connection data.</summary>
|
||||
/// <param name="peer">The <see cref="Riptide.Peer"/> which this connection belongs to.</param>
|
||||
/// <param name="timeoutTime">The timeout time.</param>
|
||||
internal void Initialize(Peer peer, int timeoutTime)
|
||||
{
|
||||
Peer = peer;
|
||||
TimeoutTime = timeoutTime;
|
||||
}
|
||||
|
||||
/// <summary>Resets the connection's timeout time.</summary>
|
||||
public void ResetTimeout()
|
||||
{
|
||||
lastHeartbeat = Peer.CurrentTime;
|
||||
}
|
||||
|
||||
/// <summary>Sends a message.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <returns>For reliable and notify messages, the sequence ID that the message was sent with. 0 for unreliable messages.</returns>
|
||||
/// <remarks>
|
||||
/// If you intend to continue using the message instance after calling this method, you <i>must</i> set <paramref name="shouldRelease"/>
|
||||
/// to <see langword="false"/>. <see cref="Message.Release"/> can be used to manually return the message to the pool at a later time.
|
||||
/// </remarks>
|
||||
public ushort Send(Message message, bool shouldRelease = true)
|
||||
{
|
||||
ushort sequenceId = 0;
|
||||
if (message.SendMode == MessageSendMode.Notify)
|
||||
{
|
||||
sequenceId = notify.InsertHeader(message);
|
||||
int byteAmount = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, Message.ByteBuffer, 0, byteAmount);
|
||||
Send(Message.ByteBuffer, byteAmount);
|
||||
Metrics.SentNotify(byteAmount);
|
||||
}
|
||||
else if (message.SendMode == MessageSendMode.Unreliable)
|
||||
{
|
||||
int byteAmount = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, Message.ByteBuffer, 0, byteAmount);
|
||||
Send(Message.ByteBuffer, byteAmount);
|
||||
Metrics.SentUnreliable(byteAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
sequenceId = reliable.NextSequenceId;
|
||||
PendingMessage pendingMessage = PendingMessage.Create(sequenceId, message, this);
|
||||
pendingMessages.Add(sequenceId, pendingMessage);
|
||||
pendingMessage.TrySend();
|
||||
Metrics.ReliableUniques++;
|
||||
}
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
|
||||
return sequenceId;
|
||||
}
|
||||
|
||||
/// <summary>Sends data.</summary>
|
||||
/// <param name="dataBuffer">The array containing the data.</param>
|
||||
/// <param name="amount">The number of bytes in the array which should be sent.</param>
|
||||
protected internal abstract void Send(byte[] dataBuffer, int amount);
|
||||
|
||||
/// <summary>Processes a notify message.</summary>
|
||||
/// <param name="dataBuffer">The received data.</param>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="message">The message instance to use.</param>
|
||||
internal void ProcessNotify(byte[] dataBuffer, int amount, Message message)
|
||||
{
|
||||
notify.UpdateReceivedAcks(Converter.UShortFromBits(dataBuffer, Message.HeaderBits), Converter.ByteFromBits(dataBuffer, Message.HeaderBits + 16));
|
||||
|
||||
Metrics.ReceivedNotify(amount);
|
||||
if (notify.ShouldHandle(Converter.UShortFromBits(dataBuffer, Message.HeaderBits + 24)))
|
||||
{
|
||||
Buffer.BlockCopy(dataBuffer, 1, message.Data, 1, amount - 1); // Copy payload
|
||||
NotifyReceived?.Invoke(message);
|
||||
}
|
||||
else
|
||||
Metrics.NotifyDiscarded++;
|
||||
}
|
||||
|
||||
/// <summary>Determines if the message with the given sequence ID should be handled.</summary>
|
||||
/// <param name="sequenceId">The message's sequence ID.</param>
|
||||
/// <returns>Whether or not the message should be handled.</returns>
|
||||
internal bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
return reliable.ShouldHandle(sequenceId);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the connection.</summary>
|
||||
internal void LocalDisconnect()
|
||||
{
|
||||
state = ConnectionState.NotConnected;
|
||||
|
||||
foreach (PendingMessage pendingMessage in pendingMessages.Values)
|
||||
pendingMessage.Clear();
|
||||
|
||||
pendingMessages.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Resends the <see cref="PendingMessage"/> with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the message to resend.</param>
|
||||
private void ResendMessage(ushort sequenceId)
|
||||
{
|
||||
if (pendingMessages.TryGetValue(sequenceId, out PendingMessage pendingMessage))
|
||||
pendingMessage.RetrySend();
|
||||
}
|
||||
|
||||
/// <summary>Clears the <see cref="PendingMessage"/> with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID that was acknowledged.</param>
|
||||
internal void ClearMessage(ushort sequenceId)
|
||||
{
|
||||
if (pendingMessages.TryGetValue(sequenceId, out PendingMessage pendingMessage))
|
||||
{
|
||||
ReliableDelivered?.Invoke(sequenceId);
|
||||
pendingMessage.Clear();
|
||||
pendingMessages.Remove(sequenceId);
|
||||
UpdateSendAttemptsViolations();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Puts the connection in the pending state.</summary>
|
||||
internal void SetPending()
|
||||
{
|
||||
if (IsConnecting)
|
||||
{
|
||||
state = ConnectionState.Pending;
|
||||
ResetTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks the average send attempts (of reliable messages) and updates <see cref="sendAttemptsViolations"/> accordingly.</summary>
|
||||
private void UpdateSendAttemptsViolations()
|
||||
{
|
||||
if (Metrics.RollingReliableSends.Mean > MaxAvgSendAttempts)
|
||||
{
|
||||
sendAttemptsViolations++;
|
||||
if (sendAttemptsViolations >= AvgSendAttemptsResilience)
|
||||
Peer.Disconnect(this, DisconnectReason.PoorConnection);
|
||||
}
|
||||
else
|
||||
sendAttemptsViolations = 0;
|
||||
}
|
||||
|
||||
/// <summary>Checks the loss rate (of notify messages) and updates <see cref="lossRateViolations"/> accordingly.</summary>
|
||||
private void UpdateLossViolations()
|
||||
{
|
||||
if (Metrics.RollingNotifyLossRate > MaxNotifyLoss)
|
||||
{
|
||||
lossRateViolations++;
|
||||
if (lossRateViolations >= NotifyLossResilience)
|
||||
Peer.Disconnect(this, DisconnectReason.PoorConnection);
|
||||
}
|
||||
else
|
||||
lossRateViolations = 0;
|
||||
}
|
||||
|
||||
#region Messages
|
||||
/// <summary>Sends an ack message for the given sequence ID.</summary>
|
||||
/// <param name="forSeqId">The sequence ID to acknowledge.</param>
|
||||
/// <param name="lastReceivedSeqId">The sequence ID of the latest message we've received.</param>
|
||||
/// <param name="receivedSeqIds">Sequence IDs of previous messages that we have (or have not received).</param>
|
||||
private void SendAck(ushort forSeqId, ushort lastReceivedSeqId, Bitfield receivedSeqIds)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Ack);
|
||||
message.AddUShort(lastReceivedSeqId);
|
||||
message.AddUShort(receivedSeqIds.First16);
|
||||
|
||||
if (forSeqId == lastReceivedSeqId)
|
||||
message.AddBool(false);
|
||||
else
|
||||
message.AddBool(true);
|
||||
message.AddUShort(forSeqId);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles an ack message.</summary>
|
||||
/// <param name="message">The ack message to handle.</param>
|
||||
internal void HandleAck(Message message)
|
||||
{
|
||||
ushort remoteLastReceivedSeqId = message.GetUShort();
|
||||
ushort remoteAcksBitField = message.GetUShort();
|
||||
ushort ackedSeqId = message.GetBool() ? message.GetUShort() : remoteLastReceivedSeqId;
|
||||
|
||||
ClearMessage(ackedSeqId);
|
||||
reliable.UpdateReceivedAcks(remoteLastReceivedSeqId, remoteAcksBitField);
|
||||
}
|
||||
|
||||
#region Server
|
||||
/// <summary>Sends a welcome message.</summary>
|
||||
internal void SendWelcome()
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Welcome);
|
||||
message.AddUShort(Id);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles a welcome message on the server.</summary>
|
||||
/// <param name="message">The welcome message to handle.</param>
|
||||
/// <returns>Whether or not the connection is now connected.</returns>
|
||||
internal bool HandleWelcomeResponse(Message message)
|
||||
{
|
||||
if (!IsPending)
|
||||
return false;
|
||||
|
||||
ushort id = message.GetUShort();
|
||||
if (Id != id)
|
||||
RiptideLogger.Log(LogType.Error, Peer.LogName, $"Client has assumed ID {id} instead of {Id}!");
|
||||
|
||||
state = ConnectionState.Connected;
|
||||
ResetTimeout();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Handles a heartbeat message.</summary>
|
||||
/// <param name="message">The heartbeat message to handle.</param>
|
||||
internal void HandleHeartbeat(Message message)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return; // A client that is not yet fully connected should not be sending heartbeats
|
||||
|
||||
RespondHeartbeat(message.GetByte());
|
||||
RTT = message.GetShort();
|
||||
|
||||
ResetTimeout();
|
||||
}
|
||||
|
||||
/// <summary>Sends a heartbeat message.</summary>
|
||||
private void RespondHeartbeat(byte pingId)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Heartbeat);
|
||||
message.AddByte(pingId);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
/// <summary>Handles a welcome message on the client.</summary>
|
||||
/// <param name="message">The welcome message to handle.</param>
|
||||
internal void HandleWelcome(Message message)
|
||||
{
|
||||
Id = message.GetUShort();
|
||||
state = ConnectionState.Connected;
|
||||
ResetTimeout();
|
||||
|
||||
RespondWelcome();
|
||||
}
|
||||
|
||||
/// <summary>Sends a welcome response message.</summary>
|
||||
private void RespondWelcome()
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Welcome);
|
||||
message.AddUShort(Id);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Sends a heartbeat message.</summary>
|
||||
internal void SendHeartbeat()
|
||||
{
|
||||
pendingPingId = lastPingId++;
|
||||
pendingPingSendTime = Peer.CurrentTime;
|
||||
|
||||
Message message = Message.Create(MessageHeader.Heartbeat);
|
||||
message.AddByte(pendingPingId);
|
||||
message.AddShort(RTT);
|
||||
|
||||
Send(message);
|
||||
}
|
||||
|
||||
/// <summary>Handles a heartbeat message.</summary>
|
||||
/// <param name="message">The heartbeat message to handle.</param>
|
||||
internal void HandleHeartbeatResponse(Message message)
|
||||
{
|
||||
byte pingId = message.GetByte();
|
||||
|
||||
if (pendingPingId == pingId)
|
||||
RTT = (short)Math.Max(1, Peer.CurrentTime - pendingPingSendTime);
|
||||
|
||||
ResetTimeout();
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="NotifyDelivered"/> event.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the delivered message.</param>
|
||||
protected virtual void OnNotifyDelivered(ushort sequenceId)
|
||||
{
|
||||
Metrics.DeliveredNotify();
|
||||
NotifyDelivered?.Invoke(sequenceId);
|
||||
UpdateLossViolations();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="NotifyLost"/> event.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the lost message.</param>
|
||||
protected virtual void OnNotifyLost(ushort sequenceId)
|
||||
{
|
||||
Metrics.LostNotify();
|
||||
NotifyLost?.Invoke(sequenceId);
|
||||
UpdateLossViolations();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Message Sequencing
|
||||
/// <summary>Provides functionality for filtering out duplicate messages and determining delivery/loss status.</summary>
|
||||
private abstract class Sequencer
|
||||
{
|
||||
/// <summary>The next sequence ID to use.</summary>
|
||||
internal ushort NextSequenceId => _nextSequenceId++;
|
||||
private ushort _nextSequenceId = 1;
|
||||
|
||||
/// <summary>The connection this sequencer belongs to.</summary>
|
||||
protected readonly Connection connection;
|
||||
/// <summary>The sequence ID of the latest message that we want to acknowledge.</summary>
|
||||
protected ushort lastReceivedSeqId;
|
||||
/// <summary>Sequence IDs of messages which we have (or have not) received and want to acknowledge.</summary>
|
||||
protected readonly Bitfield receivedSeqIds = new Bitfield();
|
||||
/// <summary>The sequence ID of the latest message that we've received an ack for.</summary>
|
||||
protected ushort lastAckedSeqId;
|
||||
/// <summary>Sequence IDs of messages we sent and which we have (or have not) received acks for.</summary>
|
||||
protected readonly Bitfield ackedSeqIds = new Bitfield(false);
|
||||
|
||||
/// <summary>Initializes the sequencer.</summary>
|
||||
/// <param name="connection">The connection this sequencer belongs to.</param>
|
||||
protected Sequencer(Connection connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>Determines whether or not to handle a message with the given sequence ID.</summary>
|
||||
/// <param name="sequenceId">The sequence ID in question.</param>
|
||||
/// <returns>Whether or not to handle the message.</returns>
|
||||
internal abstract bool ShouldHandle(ushort sequenceId);
|
||||
|
||||
/// <summary>Updates which messages we've received acks for.</summary>
|
||||
/// <param name="remoteLastReceivedSeqId">The latest sequence ID that the other end has received.</param>
|
||||
/// <param name="remoteReceivedSeqIds">Sequence IDs which the other end has (or has not) received.</param>
|
||||
internal abstract void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private class NotifySequencer : Sequencer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal NotifySequencer(Connection connection) : base(connection) { }
|
||||
|
||||
/// <summary>Inserts the notify header into the given message.</summary>
|
||||
/// <param name="message">The message to insert the header into.</param>
|
||||
/// <returns>The sequence ID of the message.</returns>
|
||||
internal ushort InsertHeader(Message message)
|
||||
{
|
||||
ushort sequenceId = NextSequenceId;
|
||||
ulong notifyBits = lastReceivedSeqId | ((ulong)receivedSeqIds.First8 << (2 * Converter.BitsPerByte)) | ((ulong)sequenceId << (3 * Converter.BitsPerByte));
|
||||
message.SetBits(notifyBits, 5 * Converter.BitsPerByte, Message.HeaderBits);
|
||||
return sequenceId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Duplicate and out of order messages are filtered out and not handled.</remarks>
|
||||
internal override bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(sequenceId, lastReceivedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The received sequence ID is newer than the previous one
|
||||
receivedSeqIds.ShiftBy(sequenceGap);
|
||||
lastReceivedSeqId = sequenceId;
|
||||
|
||||
if (receivedSeqIds.IsSet(sequenceGap))
|
||||
return false;
|
||||
|
||||
receivedSeqIds.Set(sequenceGap);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The received sequence ID is older than or the same as the previous one (out of order or duplicate message)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(remoteLastReceivedSeqId, lastAckedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
if (sequenceGap > 1)
|
||||
{
|
||||
// Deal with messages in the gap
|
||||
while (sequenceGap > 9) // 9 because a gap of 1 means sequence IDs are consecutive, and notify uses 8 bits for the bitfield. 9 means all 8 bits are in use
|
||||
{
|
||||
lastAckedSeqId++;
|
||||
sequenceGap--;
|
||||
connection.NotifyLost?.Invoke(lastAckedSeqId);
|
||||
}
|
||||
|
||||
int bitCount = sequenceGap - 1;
|
||||
int bit = 1 << bitCount;
|
||||
for (int i = 0; i < bitCount; i++)
|
||||
{
|
||||
lastAckedSeqId++;
|
||||
bit >>= 1;
|
||||
if ((remoteReceivedSeqIds & bit) == 0)
|
||||
connection.OnNotifyLost(lastAckedSeqId);
|
||||
else
|
||||
connection.OnNotifyDelivered(lastAckedSeqId);
|
||||
}
|
||||
}
|
||||
|
||||
lastAckedSeqId = remoteLastReceivedSeqId;
|
||||
connection.OnNotifyDelivered(lastAckedSeqId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private class ReliableSequencer : Sequencer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal ReliableSequencer(Connection connection) : base(connection) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Duplicate messages are filtered out while out of order messages are handled.</remarks>
|
||||
internal override bool ShouldHandle(ushort sequenceId)
|
||||
{
|
||||
bool doHandle = false;
|
||||
int sequenceGap = Helper.GetSequenceGap(sequenceId, lastReceivedSeqId);
|
||||
|
||||
if (sequenceGap != 0)
|
||||
{
|
||||
// The received sequence ID is different from the previous one
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The received sequence ID is newer than the previous one
|
||||
if (sequenceGap > 64)
|
||||
RiptideLogger.Log(LogType.Warning, connection.Peer.LogName, $"The gap between received sequence IDs was very large ({sequenceGap})!");
|
||||
|
||||
receivedSeqIds.ShiftBy(sequenceGap);
|
||||
lastReceivedSeqId = sequenceId;
|
||||
}
|
||||
else // The received sequence ID is older than the previous one (out of order message)
|
||||
sequenceGap = -sequenceGap;
|
||||
|
||||
doHandle = !receivedSeqIds.IsSet(sequenceGap);
|
||||
receivedSeqIds.Set(sequenceGap);
|
||||
}
|
||||
|
||||
connection.SendAck(sequenceId, lastReceivedSeqId, receivedSeqIds);
|
||||
return doHandle;
|
||||
}
|
||||
|
||||
/// <summary>Updates which messages we've received acks for.</summary>
|
||||
/// <param name="remoteLastReceivedSeqId">The latest sequence ID that the other end has received.</param>
|
||||
/// <param name="remoteReceivedSeqIds">Sequence IDs which the other end has (or has not) received.</param>
|
||||
internal override void UpdateReceivedAcks(ushort remoteLastReceivedSeqId, ushort remoteReceivedSeqIds)
|
||||
{
|
||||
int sequenceGap = Helper.GetSequenceGap(remoteLastReceivedSeqId, lastAckedSeqId);
|
||||
|
||||
if (sequenceGap > 0)
|
||||
{
|
||||
// The latest sequence ID that the other end has received is newer than the previous one
|
||||
if (!ackedSeqIds.HasCapacityFor(sequenceGap, out int overflow))
|
||||
{
|
||||
for (int i = 0; i < overflow; i++)
|
||||
{
|
||||
// Resend those messages which haven't been acked and whose sequence IDs are about to be pushed out of the bitfield
|
||||
if (!ackedSeqIds.CheckAndTrimLast(out int checkedPosition))
|
||||
connection.ResendMessage((ushort)(lastAckedSeqId - checkedPosition));
|
||||
else
|
||||
connection.ClearMessage((ushort)(lastAckedSeqId - checkedPosition));
|
||||
}
|
||||
}
|
||||
|
||||
ackedSeqIds.ShiftBy(sequenceGap);
|
||||
lastAckedSeqId = remoteLastReceivedSeqId;
|
||||
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
// Clear any messages that have been newly acknowledged
|
||||
if (!ackedSeqIds.IsSet(i + 1) && (remoteReceivedSeqIds & (1 << i)) != 0)
|
||||
connection.ClearMessage((ushort)(lastAckedSeqId - (i + 1)));
|
||||
}
|
||||
|
||||
ackedSeqIds.Combine(remoteReceivedSeqIds);
|
||||
ackedSeqIds.Set(sequenceGap); // Ensure that the bit corresponding to the previous acked sequence ID is set
|
||||
connection.ClearMessage(remoteLastReceivedSeqId);
|
||||
}
|
||||
else if (sequenceGap < 0)
|
||||
{
|
||||
// The latest sequence ID that the other end has received is older than the previous one (out of order ack)
|
||||
ackedSeqIds.Set(-sequenceGap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The latest sequence ID that the other end has received is the same as the previous one (duplicate ack)
|
||||
ackedSeqIds.Combine(remoteReceivedSeqIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Contains event data for when a client connects to the server.</summary>
|
||||
public class ServerConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The newly connected client.</summary>
|
||||
public readonly Connection Client;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The newly connected client.</param>
|
||||
public ServerConnectedEventArgs(Connection client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a connection fails to be fully established.</summary>
|
||||
public class ServerConnectionFailedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The connection that failed to be established.</summary>
|
||||
public readonly Connection Client;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The connection that failed to be established.</param>
|
||||
public ServerConnectionFailedEventArgs(Connection client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a client disconnects from the server.</summary>
|
||||
public class ServerDisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The client that disconnected.</summary>
|
||||
public readonly Connection Client;
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="client">The client that disconnected.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
public ServerDisconnectedEventArgs(Connection client, DisconnectReason reason)
|
||||
{
|
||||
Client = client;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a message is received.</summary>
|
||||
public class MessageReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The connection from which the message was received.</summary>
|
||||
public readonly Connection FromConnection;
|
||||
/// <summary>The ID of the message.</summary>
|
||||
public readonly ushort MessageId;
|
||||
/// <summary>The received message.</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="fromConnection">The connection from which the message was received.</param>
|
||||
/// <param name="messageId">The ID of the message.</param>
|
||||
/// <param name="message">The received message.</param>
|
||||
public MessageReceivedEventArgs(Connection fromConnection, ushort messageId, Message message)
|
||||
{
|
||||
FromConnection = fromConnection;
|
||||
MessageId = messageId;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a connection attempt to a server fails.</summary>
|
||||
public class ConnectionFailedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The reason for the connection failure.</summary>
|
||||
public readonly RejectReason Reason;
|
||||
/// <summary>Additional data related to the failed connection attempt (if any).</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="reason">The reason for the connection failure.</param>
|
||||
/// <param name="message">Additional data related to the failed connection attempt (if any).</param>
|
||||
public ConnectionFailedEventArgs(RejectReason reason, Message message)
|
||||
{
|
||||
Reason = reason;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when the client disconnects from a server.</summary>
|
||||
public class DisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
/// <summary>Additional data related to the disconnection (if any).</summary>
|
||||
public readonly Message Message;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
/// <param name="message">Additional data related to the disconnection (if any).</param>
|
||||
public DisconnectedEventArgs(DisconnectReason reason, Message message)
|
||||
{
|
||||
Reason = reason;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a non-local client connects to the server.</summary>
|
||||
public class ClientConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The numeric ID of the client that connected.</summary>
|
||||
public readonly ushort Id;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="id">The numeric ID of the client that connected.</param>
|
||||
public ClientConnectedEventArgs(ushort id) => Id = id;
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a non-local client disconnects from the server.</summary>
|
||||
public class ClientDisconnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The numeric ID of the client that disconnected.</summary>
|
||||
public readonly ushort Id;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="id">The numeric ID of the client that disconnected.</param>
|
||||
public ClientDisconnectedEventArgs(ushort id) => Id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The exception that is thrown when a <see cref="Message"/> does not contain enough unwritten bits to perform an operation.</summary>
|
||||
public class InsufficientCapacityException : Exception
|
||||
{
|
||||
/// <summary>The message with insufficient remaining capacity.</summary>
|
||||
public readonly Message RiptideMessage;
|
||||
/// <summary>The name of the type which could not be added to the message.</summary>
|
||||
public readonly string TypeName;
|
||||
/// <summary>The number of available bits the type requires in order to be added successfully.</summary>
|
||||
public readonly int RequiredBits;
|
||||
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance.</summary>
|
||||
public InsufficientCapacityException() { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public InsufficientCapacityException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public InsufficientCapacityException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="reserveBits">The number of bits which were attempted to be reserved.</param>
|
||||
public InsufficientCapacityException(Message message, int reserveBits) : base(GetErrorMessage(message, reserveBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = "reservation";
|
||||
RequiredBits = reserveBits;
|
||||
}
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="typeName">The name of the type which could not be added to the message.</param>
|
||||
/// <param name="requiredBits">The number of available bits required for the type to be added successfully.</param>
|
||||
public InsufficientCapacityException(Message message, string typeName, int requiredBits) : base(GetErrorMessage(message, typeName, requiredBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = typeName;
|
||||
RequiredBits = requiredBits;
|
||||
}
|
||||
/// <summary>Initializes a new <see cref="InsufficientCapacityException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="message">The message with insufficient remaining capacity.</param>
|
||||
/// <param name="arrayLength">The length of the array which could not be added to the message.</param>
|
||||
/// <param name="typeName">The name of the array's type.</param>
|
||||
/// <param name="requiredBits">The number of available bits required for a single element of the array to be added successfully.</param>
|
||||
public InsufficientCapacityException(Message message, int arrayLength, string typeName, int requiredBits) : base(GetErrorMessage(message, arrayLength, typeName, requiredBits))
|
||||
{
|
||||
RiptideMessage = message;
|
||||
TypeName = $"{typeName}[]";
|
||||
RequiredBits = requiredBits * arrayLength;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, int reserveBits)
|
||||
{
|
||||
return $"Cannot reserve {reserveBits} {Helper.CorrectForm(reserveBits, "bit")} in a message with {message.UnwrittenBits} " +
|
||||
$"{Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, string typeName, int requiredBits)
|
||||
{
|
||||
return $"Cannot add a value of type '{typeName}' (requires {requiredBits} {Helper.CorrectForm(requiredBits, "bit")}) to " +
|
||||
$"a message with {message.UnwrittenBits} {Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Message message, int arrayLength, string typeName, int requiredBits)
|
||||
{
|
||||
requiredBits *= arrayLength;
|
||||
return $"Cannot add an array of type '{typeName}[]' with {arrayLength} {Helper.CorrectForm(arrayLength, "element")} (requires {requiredBits} {Helper.CorrectForm(requiredBits, "bit")}) " +
|
||||
$"to a message with {message.UnwrittenBits} {Helper.CorrectForm(message.UnwrittenBits, "bit")} of remaining capacity!";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when a method with a <see cref="MessageHandlerAttribute"/> is not marked as <see langword="static"/>.</summary>
|
||||
public class NonStaticHandlerException : Exception
|
||||
{
|
||||
/// <summary>The type containing the handler method.</summary>
|
||||
public readonly Type DeclaringType;
|
||||
/// <summary>The name of the handler method.</summary>
|
||||
public readonly string HandlerMethodName;
|
||||
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance.</summary>
|
||||
public NonStaticHandlerException() { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public NonStaticHandlerException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public NonStaticHandlerException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="NonStaticHandlerException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="declaringType">The type containing the handler method.</param>
|
||||
/// <param name="handlerMethodName">The name of the handler method.</param>
|
||||
public NonStaticHandlerException(Type declaringType, string handlerMethodName) : base(GetErrorMessage(declaringType, handlerMethodName))
|
||||
{
|
||||
DeclaringType = declaringType;
|
||||
HandlerMethodName = handlerMethodName;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Type declaringType, string handlerMethodName)
|
||||
{
|
||||
return $"'{declaringType.Name}.{handlerMethodName}' is an instance method, but message handler methods must be static!";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when a method with a <see cref="MessageHandlerAttribute"/> does not have an acceptable message handler method signature (either <see cref="Server.MessageHandler"/> or <see cref="Client.MessageHandler"/>).</summary>
|
||||
public class InvalidHandlerSignatureException : Exception
|
||||
{
|
||||
/// <summary>The type containing the handler method.</summary>
|
||||
public readonly Type DeclaringType;
|
||||
/// <summary>The name of the handler method.</summary>
|
||||
public readonly string HandlerMethodName;
|
||||
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance.</summary>
|
||||
public InvalidHandlerSignatureException() { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public InvalidHandlerSignatureException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public InvalidHandlerSignatureException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="InvalidHandlerSignatureException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="declaringType">The type containing the handler method.</param>
|
||||
/// <param name="handlerMethodName">The name of the handler method.</param>
|
||||
public InvalidHandlerSignatureException(Type declaringType, string handlerMethodName) : base(GetErrorMessage(declaringType, handlerMethodName))
|
||||
{
|
||||
DeclaringType = declaringType;
|
||||
HandlerMethodName = handlerMethodName;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(Type declaringType, string handlerMethodName)
|
||||
{
|
||||
return $"'{declaringType.Name}.{handlerMethodName}' doesn't match any acceptable message handler method signatures! Server message handler methods should have a 'ushort' and a '{nameof(Riptide.Message)}' parameter, while client message handler methods should only have a '{nameof(Riptide.Message)}' parameter.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception that is thrown when multiple methods with <see cref="MessageHandlerAttribute"/>s are set to handle messages with the same ID <i>and</i> have the same method signature.</summary>
|
||||
public class DuplicateHandlerException : Exception
|
||||
{
|
||||
/// <summary>The message ID with multiple handler methods.</summary>
|
||||
public readonly ushort Id;
|
||||
/// <summary>The type containing the first handler method.</summary>
|
||||
public readonly Type DeclaringType1;
|
||||
/// <summary>The name of the first handler method.</summary>
|
||||
public readonly string HandlerMethodName1;
|
||||
/// <summary>The type containing the second handler method.</summary>
|
||||
public readonly Type DeclaringType2;
|
||||
/// <summary>The name of the second handler method.</summary>
|
||||
public readonly string HandlerMethodName2;
|
||||
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message.</summary>
|
||||
public DuplicateHandlerException() { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public DuplicateHandlerException(string message) : base(message) { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance with a specified error message and a reference to the inner exception that is the cause of this exception.</summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="inner">The exception that is the cause of the current exception. If <paramref name="inner"/> is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
|
||||
public DuplicateHandlerException(string message, Exception inner) : base(message, inner) { }
|
||||
/// <summary>Initializes a new <see cref="DuplicateHandlerException"/> instance and constructs an error message from the given information.</summary>
|
||||
/// <param name="id">The message ID with multiple handler methods.</param>
|
||||
/// <param name="method1">The first handler method's info.</param>
|
||||
/// <param name="method2">The second handler method's info.</param>
|
||||
public DuplicateHandlerException(ushort id, MethodInfo method1, MethodInfo method2) : base(GetErrorMessage(id, method1, method2))
|
||||
{
|
||||
Id = id;
|
||||
DeclaringType1 = method1.DeclaringType;
|
||||
HandlerMethodName1 = method1.Name;
|
||||
DeclaringType2 = method2.DeclaringType;
|
||||
HandlerMethodName2 = method2.Name;
|
||||
}
|
||||
|
||||
/// <summary>Constructs the error message from the given information.</summary>
|
||||
/// <returns>The error message.</returns>
|
||||
private static string GetErrorMessage(ushort id, MethodInfo method1, MethodInfo method2)
|
||||
{
|
||||
return $"Message handler methods '{method1.DeclaringType.Name}.{method1.Name}' and '{method2.DeclaringType.Name}.{method2.Name}' are both set to handle messages with ID {id}! Only one handler method is allowed per message ID!";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Represents a type that can be added to and retrieved from messages using the <see cref="Message.AddSerializable{T}(T)"/> and <see cref="Message.GetSerializable{T}"/> methods.</summary>
|
||||
public interface IMessageSerializable
|
||||
{
|
||||
/// <summary>Adds the type to the message.</summary>
|
||||
/// <param name="message">The message to add the type to.</param>
|
||||
void Serialize(Message message);
|
||||
/// <summary>Retrieves the type from the message.</summary>
|
||||
/// <param name="message">The message to retrieve the type from.</param>
|
||||
void Deserialize(Message message);
|
||||
}
|
||||
}
|
||||
1922
Riptide/Message.cs
1922
Riptide/Message.cs
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Specifies a method as the message handler for messages with the given ID.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// In order for a method to qualify as a message handler, it <i>must</i> match a valid message handler method signature. <see cref="Server"/>s
|
||||
/// will only use methods marked with this attribute if they match the <see cref="Server.MessageHandler"/> signature, and <see cref="Client"/>s
|
||||
/// will only use methods marked with this attribute if they match the <see cref="Client.MessageHandler"/> signature.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Methods marked with this attribute which match neither of the valid message handler signatures will not be used by <see cref="Server"/>s
|
||||
/// or <see cref="Client"/>s and will cause warnings at runtime.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If you want a <see cref="Server"/> or <see cref="Client"/> to only use a subset of all message handler methods, you can do so by setting up
|
||||
/// custom message handler groups. Simply set the group ID in the <see cref="MessageHandlerAttribute(ushort, byte)"/> constructor and pass the
|
||||
/// same value to the <see cref="Server.Start(ushort, ushort, byte, bool)"/> or <see cref="Client.Connect(string, int, byte, Message, bool)"/> method. This
|
||||
/// will make that <see cref="Server"/> or <see cref="Client"/> only use message handlers which have the same group ID.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class MessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <summary>The ID of the message type which this method is meant to handle.</summary>
|
||||
public readonly ushort MessageId;
|
||||
/// <summary>The ID of the group of message handlers which this method belongs to.</summary>
|
||||
public readonly byte GroupId;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="MessageHandlerAttribute"/> class with the <paramref name="messageId"/> and <paramref name="groupId"/> values.</summary>
|
||||
/// <param name="messageId">The ID of the message type which this method is meant to handle.</param>
|
||||
/// <param name="groupId">The ID of the group of message handlers which this method belongs to.</param>
|
||||
/// <remarks>
|
||||
/// <see cref="Server"/>s will only use this method if its signature matches the <see cref="Server.MessageHandler"/> signature.
|
||||
/// <see cref="Client"/>s will only use this method if its signature matches the <see cref="Client.MessageHandler"/> signature.
|
||||
/// This method will be ignored if its signature matches neither of the valid message handler signatures.
|
||||
/// </remarks>
|
||||
public MessageHandlerAttribute(ushort messageId, byte groupId = 0)
|
||||
{
|
||||
MessageId = messageId;
|
||||
GroupId = groupId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Provides functionality for enabling/disabling automatic message relaying by message type.</summary>
|
||||
public class MessageRelayFilter
|
||||
{
|
||||
/// <summary>The number of bits an int consists of.</summary>
|
||||
private const int BitsPerInt = sizeof(int) * 8;
|
||||
|
||||
/// <summary>An array storing all the bits which represent whether messages of a given ID should be relayed or not.</summary>
|
||||
private int[] filter;
|
||||
|
||||
/// <summary>Creates a filter of a given size.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
/// <remarks>
|
||||
/// <paramref name="size"/> should be set to the value of the largest message ID, plus 1. For example, if a server will
|
||||
/// handle messages with IDs 1, 2, 3, 7, and 8, <paramref name="size"/> should be set to 9 (8 is the largest possible value,
|
||||
/// and 8 + 1 = 9) despite the fact that there are only 5 unique message IDs the server will ever handle.
|
||||
/// </remarks>
|
||||
public MessageRelayFilter(int size) => Set(size);
|
||||
/// <summary>Creates a filter based on an enum of message IDs.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
public MessageRelayFilter(Type idEnum) => Set(GetSizeFromEnum(idEnum));
|
||||
/// <summary>Creates a filter of a given size and enables relaying for the given message IDs.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
/// <param name="idsToEnable">Message IDs to enable auto relaying for.</param>
|
||||
/// <remarks>
|
||||
/// <paramref name="size"/> should be set to the value of the largest message ID, plus 1. For example, if a server will
|
||||
/// handle messages with IDs 1, 2, 3, 7, and 8, <paramref name="size"/> should be set to 9 (8 is the largest possible value,
|
||||
/// and 8 + 1 = 9) despite the fact that there are only 5 unique message IDs the server will ever handle.
|
||||
/// </remarks>
|
||||
public MessageRelayFilter(int size, params ushort[] idsToEnable)
|
||||
{
|
||||
Set(size);
|
||||
EnableIds(idsToEnable);
|
||||
}
|
||||
/// <summary>Creates a filter based on an enum of message IDs and enables relaying for the given message IDs.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
/// <param name="idsToEnable">Message IDs to enable relaying for.</param>
|
||||
public MessageRelayFilter(Type idEnum, params Enum[] idsToEnable)
|
||||
{
|
||||
Set(GetSizeFromEnum(idEnum));
|
||||
EnableIds(idsToEnable.Cast<ushort>().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Enables auto relaying for the given message IDs.</summary>
|
||||
/// <param name="idsToEnable">Message IDs to enable relaying for.</param>
|
||||
private void EnableIds(ushort[] idsToEnable)
|
||||
{
|
||||
for (int i = 0; i < idsToEnable.Length; i++)
|
||||
EnableRelay(idsToEnable[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calculate the filter size necessary to manage all message IDs in the given enum.</summary>
|
||||
/// <param name="idEnum">The enum type.</param>
|
||||
/// <returns>The appropriate filter size.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="idEnum"/> is not an <see cref="Enum"/>.</exception>
|
||||
private int GetSizeFromEnum(Type idEnum)
|
||||
{
|
||||
if (!idEnum.IsEnum)
|
||||
throw new ArgumentException($"Parameter '{nameof(idEnum)}' must be an enum type!", nameof(idEnum));
|
||||
|
||||
return Enum.GetValues(idEnum).Cast<ushort>().Max() + 1;
|
||||
}
|
||||
|
||||
/// <summary>Sets the filter size.</summary>
|
||||
/// <param name="size">How big to make the filter.</param>
|
||||
private void Set(int size)
|
||||
{
|
||||
filter = new int[size / BitsPerInt + (size % BitsPerInt > 0 ? 1 : 0)];
|
||||
}
|
||||
|
||||
/// <summary>Enables auto relaying for the given message ID.</summary>
|
||||
/// <param name="forMessageId">The message ID to enable relaying for.</param>
|
||||
public void EnableRelay(ushort forMessageId)
|
||||
{
|
||||
filter[forMessageId / BitsPerInt] |= 1 << (forMessageId % BitsPerInt);
|
||||
}
|
||||
/// <inheritdoc cref="EnableRelay(ushort)"/>
|
||||
public void EnableRelay(Enum forMessageId) => EnableRelay((ushort)(object)forMessageId);
|
||||
|
||||
/// <summary>Disables auto relaying for the given message ID.</summary>
|
||||
/// <param name="forMessageId">The message ID to enable relaying for.</param>
|
||||
public void DisableRelay(ushort forMessageId)
|
||||
{
|
||||
filter[forMessageId / BitsPerInt] &= ~(1 << (forMessageId % BitsPerInt));
|
||||
}
|
||||
/// <inheritdoc cref="DisableRelay(ushort)"/>
|
||||
public void DisableRelay(Enum forMessageId) => DisableRelay((ushort)(object)forMessageId);
|
||||
|
||||
/// <summary>Checks whether or not messages with the given ID should be relayed.</summary>
|
||||
/// <param name="forMessageId">The message ID to check.</param>
|
||||
/// <returns>Whether or not messages with the given ID should be relayed.</returns>
|
||||
internal bool ShouldRelay(ushort forMessageId)
|
||||
{
|
||||
return (filter[forMessageId / BitsPerInt] & (1 << (forMessageId % BitsPerInt))) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Riptide/Peer.cs
242
Riptide/Peer.cs
@@ -1,242 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>The reason the connection attempt was rejected.</summary>
|
||||
public enum RejectReason : byte
|
||||
{
|
||||
/// <summary>No response was received from the server (because the client has no internet connection, the server is offline, no server is listening on the target endpoint, etc.).</summary>
|
||||
NoConnection,
|
||||
/// <summary>The client is already connected.</summary>
|
||||
AlreadyConnected,
|
||||
/// <summary>The server is full.</summary>
|
||||
ServerFull,
|
||||
/// <summary>The connection attempt was rejected.</summary>
|
||||
Rejected,
|
||||
/// <summary>The connection attempt was rejected and custom data may have been included with the rejection message.</summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>The reason for a disconnection.</summary>
|
||||
public enum DisconnectReason : byte
|
||||
{
|
||||
/// <summary>No connection was ever established.</summary>
|
||||
NeverConnected,
|
||||
/// <summary>The connection attempt was rejected by the server.</summary>
|
||||
ConnectionRejected,
|
||||
/// <summary>The active transport detected a problem with the connection.</summary>
|
||||
TransportError,
|
||||
/// <summary>The connection timed out.</summary>
|
||||
/// <remarks>
|
||||
/// This also acts as the fallback reason—if a client disconnects and the message containing the <i>real</i> reason is lost
|
||||
/// in transmission, it can't be resent as the connection will have already been closed. As a result, the other end will time
|
||||
/// out the connection after a short period of time and this will be used as the reason.
|
||||
/// </remarks>
|
||||
TimedOut,
|
||||
/// <summary>The client was forcibly disconnected by the server.</summary>
|
||||
Kicked,
|
||||
/// <summary>The server shut down.</summary>
|
||||
ServerStopped,
|
||||
/// <summary>The disconnection was initiated by the client.</summary>
|
||||
Disconnected,
|
||||
/// <summary>The connection's loss and/or resend rates exceeded the maximum acceptable thresholds, or a reliably sent message could not be delivered.</summary>
|
||||
PoorConnection
|
||||
}
|
||||
|
||||
/// <summary>Provides base functionality for <see cref="Server"/> and <see cref="Client"/>.</summary>
|
||||
public abstract class Peer
|
||||
{
|
||||
/// <summary>The name to use when logging messages via <see cref="RiptideLogger"/>.</summary>
|
||||
public readonly string LogName;
|
||||
/// <summary>Sets the relevant connections' <see cref="Connection.TimeoutTime"/>s.</summary>
|
||||
public abstract int TimeoutTime { set; }
|
||||
/// <summary>The interval (in milliseconds) at which to send and expect heartbeats to be received.</summary>
|
||||
/// <remarks>Changes to this value will only take effect after the next heartbeat is executed.</remarks>
|
||||
public int HeartbeatInterval { get; set; } = 1000;
|
||||
|
||||
/// <summary>The number of currently active <see cref="Server"/> and <see cref="Client"/> instances.</summary>
|
||||
internal static int ActiveCount { get; private set; }
|
||||
|
||||
/// <summary>The time (in milliseconds) for which to wait before giving up on a connection attempt.</summary>
|
||||
internal int ConnectTimeoutTime { get; set; } = 10000;
|
||||
/// <summary>The current time.</summary>
|
||||
internal long CurrentTime { get; private set; }
|
||||
|
||||
/// <summary>Whether or not the peer should use the built-in message handler system.</summary>
|
||||
protected bool useMessageHandlers;
|
||||
/// <summary>The default time (in milliseconds) after which to disconnect if no heartbeats are received.</summary>
|
||||
protected int defaultTimeout = 5000;
|
||||
|
||||
/// <summary>A stopwatch used to track how much time has passed.</summary>
|
||||
private readonly System.Diagnostics.Stopwatch time = new System.Diagnostics.Stopwatch();
|
||||
/// <summary>Received messages which need to be handled.</summary>
|
||||
private readonly Queue<MessageToHandle> messagesToHandle = new Queue<MessageToHandle>();
|
||||
/// <summary>A queue of events to execute, ordered by how soon they need to be executed.</summary>
|
||||
private readonly PriorityQueue<DelayedEvent, long> eventQueue = new PriorityQueue<DelayedEvent, long>();
|
||||
|
||||
/// <summary>Initializes the peer.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Peer(string logName)
|
||||
{
|
||||
LogName = logName;
|
||||
}
|
||||
|
||||
/// <summary>Retrieves methods marked with <see cref="MessageHandlerAttribute"/>.</summary>
|
||||
/// <returns>An array containing message handler methods.</returns>
|
||||
protected MethodInfo[] FindMessageHandlers()
|
||||
{
|
||||
return new MethodInfo[0];
|
||||
/*string thisAssemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a
|
||||
.GetReferencedAssemblies()
|
||||
.Any(n => n.FullName == thisAssemblyName)) // Get only assemblies that reference this assembly
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) // Include instance methods in the search so we can show the developer an error instead of silently not adding instance methods to the dictionary
|
||||
.Where(m => m.GetCustomAttributes(typeof(MessageHandlerAttribute), false).Length > 0)
|
||||
.ToArray();*/
|
||||
}
|
||||
|
||||
/// <summary>Builds a dictionary of message IDs and their corresponding message handler methods.</summary>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to include in the dictionary.</param>
|
||||
protected abstract void CreateMessageHandlersDictionary(byte messageHandlerGroupId);
|
||||
|
||||
/// <summary>Starts tracking how much time has passed.</summary>
|
||||
protected void StartTime()
|
||||
{
|
||||
CurrentTime = 0;
|
||||
time.Restart();
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking how much time has passed.</summary>
|
||||
protected void StopTime()
|
||||
{
|
||||
CurrentTime = 0;
|
||||
time.Reset();
|
||||
eventQueue.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Beats the heart.</summary>
|
||||
internal abstract void Heartbeat();
|
||||
|
||||
/// <summary>Handles any received messages and invokes any delayed events which need to be invoked.</summary>
|
||||
public virtual void Update()
|
||||
{
|
||||
CurrentTime = time.ElapsedMilliseconds;
|
||||
|
||||
while (eventQueue.Count > 0 && eventQueue.PeekPriority() <= CurrentTime)
|
||||
eventQueue.Dequeue().Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Sets up a delayed event to be executed after the given time has passed.</summary>
|
||||
/// <param name="inMS">How long from now to execute the delayed event, in milliseconds.</param>
|
||||
/// <param name="delayedEvent">The delayed event to execute later.</param>
|
||||
internal void ExecuteLater(long inMS, DelayedEvent delayedEvent)
|
||||
{
|
||||
eventQueue.Enqueue(delayedEvent, CurrentTime + inMS);
|
||||
}
|
||||
|
||||
/// <summary>Handles all queued messages.</summary>
|
||||
protected void HandleMessages()
|
||||
{
|
||||
while (messagesToHandle.Count > 0)
|
||||
{
|
||||
MessageToHandle handle = messagesToHandle.Dequeue();
|
||||
Handle(handle.Message, handle.Header, handle.FromConnection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles data received by the transport.</summary>
|
||||
protected void HandleData(object _, DataReceivedEventArgs e)
|
||||
{
|
||||
Message message = Message.Create().Init(e.DataBuffer[0], e.Amount, out MessageHeader header);
|
||||
|
||||
if (message.SendMode == MessageSendMode.Notify)
|
||||
{
|
||||
if (e.Amount < Message.MinNotifyBytes)
|
||||
return;
|
||||
|
||||
e.FromConnection.ProcessNotify(e.DataBuffer, e.Amount, message);
|
||||
}
|
||||
else if (message.SendMode == MessageSendMode.Unreliable)
|
||||
{
|
||||
if (e.Amount > Message.MinUnreliableBytes)
|
||||
Buffer.BlockCopy(e.DataBuffer, 1, message.Data, 1, e.Amount - 1);
|
||||
|
||||
messagesToHandle.Enqueue(new MessageToHandle(message, header, e.FromConnection));
|
||||
e.FromConnection.Metrics.ReceivedUnreliable(e.Amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (e.Amount < Message.MinReliableBytes)
|
||||
return;
|
||||
|
||||
e.FromConnection.Metrics.ReceivedReliable(e.Amount);
|
||||
if (e.FromConnection.ShouldHandle(Converter.UShortFromBits(e.DataBuffer, Message.HeaderBits)))
|
||||
{
|
||||
Buffer.BlockCopy(e.DataBuffer, 1, message.Data, 1, e.Amount - 1);
|
||||
messagesToHandle.Enqueue(new MessageToHandle(message, header, e.FromConnection));
|
||||
}
|
||||
else
|
||||
e.FromConnection.Metrics.ReliableDiscarded++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles a message.</summary>
|
||||
/// <param name="message">The message to handle.</param>
|
||||
/// <param name="header">The message's header type.</param>
|
||||
/// <param name="connection">The connection which the message was received on.</param>
|
||||
protected abstract void Handle(Message message, MessageHeader header, Connection connection);
|
||||
|
||||
/// <summary>Disconnects the connection in question. Necessary for connections to be able to initiate disconnections (like in the case of poor connection quality).</summary>
|
||||
/// <param name="connection">The connection to disconnect.</param>
|
||||
/// <param name="reason">The reason why the connection is being disconnected.</param>
|
||||
internal abstract void Disconnect(Connection connection, DisconnectReason reason);
|
||||
|
||||
/// <summary>Increases <see cref="ActiveCount"/>. For use when a new <see cref="Server"/> or <see cref="Client"/> is started.</summary>
|
||||
protected static void IncreaseActiveCount()
|
||||
{
|
||||
ActiveCount++;
|
||||
}
|
||||
|
||||
/// <summary>Decreases <see cref="ActiveCount"/>. For use when a <see cref="Server"/> or <see cref="Client"/> is stopped.</summary>
|
||||
protected static void DecreaseActiveCount()
|
||||
{
|
||||
ActiveCount--;
|
||||
if (ActiveCount < 0)
|
||||
ActiveCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stores information about a message that needs to be handled.</summary>
|
||||
internal struct MessageToHandle
|
||||
{
|
||||
/// <summary>The message that needs to be handled.</summary>
|
||||
internal readonly Message Message;
|
||||
/// <summary>The message's header type.</summary>
|
||||
internal readonly MessageHeader Header;
|
||||
/// <summary>The connection on which the message was received.</summary>
|
||||
internal readonly Connection FromConnection;
|
||||
|
||||
/// <summary>Handles initialization.</summary>
|
||||
/// <param name="message">The message that needs to be handled.</param>
|
||||
/// <param name="header">The message's header type.</param>
|
||||
/// <param name="fromConnection">The connection on which the message was received.</param>
|
||||
public MessageToHandle(Message message, MessageHeader header, Connection fromConnection)
|
||||
{
|
||||
Message = message;
|
||||
Header = header;
|
||||
FromConnection = fromConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>Represents a currently pending reliably sent message whose delivery has not been acknowledged yet.</summary>
|
||||
internal class PendingMessage
|
||||
{
|
||||
/// <summary>The time of the latest send attempt.</summary>
|
||||
internal long LastSendTime { get; private set; }
|
||||
|
||||
/// <summary>The multiplier used to determine how long to wait before resending a pending message.</summary>
|
||||
private const float RetryTimeMultiplier = 1.2f;
|
||||
|
||||
/// <summary>A pool of reusable <see cref="PendingMessage"/> instances.</summary>
|
||||
private static readonly List<PendingMessage> pool = new List<PendingMessage>();
|
||||
|
||||
/// <summary>The <see cref="Connection"/> to use to send (and resend) the pending message.</summary>
|
||||
private Connection connection;
|
||||
/// <summary>The contents of the message.</summary>
|
||||
private readonly byte[] data;
|
||||
/// <summary>The length in bytes of the message.</summary>
|
||||
private int size;
|
||||
/// <summary>How many send attempts have been made so far.</summary>
|
||||
private byte sendAttempts;
|
||||
/// <summary>Whether the pending message has been cleared or not.</summary>
|
||||
private bool wasCleared;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
internal PendingMessage()
|
||||
{
|
||||
data = new byte[Message.MaxSize];
|
||||
}
|
||||
|
||||
#region Pooling
|
||||
/// <summary>Retrieves a <see cref="PendingMessage"/> instance and initializes it.</summary>
|
||||
/// <param name="sequenceId">The sequence ID of the message.</param>
|
||||
/// <param name="message">The message that is being sent reliably.</param>
|
||||
/// <param name="connection">The <see cref="Connection"/> to use to send (and resend) the pending message.</param>
|
||||
/// <returns>An intialized <see cref="PendingMessage"/> instance.</returns>
|
||||
internal static PendingMessage Create(ushort sequenceId, Message message, Connection connection)
|
||||
{
|
||||
PendingMessage pendingMessage = RetrieveFromPool();
|
||||
pendingMessage.connection = connection;
|
||||
|
||||
message.SetBits(sequenceId, sizeof(ushort) * Converter.BitsPerByte, Message.HeaderBits);
|
||||
pendingMessage.size = message.BytesInUse;
|
||||
Buffer.BlockCopy(message.Data, 0, pendingMessage.data, 0, pendingMessage.size);
|
||||
|
||||
pendingMessage.sendAttempts = 0;
|
||||
pendingMessage.wasCleared = false;
|
||||
return pendingMessage;
|
||||
}
|
||||
|
||||
/// <summary>Retrieves a <see cref="PendingMessage"/> instance from the pool. If none is available, a new instance is created.</summary>
|
||||
/// <returns>A <see cref="PendingMessage"/> instance.</returns>
|
||||
private static PendingMessage RetrieveFromPool()
|
||||
{
|
||||
PendingMessage message;
|
||||
if (pool.Count > 0)
|
||||
{
|
||||
message = pool[0];
|
||||
pool.RemoveAt(0);
|
||||
}
|
||||
else
|
||||
message = new PendingMessage();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>Empties the pool. Does not affect <see cref="PendingMessage"/> instances which are actively pending and therefore not in the pool.</summary>
|
||||
public static void ClearPool()
|
||||
{
|
||||
pool.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Returns the <see cref="PendingMessage"/> instance to the pool so it can be reused.</summary>
|
||||
private void Release()
|
||||
{
|
||||
if (!pool.Contains(this))
|
||||
pool.Add(this); // Only add it if it's not already in the list, otherwise this method being called twice in a row for whatever reason could cause *serious* issues
|
||||
|
||||
// TODO: consider doing something to decrease pool capacity if there are far more
|
||||
// available instance than are needed, which could occur if a large burst of
|
||||
// messages has to be sent for some reason
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>Resends the message.</summary>
|
||||
internal void RetrySend()
|
||||
{
|
||||
if (!wasCleared)
|
||||
{
|
||||
long time = connection.Peer.CurrentTime;
|
||||
if (LastSendTime + (connection.SmoothRTT < 0 ? 25 : connection.SmoothRTT / 2) <= time) // Avoid triggering a resend if the latest resend was less than half a RTT ago
|
||||
TrySend();
|
||||
else
|
||||
connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, time));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Attempts to send the message.</summary>
|
||||
internal void TrySend()
|
||||
{
|
||||
if (sendAttempts >= connection.MaxSendAttempts && connection.CanQualityDisconnect)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, connection.Peer.LogName, $"Could not guarantee delivery of a {(MessageHeader)(data[0] & Message.HeaderBitmask)} message after {sendAttempts} attempts! Disconnecting...");
|
||||
connection.Peer.Disconnect(connection, DisconnectReason.PoorConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.Send(data, size);
|
||||
connection.Metrics.SentReliable(size);
|
||||
|
||||
LastSendTime = connection.Peer.CurrentTime;
|
||||
sendAttempts++;
|
||||
|
||||
connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, connection.Peer.CurrentTime));
|
||||
}
|
||||
|
||||
/// <summary>Clears the message.</summary>
|
||||
internal void Clear()
|
||||
{
|
||||
connection.Metrics.RollingReliableSends.Add(sendAttempts);
|
||||
wasCleared = true;
|
||||
Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Riptide
|
||||
{
|
||||
/// <summary>A server that can accept connections from <see cref="Client"/>s.</summary>
|
||||
public class Server : Peer
|
||||
{
|
||||
/// <summary>Invoked when a client connects.</summary>
|
||||
public event EventHandler<ServerConnectedEventArgs> ClientConnected;
|
||||
/// <summary>Invoked when a connection fails to be fully established.</summary>
|
||||
public event EventHandler<ServerConnectionFailedEventArgs> ConnectionFailed;
|
||||
/// <summary>Invoked when a message is received.</summary>
|
||||
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
||||
/// <summary>Invoked when a client disconnects.</summary>
|
||||
public event EventHandler<ServerDisconnectedEventArgs> ClientDisconnected;
|
||||
|
||||
/// <summary>Whether or not the server is currently running.</summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
/// <summary>The local port that the server is running on.</summary>
|
||||
public ushort Port => transport.Port;
|
||||
/// <summary>Sets the default timeout time for future connections and updates the <see cref="Connection.TimeoutTime"/> of all connected clients.</summary>
|
||||
public override int TimeoutTime
|
||||
{
|
||||
set
|
||||
{
|
||||
defaultTimeout = value;
|
||||
foreach (Connection connection in clients.Values)
|
||||
connection.TimeoutTime = defaultTimeout;
|
||||
}
|
||||
}
|
||||
/// <summary>The maximum number of concurrent connections.</summary>
|
||||
public ushort MaxClientCount { get; private set; }
|
||||
/// <summary>The number of currently connected clients.</summary>
|
||||
public int ClientCount => clients.Count;
|
||||
/// <summary>An array of all the currently connected clients.</summary>
|
||||
/// <remarks>The position of each <see cref="Connection"/> instance in the array does <i>not</i> correspond to that client's numeric ID (except by coincidence).</remarks>
|
||||
public Connection[] Clients => clients.Values.ToArray();
|
||||
/// <summary>Encapsulates a method that handles a message from a client.</summary>
|
||||
/// <param name="fromClientId">The numeric ID of the client from whom the message was received.</param>
|
||||
/// <param name="message">The message that was received.</param>
|
||||
public delegate void MessageHandler(ushort fromClientId, Message message);
|
||||
/// <summary>Encapsulates a method that determines whether or not to accept a client's connection attempt.</summary>
|
||||
public delegate void ConnectionAttemptHandler(Connection pendingConnection, Message connectMessage);
|
||||
/// <summary>An optional method which determines whether or not to accept a client's connection attempt.</summary>
|
||||
/// <remarks>The <see cref="Connection"/> parameter is the pending connection and the <see cref="Message"/> parameter is a message containing any additional data the
|
||||
/// client included with the connection attempt. If you choose to subscribe a method to this delegate, you should use it to call either <see cref="Accept(Connection)"/>
|
||||
/// or <see cref="Reject(Connection, Message)"/>. Not doing so will result in the connection hanging until the client times out.</remarks>
|
||||
public ConnectionAttemptHandler HandleConnection;
|
||||
/// <summary>Stores which message IDs have auto relaying enabled. Relaying is disabled entirely when this is <see langword="null"/>.</summary>
|
||||
public MessageRelayFilter RelayFilter;
|
||||
|
||||
/// <summary>Currently pending connections which are waiting to be accepted or rejected.</summary>
|
||||
private readonly List<Connection> pendingConnections;
|
||||
/// <summary>Currently connected clients.</summary>
|
||||
private Dictionary<ushort, Connection> clients;
|
||||
/// <summary>Clients that have timed out and need to be removed from <see cref="clients"/>.</summary>
|
||||
private readonly List<Connection> timedOutClients;
|
||||
/// <summary>Methods used to handle messages, accessible by their corresponding message IDs.</summary>
|
||||
private Dictionary<ushort, MessageHandler> messageHandlers;
|
||||
/// <summary>The underlying transport's server that is used for sending and receiving data.</summary>
|
||||
private IServer transport;
|
||||
/// <summary>All currently unused client IDs.</summary>
|
||||
private Queue<ushort> availableClientIds;
|
||||
|
||||
/// <summary>Handles initial setup.</summary>
|
||||
/// <param name="transport">The transport to use for sending and receiving data.</param>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Server(IServer transport, string logName = "SERVER") : base(logName)
|
||||
{
|
||||
this.transport = transport;
|
||||
pendingConnections = new List<Connection>();
|
||||
clients = new Dictionary<ushort, Connection>();
|
||||
timedOutClients = new List<Connection>();
|
||||
}
|
||||
/// <summary>Handles initial setup using the built-in UDP transport.</summary>
|
||||
/// <param name="logName">The name to use when logging messages via <see cref="RiptideLogger"/>.</param>
|
||||
public Server(string logName = "SERVER") : this(new Transports.Udp.UdpServer(), logName) { }
|
||||
|
||||
/// <summary>Stops the server if it's running and swaps out the transport it's using.</summary>
|
||||
/// <param name="newTransport">The new underlying transport server to use for sending and receiving data.</param>
|
||||
/// <remarks>This method does not automatically restart the server. To continue accepting connections, <see cref="Start(ushort, ushort, byte, bool)"/> must be called again.</remarks>
|
||||
public void ChangeTransport(IServer newTransport)
|
||||
{
|
||||
Stop();
|
||||
transport = newTransport;
|
||||
}
|
||||
|
||||
/// <summary>Starts the server.</summary>
|
||||
/// <param name="port">The local port on which to start the server.</param>
|
||||
/// <param name="maxClientCount">The maximum number of concurrent connections to allow.</param>
|
||||
/// <param name="messageHandlerGroupId">The ID of the group of message handler methods to use when building <see cref="messageHandlers"/>.</param>
|
||||
/// <param name="useMessageHandlers">Whether or not the server should use the built-in message handler system.</param>
|
||||
/// <remarks>Setting <paramref name="useMessageHandlers"/> to <see langword="false"/> will disable the automatic detection and execution of methods with the <see cref="MessageHandlerAttribute"/>, which is beneficial if you prefer to handle messages via the <see cref="MessageReceived"/> event.</remarks>
|
||||
public void Start(ushort port, ushort maxClientCount, byte messageHandlerGroupId = 0, bool useMessageHandlers = true)
|
||||
{
|
||||
Stop();
|
||||
|
||||
IncreaseActiveCount();
|
||||
this.useMessageHandlers = useMessageHandlers;
|
||||
if (useMessageHandlers)
|
||||
CreateMessageHandlersDictionary(messageHandlerGroupId);
|
||||
|
||||
MaxClientCount = maxClientCount;
|
||||
clients = new Dictionary<ushort, Connection>(maxClientCount);
|
||||
InitializeClientIds();
|
||||
|
||||
SubToTransportEvents();
|
||||
transport.Start(port);
|
||||
|
||||
StartTime();
|
||||
Heartbeat();
|
||||
IsRunning = true;
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Started on port {port}.");
|
||||
}
|
||||
|
||||
/// <summary>Subscribes appropriate methods to the transport's events.</summary>
|
||||
private void SubToTransportEvents()
|
||||
{
|
||||
transport.Connected += HandleConnectionAttempt;
|
||||
transport.DataReceived += HandleData;
|
||||
transport.Disconnected += TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes methods from all of the transport's events.</summary>
|
||||
private void UnsubFromTransportEvents()
|
||||
{
|
||||
transport.Connected -= HandleConnectionAttempt;
|
||||
transport.DataReceived -= HandleData;
|
||||
transport.Disconnected -= TransportDisconnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreateMessageHandlersDictionary(byte messageHandlerGroupId)
|
||||
{
|
||||
MethodInfo[] methods = FindMessageHandlers();
|
||||
|
||||
messageHandlers = new Dictionary<ushort, MessageHandler>(methods.Length);
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
MessageHandlerAttribute attribute = method.GetCustomAttribute<MessageHandlerAttribute>();
|
||||
if (attribute.GroupId != messageHandlerGroupId)
|
||||
continue;
|
||||
|
||||
if (!method.IsStatic)
|
||||
throw new NonStaticHandlerException(method.DeclaringType, method.Name);
|
||||
|
||||
Delegate serverMessageHandler = Delegate.CreateDelegate(typeof(MessageHandler), method, false);
|
||||
if (serverMessageHandler != null)
|
||||
{
|
||||
// It's a message handler for Server instances
|
||||
if (messageHandlers.ContainsKey(attribute.MessageId))
|
||||
{
|
||||
MethodInfo otherMethodWithId = messageHandlers[attribute.MessageId].GetMethodInfo();
|
||||
throw new DuplicateHandlerException(attribute.MessageId, method, otherMethodWithId);
|
||||
}
|
||||
else
|
||||
messageHandlers.Add(attribute.MessageId, (MessageHandler)serverMessageHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's not a message handler for Server instances, but it might be one for Client instances
|
||||
if (Delegate.CreateDelegate(typeof(Client.MessageHandler), method, false) == null)
|
||||
throw new InvalidHandlerSignatureException(method.DeclaringType, method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles an incoming connection attempt.</summary>
|
||||
private void HandleConnectionAttempt(object _, ConnectedEventArgs e)
|
||||
{
|
||||
e.Connection.Initialize(this, defaultTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Handles a connect message.</summary>
|
||||
/// <param name="connection">The client that sent the connect message.</param>
|
||||
/// <param name="connectMessage">The connect message.</param>
|
||||
private void HandleConnect(Connection connection, Message connectMessage)
|
||||
{
|
||||
connection.SetPending();
|
||||
|
||||
if (HandleConnection == null)
|
||||
AcceptConnection(connection);
|
||||
else if (ClientCount < MaxClientCount)
|
||||
{
|
||||
if (!clients.ContainsValue(connection) && !pendingConnections.Contains(connection))
|
||||
{
|
||||
pendingConnections.Add(connection);
|
||||
Send(Message.Create(MessageHeader.Connect), connection); // Inform the client we've received the connection attempt
|
||||
HandleConnection(connection, connectMessage); // Externally determines whether to accept
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.AlreadyConnected);
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.ServerFull);
|
||||
}
|
||||
|
||||
/// <summary>Accepts the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to accept.</param>
|
||||
public void Accept(Connection connection)
|
||||
{
|
||||
if (pendingConnections.Remove(connection))
|
||||
AcceptConnection(connection);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't accept connection from {connection} because no such connection was pending!");
|
||||
}
|
||||
|
||||
/// <summary>Rejects the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to reject.</param>
|
||||
/// <param name="message">Data that should be sent to the client being rejected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void Reject(Connection connection, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting rejection data!");
|
||||
|
||||
if (pendingConnections.Remove(connection))
|
||||
Reject(connection, message == null ? RejectReason.Rejected : RejectReason.Custom, message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't reject connection from {connection} because no such connection was pending!");
|
||||
}
|
||||
|
||||
/// <summary>Accepts the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to accept.</param>
|
||||
private void AcceptConnection(Connection connection)
|
||||
{
|
||||
if (ClientCount < MaxClientCount)
|
||||
{
|
||||
if (!clients.ContainsValue(connection))
|
||||
{
|
||||
ushort clientId = GetAvailableClientId();
|
||||
connection.Id = clientId;
|
||||
clients.Add(clientId, connection);
|
||||
connection.ResetTimeout();
|
||||
connection.SendWelcome();
|
||||
return;
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.AlreadyConnected);
|
||||
}
|
||||
else
|
||||
Reject(connection, RejectReason.ServerFull);
|
||||
}
|
||||
|
||||
/// <summary>Rejects the given pending connection.</summary>
|
||||
/// <param name="connection">The connection to reject.</param>
|
||||
/// <param name="reason">The reason why the connection is being rejected.</param>
|
||||
/// <param name="rejectMessage">Data that should be sent to the client being rejected.</param>
|
||||
private void Reject(Connection connection, RejectReason reason, Message rejectMessage = null)
|
||||
{
|
||||
if (reason != RejectReason.AlreadyConnected)
|
||||
{
|
||||
// Sending a reject message about the client already being connected could theoretically be exploited to obtain information
|
||||
// on other connected clients, although in practice that seems very unlikely. However, under normal circumstances, clients
|
||||
// should never actually encounter a scenario where they are "already connected".
|
||||
|
||||
Message message = Message.Create(MessageHeader.Reject);
|
||||
message.AddByte((byte)reason);
|
||||
if (reason == RejectReason.Custom)
|
||||
message.AddMessage(rejectMessage);
|
||||
|
||||
for (int i = 0; i < 3; i++) // Send the rejection message a few times to increase the odds of it arriving
|
||||
connection.Send(message, false);
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
connection.ResetTimeout(); // Keep the connection alive for a moment so the same client can't immediately attempt to connect again
|
||||
connection.LocalDisconnect();
|
||||
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Rejected connection from {connection}: {Helper.GetReasonString(reason)}.");
|
||||
}
|
||||
|
||||
/// <summary>Checks if clients have timed out.</summary>
|
||||
internal override void Heartbeat()
|
||||
{
|
||||
foreach (Connection connection in clients.Values)
|
||||
if (connection.HasTimedOut)
|
||||
timedOutClients.Add(connection);
|
||||
|
||||
foreach (Connection connection in pendingConnections)
|
||||
if (connection.HasConnectAttemptTimedOut)
|
||||
timedOutClients.Add(connection);
|
||||
|
||||
foreach (Connection connection in timedOutClients)
|
||||
LocalDisconnect(connection, DisconnectReason.TimedOut);
|
||||
|
||||
timedOutClients.Clear();
|
||||
|
||||
ExecuteLater(HeartbeatInterval, new HeartbeatEvent(this));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
transport.Poll();
|
||||
HandleMessages();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Handle(Message message, MessageHeader header, Connection connection)
|
||||
{
|
||||
switch (header)
|
||||
{
|
||||
// User messages
|
||||
case MessageHeader.Unreliable:
|
||||
case MessageHeader.Reliable:
|
||||
OnMessageReceived(message, connection);
|
||||
break;
|
||||
|
||||
// Internal messages
|
||||
case MessageHeader.Ack:
|
||||
connection.HandleAck(message);
|
||||
break;
|
||||
case MessageHeader.Connect:
|
||||
HandleConnect(connection, message);
|
||||
break;
|
||||
case MessageHeader.Heartbeat:
|
||||
connection.HandleHeartbeat(message);
|
||||
break;
|
||||
case MessageHeader.Disconnect:
|
||||
LocalDisconnect(connection, DisconnectReason.Disconnected);
|
||||
break;
|
||||
case MessageHeader.Welcome:
|
||||
if (connection.HandleWelcomeResponse(message))
|
||||
OnClientConnected(connection);
|
||||
break;
|
||||
default:
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Unexpected message header '{header}'! Discarding {message.BytesInUse} bytes received from {connection}.");
|
||||
break;
|
||||
}
|
||||
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Sends a message to a given client.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="toClient">The numeric ID of the client to send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void Send(Message message, ushort toClient, bool shouldRelease = true)
|
||||
{
|
||||
if (clients.TryGetValue(toClient, out Connection connection))
|
||||
Send(message, connection, shouldRelease);
|
||||
}
|
||||
/// <summary>Sends a message to a given client.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="toClient">The client to send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public ushort Send(Message message, Connection toClient, bool shouldRelease = true) => toClient.Send(message, shouldRelease);
|
||||
|
||||
/// <summary>Sends a message to all connected clients.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void SendToAll(Message message, bool shouldRelease = true)
|
||||
{
|
||||
foreach (Connection client in clients.Values)
|
||||
client.Send(message, false);
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
}
|
||||
/// <summary>Sends a message to all connected clients except the given one.</summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="exceptToClientId">The numeric ID of the client to <i>not</i> send the message to.</param>
|
||||
/// <param name="shouldRelease">Whether or not to return the message to the pool after it is sent.</param>
|
||||
/// <inheritdoc cref="Connection.Send(Message, bool)"/>
|
||||
public void SendToAll(Message message, ushort exceptToClientId, bool shouldRelease = true)
|
||||
{
|
||||
foreach (Connection client in clients.Values)
|
||||
if (client.Id != exceptToClientId)
|
||||
client.Send(message, false);
|
||||
|
||||
if (shouldRelease)
|
||||
message.Release();
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the client with the given ID, if a client with that ID is currently connected.</summary>
|
||||
/// <param name="id">The ID of the client to retrieve.</param>
|
||||
/// <param name="client">The retrieved client.</param>
|
||||
/// <returns><see langword="true"/> if a client with the given ID was connected; otherwise <see langword="false"/>.</returns>
|
||||
public bool TryGetClient(ushort id, out Connection client) => clients.TryGetValue(id, out client);
|
||||
|
||||
/// <summary>Disconnects a specific client.</summary>
|
||||
/// <param name="id">The numeric ID of the client to disconnect.</param>
|
||||
/// <param name="message">Data that should be sent to the client being disconnected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void DisconnectClient(ushort id, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting disconnection data!");
|
||||
|
||||
if (clients.TryGetValue(id, out Connection client))
|
||||
{
|
||||
SendDisconnect(client, DisconnectReason.Kicked, message);
|
||||
LocalDisconnect(client, DisconnectReason.Kicked);
|
||||
}
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't disconnect client {id} because it wasn't connected!");
|
||||
}
|
||||
|
||||
/// <summary>Disconnects the given client.</summary>
|
||||
/// <param name="client">The client to disconnect.</param>
|
||||
/// <param name="message">Data that should be sent to the client being disconnected. Use <see cref="Message.Create()"/> to get an empty message instance.</param>
|
||||
public void DisconnectClient(Connection client, Message message = null)
|
||||
{
|
||||
if (message != null && message.ReadBits != 0)
|
||||
RiptideLogger.Log(LogType.Error, LogName, $"Use the parameterless 'Message.Create()' overload when setting disconnection data!");
|
||||
|
||||
if (clients.ContainsKey(client.Id))
|
||||
{
|
||||
SendDisconnect(client, DisconnectReason.Kicked, message);
|
||||
LocalDisconnect(client, DisconnectReason.Kicked);
|
||||
}
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"Couldn't disconnect client {client.Id} because it wasn't connected!");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Disconnect(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
if (connection.IsConnected && connection.CanQualityDisconnect)
|
||||
LocalDisconnect(connection, reason);
|
||||
}
|
||||
|
||||
/// <summary>Cleans up the local side of the given connection.</summary>
|
||||
/// <param name="client">The client to disconnect.</param>
|
||||
/// <param name="reason">The reason why the client is being disconnected.</param>
|
||||
private void LocalDisconnect(Connection client, DisconnectReason reason)
|
||||
{
|
||||
if (client.Peer != this)
|
||||
return; // Client does not belong to this Server instance
|
||||
|
||||
transport.Close(client);
|
||||
|
||||
if (clients.Remove(client.Id))
|
||||
availableClientIds.Enqueue(client.Id);
|
||||
|
||||
if (client.IsConnected)
|
||||
OnClientDisconnected(client, reason); // Only run if the client was ever actually connected
|
||||
else if (client.IsPending)
|
||||
OnConnectionFailed(client);
|
||||
|
||||
client.LocalDisconnect();
|
||||
}
|
||||
|
||||
/// <summary>What to do when the transport disconnects a client.</summary>
|
||||
private void TransportDisconnected(object sender, Transports.DisconnectedEventArgs e)
|
||||
{
|
||||
LocalDisconnect(e.Connection, e.Reason);
|
||||
}
|
||||
|
||||
/// <summary>Stops the server.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (!IsRunning)
|
||||
return;
|
||||
|
||||
pendingConnections.Clear();
|
||||
byte[] disconnectBytes = { (byte)MessageHeader.Disconnect, (byte)DisconnectReason.ServerStopped };
|
||||
foreach (Connection client in clients.Values)
|
||||
client.Send(disconnectBytes, disconnectBytes.Length);
|
||||
clients.Clear();
|
||||
|
||||
transport.Shutdown();
|
||||
UnsubFromTransportEvents();
|
||||
|
||||
DecreaseActiveCount();
|
||||
|
||||
StopTime();
|
||||
IsRunning = false;
|
||||
RiptideLogger.Log(LogType.Info, LogName, "Server stopped.");
|
||||
}
|
||||
|
||||
/// <summary>Initializes available client IDs.</summary>
|
||||
private void InitializeClientIds()
|
||||
{
|
||||
if (MaxClientCount > ushort.MaxValue - 1)
|
||||
throw new Exception($"A server's max client count may not exceed {ushort.MaxValue - 1}!");
|
||||
|
||||
availableClientIds = new Queue<ushort>(MaxClientCount);
|
||||
for (ushort i = 1; i <= MaxClientCount; i++)
|
||||
availableClientIds.Enqueue(i);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves an available client ID.</summary>
|
||||
/// <returns>The client ID. 0 if none were available.</returns>
|
||||
private ushort GetAvailableClientId()
|
||||
{
|
||||
if (availableClientIds.Count > 0)
|
||||
return availableClientIds.Dequeue();
|
||||
|
||||
RiptideLogger.Log(LogType.Error, LogName, "No available client IDs, assigned 0!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#region Messages
|
||||
/// <summary>Sends a disconnect message.</summary>
|
||||
/// <param name="client">The client to send the disconnect message to.</param>
|
||||
/// <param name="reason">Why the client is being disconnected.</param>
|
||||
/// <param name="disconnectMessage">Optional custom data that should be sent to the client being disconnected.</param>
|
||||
private void SendDisconnect(Connection client, DisconnectReason reason, Message disconnectMessage)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.Disconnect);
|
||||
message.AddByte((byte)reason);
|
||||
|
||||
if (reason == DisconnectReason.Kicked && disconnectMessage != null)
|
||||
message.AddMessage(disconnectMessage);
|
||||
|
||||
Send(message, client);
|
||||
}
|
||||
|
||||
/// <summary>Sends a client connected message.</summary>
|
||||
/// <param name="newClient">The newly connected client.</param>
|
||||
private void SendClientConnected(Connection newClient)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.ClientConnected);
|
||||
message.AddUShort(newClient.Id);
|
||||
|
||||
SendToAll(message, newClient.Id);
|
||||
}
|
||||
|
||||
/// <summary>Sends a client disconnected message.</summary>
|
||||
/// <param name="id">The numeric ID of the client that disconnected.</param>
|
||||
private void SendClientDisconnected(ushort id)
|
||||
{
|
||||
Message message = Message.Create(MessageHeader.ClientDisconnected);
|
||||
message.AddUShort(id);
|
||||
|
||||
SendToAll(message);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>Invokes the <see cref="ClientConnected"/> event.</summary>
|
||||
/// <param name="client">The newly connected client.</param>
|
||||
protected virtual void OnClientConnected(Connection client)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {client.Id} ({client}) connected successfully!");
|
||||
SendClientConnected(client);
|
||||
ClientConnected?.Invoke(this, new ServerConnectedEventArgs(client));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
/// <param name="connection">The connection that failed to be fully established.</param>
|
||||
protected virtual void OnConnectionFailed(Connection connection)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {connection} stopped responding before the connection was fully established!");
|
||||
ConnectionFailed?.Invoke(this, new ServerConnectionFailedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="MessageReceived"/> event and initiates handling of the received message.</summary>
|
||||
/// <param name="message">The received message.</param>
|
||||
/// <param name="fromConnection">The client from which the message was received.</param>
|
||||
protected virtual void OnMessageReceived(Message message, Connection fromConnection)
|
||||
{
|
||||
ushort messageId = (ushort)message.GetVarULong();
|
||||
if (RelayFilter != null && RelayFilter.ShouldRelay(messageId))
|
||||
{
|
||||
// The message should be automatically relayed to clients instead of being handled on the server
|
||||
SendToAll(message, fromConnection.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(fromConnection, messageId, message));
|
||||
|
||||
if (useMessageHandlers)
|
||||
{
|
||||
if (messageHandlers.TryGetValue(messageId, out MessageHandler messageHandler))
|
||||
messageHandler(fromConnection.Id, message);
|
||||
else
|
||||
RiptideLogger.Log(LogType.Warning, LogName, $"No message handler method found for message ID {messageId}!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ClientDisconnected"/> event.</summary>
|
||||
/// <param name="connection">The client that disconnected.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected virtual void OnClientDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
RiptideLogger.Log(LogType.Info, LogName, $"Client {connection.Id} ({connection}) disconnected: {Helper.GetReasonString(reason)}.");
|
||||
SendClientDisconnected(connection.Id);
|
||||
ClientDisconnected?.Invoke(this, new ServerDisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Contains event data for when a server's transport successfully establishes a connection to a client.</summary>
|
||||
public class ConnectedEventArgs
|
||||
{
|
||||
/// <summary>The newly established connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The newly established connection.</param>
|
||||
public ConnectedEventArgs(Connection connection)
|
||||
{
|
||||
Connection = connection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport receives data.</summary>
|
||||
public class DataReceivedEventArgs
|
||||
{
|
||||
/// <summary>An array containing the received data.</summary>
|
||||
public readonly byte[] DataBuffer;
|
||||
/// <summary>The number of bytes that were received.</summary>
|
||||
public readonly int Amount;
|
||||
/// <summary>The connection which the data was received from.</summary>
|
||||
public readonly Connection FromConnection;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="dataBuffer">An array containing the received data.</param>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="fromConnection">The connection which the data was received from.</param>
|
||||
public DataReceivedEventArgs(byte[] dataBuffer, int amount, Connection fromConnection)
|
||||
{
|
||||
DataBuffer = dataBuffer;
|
||||
Amount = amount;
|
||||
FromConnection = fromConnection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Contains event data for when a server's or client's transport initiates or detects a disconnection.</summary>
|
||||
public class DisconnectedEventArgs
|
||||
{
|
||||
/// <summary>The closed connection.</summary>
|
||||
public readonly Connection Connection;
|
||||
/// <summary>The reason for the disconnection.</summary>
|
||||
public readonly DisconnectReason Reason;
|
||||
|
||||
/// <summary>Initializes event data.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
public DisconnectedEventArgs(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Connection = connection;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's client must implement.</summary>
|
||||
public interface IClient : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler Connected;
|
||||
/// <summary>Invoked when a connection attempt fails at the transport level.</summary>
|
||||
event EventHandler ConnectionFailed;
|
||||
|
||||
/// <summary>Starts the transport and attempts to connect to the given host address.</summary>
|
||||
/// <param name="hostAddress">The host address to connect to.</param>
|
||||
/// <param name="connection">The pending connection. <see langword="null"/> if an issue occurred.</param>
|
||||
/// <param name="connectError">The error message associated with the issue that occurred, if any.</param>
|
||||
/// <returns><see langword="true"/> if a connection attempt will be made. <see langword="false"/> if an issue occurred (such as <paramref name="hostAddress"/> being in an invalid format) and a connection attempt will <i>not</i> be made.</returns>
|
||||
bool Connect(string hostAddress, out Connection connection, out string connectError);
|
||||
|
||||
/// <summary>Closes the connection to the server.</summary>
|
||||
void Disconnect();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>The header type of a <see cref="Message"/>.</summary>
|
||||
public enum MessageHeader : byte
|
||||
{
|
||||
/// <summary>An unreliable user message.</summary>
|
||||
Unreliable,
|
||||
/// <summary>An internal unreliable ack message.</summary>
|
||||
Ack,
|
||||
/// <summary>An internal unreliable connect message.</summary>
|
||||
Connect,
|
||||
/// <summary>An internal unreliable connection rejection message.</summary>
|
||||
Reject,
|
||||
/// <summary>An internal unreliable heartbeat message.</summary>
|
||||
Heartbeat,
|
||||
/// <summary>An internal unreliable disconnect message.</summary>
|
||||
Disconnect,
|
||||
|
||||
/// <summary>A notify message.</summary>
|
||||
Notify,
|
||||
|
||||
/// <summary>A reliable user message.</summary>
|
||||
Reliable,
|
||||
/// <summary>An internal reliable welcome message.</summary>
|
||||
Welcome,
|
||||
/// <summary>An internal reliable client connected message.</summary>
|
||||
ClientConnected,
|
||||
/// <summary>An internal reliable client disconnected message.</summary>
|
||||
ClientDisconnected,
|
||||
}
|
||||
|
||||
/// <summary>Defines methods, properties, and events which every transport's server <i>and</i> client must implement.</summary>
|
||||
public interface IPeer
|
||||
{
|
||||
/// <summary>Invoked when data is received by the transport.</summary>
|
||||
event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
/// <summary>Invoked when a disconnection is initiated or detected by the transport.</summary>
|
||||
event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>Initiates handling of any received messages.</summary>
|
||||
void Poll();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Transports
|
||||
{
|
||||
/// <summary>Defines methods, properties, and events which every transport's server must implement.</summary>
|
||||
public interface IServer : IPeer
|
||||
{
|
||||
/// <summary>Invoked when a connection is established at the transport level.</summary>
|
||||
event EventHandler<ConnectedEventArgs> Connected;
|
||||
|
||||
/// <inheritdoc cref="Server.Port"/>
|
||||
ushort Port { get; }
|
||||
|
||||
/// <summary>Starts the transport and begins listening for incoming connections.</summary>
|
||||
/// <param name="port">The local port on which to listen for connections.</param>
|
||||
void Start(ushort port);
|
||||
|
||||
/// <summary>Closes an active connection.</summary>
|
||||
/// <param name="connection">The connection to close.</param>
|
||||
void Close(Connection connection);
|
||||
|
||||
/// <summary>Closes all existing connections and stops listening for new connections.</summary>
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>A client which can connect to a <see cref="TcpServer"/>.</summary>
|
||||
public class TcpClient : TcpPeer, IClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler ConnectionFailed;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <summary>The connection to the server.</summary>
|
||||
private TcpConnection tcpConnection;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>.</remarks>
|
||||
public bool Connect(string hostAddress, out Connection connection, out string connectError)
|
||||
{
|
||||
connectError = $"Invalid host address '{hostAddress}'! IP and port should be separated by a colon, for example: '127.0.0.1:7777'.";
|
||||
if (!ParseHostAddress(hostAddress, out IPAddress ip, out ushort port))
|
||||
{
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(ip, port);
|
||||
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
SendBufferSize = socketBufferSize,
|
||||
ReceiveBufferSize = socketBufferSize,
|
||||
NoDelay = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
socket.Connect(remoteEndPoint); // TODO: do something about the fact that this is a blocking call
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// The connection failed, but invoking the transports ConnectionFailed event from
|
||||
// inside this method will cause problems, so we're just goint to eat the exception,
|
||||
// call OnConnected(), and let Riptide detect that no connection was established.
|
||||
}
|
||||
|
||||
connection = tcpConnection = new TcpConnection(socket, remoteEndPoint, this);
|
||||
OnConnected();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parses <paramref name="hostAddress"/> into <paramref name="ip"/> and <paramref name="port"/>, if possible.</summary>
|
||||
/// <param name="hostAddress">The host address to parse.</param>
|
||||
/// <param name="ip">The retrieved IP.</param>
|
||||
/// <param name="port">The retrieved port.</param>
|
||||
/// <returns>Whether or not <paramref name="hostAddress"/> was in a valid format.</returns>
|
||||
private bool ParseHostAddress(string hostAddress, out IPAddress ip, out ushort port)
|
||||
{
|
||||
string[] ipAndPort = hostAddress.Split(':');
|
||||
string ipString = "";
|
||||
string portString = "";
|
||||
if (ipAndPort.Length > 2)
|
||||
{
|
||||
// There was more than one ':' in the host address, might be IPv6
|
||||
ipString = string.Join(":", ipAndPort.Take(ipAndPort.Length - 1));
|
||||
portString = ipAndPort[ipAndPort.Length - 1];
|
||||
}
|
||||
else if (ipAndPort.Length == 2)
|
||||
{
|
||||
// IPv4
|
||||
ipString = ipAndPort[0];
|
||||
portString = ipAndPort[1];
|
||||
}
|
||||
|
||||
port = 0; // Need to make sure a value is assigned in case IP parsing fails
|
||||
return IPAddress.TryParse(ipString, out ip) && ushort.TryParse(portString, out port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Poll()
|
||||
{
|
||||
if (tcpConnection != null)
|
||||
tcpConnection.Receive();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Disconnect()
|
||||
{
|
||||
socket.Close();
|
||||
tcpConnection = null;
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
protected virtual void OnConnectionFailed()
|
||||
{
|
||||
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnDataReceived(int amount, TcpConnection fromConnection)
|
||||
{
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>Represents a connection to a <see cref="TcpServer"/> or <see cref="TcpClient"/>.</summary>
|
||||
public class TcpConnection : Connection, IEquatable<TcpConnection>
|
||||
{
|
||||
/// <summary>The endpoint representing the other end of the connection.</summary>
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
|
||||
/// <summary>Whether or not the server has received a connection attempt from this connection.</summary>
|
||||
internal bool DidReceiveConnect;
|
||||
|
||||
/// <summary>The socket to use for sending and receiving.</summary>
|
||||
private readonly Socket socket;
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
private readonly TcpPeer peer;
|
||||
/// <summary>An array to receive message size values into.</summary>
|
||||
private readonly byte[] sizeBytes = new byte[sizeof(int)];
|
||||
/// <summary>The size of the next message to be received.</summary>
|
||||
private int nextMessageSize;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
/// <param name="socket">The socket to use for sending and receiving.</param>
|
||||
/// <param name="remoteEndPoint">The endpoint representing the other end of the connection.</param>
|
||||
/// <param name="peer">The local peer this connection is associated with.</param>
|
||||
internal TcpConnection(Socket socket, IPEndPoint remoteEndPoint, TcpPeer peer)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
this.socket = socket;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Sending 0 bytes is not allowed!");
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.Connected)
|
||||
{
|
||||
Converter.FromInt(amount, peer.SendBuffer, 0);
|
||||
Array.Copy(dataBuffer, 0, peer.SendBuffer, sizeof(int), amount); // TODO: consider sending length separately with an extra socket.Send call instead of copying the data an extra time
|
||||
socket.Send(peer.SendBuffer, amount + sizeof(int), SocketFlags.None);
|
||||
}
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// May want to consider triggering a disconnect here (perhaps depending on the type
|
||||
// of SocketException)? Timeout should catch disconnections, but disconnecting
|
||||
// explicitly might be better...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Polls the socket and checks if any data was received.</summary>
|
||||
internal void Receive()
|
||||
{
|
||||
bool tryReceiveMore = true;
|
||||
while (tryReceiveMore)
|
||||
{
|
||||
int byteCount = 0;
|
||||
try
|
||||
{
|
||||
if (nextMessageSize > 0)
|
||||
{
|
||||
// We already have a size value
|
||||
tryReceiveMore = TryReceiveMessage(out byteCount);
|
||||
}
|
||||
else if (socket.Available >= sizeof(int))
|
||||
{
|
||||
// We have enough bytes for a complete size value
|
||||
socket.Receive(sizeBytes, sizeof(int), SocketFlags.None);
|
||||
nextMessageSize = Converter.ToInt(sizeBytes, 0);
|
||||
|
||||
if (nextMessageSize > 0)
|
||||
tryReceiveMore = TryReceiveMessage(out byteCount);
|
||||
}
|
||||
else
|
||||
tryReceiveMore = false;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
switch (ex.SocketErrorCode)
|
||||
{
|
||||
case SocketError.Interrupted:
|
||||
case SocketError.NotSocket:
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
break;
|
||||
case SocketError.ConnectionReset:
|
||||
peer.OnDisconnected(this, DisconnectReason.Disconnected);
|
||||
break;
|
||||
case SocketError.TimedOut:
|
||||
peer.OnDisconnected(this, DisconnectReason.TimedOut);
|
||||
break;
|
||||
case SocketError.MessageSize:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
peer.OnDisconnected(this, DisconnectReason.TransportError);
|
||||
}
|
||||
|
||||
if (byteCount > 0)
|
||||
peer.OnDataReceived(byteCount, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Receives a message, if all of its data is ready to be received.</summary>
|
||||
/// <param name="receivedByteCount">How many bytes were received.</param>
|
||||
/// <returns>Whether or not all of the message's data was ready to be received.</returns>
|
||||
private bool TryReceiveMessage(out int receivedByteCount)
|
||||
{
|
||||
if (socket.Available >= nextMessageSize)
|
||||
{
|
||||
// We have enough bytes to read the complete message
|
||||
receivedByteCount = socket.Receive(peer.ReceiveBuffer, nextMessageSize, SocketFlags.None);
|
||||
nextMessageSize = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
receivedByteCount = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Closes the connection.</summary>
|
||||
internal void Close()
|
||||
{
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as TcpConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TcpConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return RemoteEndPoint.Equals(other.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -288961498 + EqualityComparer<IPEndPoint>.Default.GetHashCode(RemoteEndPoint);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator ==(TcpConnection left, TcpConnection right)
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator !=(TcpConnection left, TcpConnection right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>Provides base send & receive functionality for <see cref="TcpServer"/> and <see cref="TcpClient"/>.</summary>
|
||||
public abstract class TcpPeer
|
||||
{
|
||||
/// <inheritdoc cref="IPeer.Disconnected"/>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>An array that incoming data is received into.</summary>
|
||||
internal readonly byte[] ReceiveBuffer;
|
||||
/// <summary>An array that outgoing data is sent out of.</summary>
|
||||
internal readonly byte[] SendBuffer;
|
||||
|
||||
/// <summary>The default size used for the socket's send and receive buffers.</summary>
|
||||
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
|
||||
/// <summary>The size to use for the socket's send and receive buffers.</summary>
|
||||
protected readonly int socketBufferSize;
|
||||
/// <summary>The main socket, either used for listening for connections or for sending and receiving data.</summary>
|
||||
protected Socket socket;
|
||||
/// <summary>The minimum size that may be used for the socket's send and receive buffers.</summary>
|
||||
private const int MinSocketBufferSize = 256 * 1024; // 256KB
|
||||
|
||||
/// <summary>Initializes the transport.</summary>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
protected TcpPeer(int socketBufferSize = DefaultSocketBufferSize)
|
||||
{
|
||||
if (socketBufferSize < MinSocketBufferSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
|
||||
|
||||
this.socketBufferSize = socketBufferSize;
|
||||
ReceiveBuffer = new byte[Message.MaxSize];
|
||||
SendBuffer = new byte[Message.MaxSize + sizeof(int)]; // Need room for the entire message plus the message length (since this is TCP)
|
||||
}
|
||||
|
||||
/// <summary>Handles received data.</summary>
|
||||
/// <param name="amount">The number of bytes that were received.</param>
|
||||
/// <param name="fromConnection">The connection from which the data was received.</param>
|
||||
protected internal abstract void OnDataReceived(int amount, TcpConnection fromConnection);
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected internal virtual void OnDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Tcp
|
||||
{
|
||||
/// <summary>A server which can accept connections from <see cref="TcpClient"/>s.</summary>
|
||||
public class TcpServer : TcpPeer, IServer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Port { get; private set; }
|
||||
/// <summary>The maximum number of pending connections to allow at any given time.</summary>
|
||||
public int MaxPendingConnections { get; private set; } = 5;
|
||||
|
||||
/// <summary>Whether or not the server is running.</summary>
|
||||
private bool isRunning = false;
|
||||
/// <summary>The currently open connections, accessible by their endpoints.</summary>
|
||||
private Dictionary<IPEndPoint, TcpConnection> connections;
|
||||
/// <summary>Connections that have been closed and need to be removed from <see cref="connections"/>.</summary>
|
||||
private readonly List<IPEndPoint> closedConnections = new List<IPEndPoint>();
|
||||
/// <summary>The IP address to bind the socket to.</summary>
|
||||
private readonly IPAddress listenAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TcpServer(int socketBufferSize = DefaultSocketBufferSize) : this(IPAddress.IPv6Any, socketBufferSize) { }
|
||||
|
||||
/// <summary>Initializes the transport, binding the socket to a specific IP address.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
public TcpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(socketBufferSize)
|
||||
{
|
||||
this.listenAddress = listenAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<IPEndPoint, TcpConnection>();
|
||||
|
||||
StartListening(port);
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for connections on the given port.</summary>
|
||||
/// <param name="port">The port to listen on.</param>
|
||||
private void StartListening(ushort port)
|
||||
{
|
||||
if (isRunning)
|
||||
StopListening();
|
||||
|
||||
IPEndPoint localEndPoint = new IPEndPoint(listenAddress, port);
|
||||
socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
SendBufferSize = socketBufferSize,
|
||||
ReceiveBufferSize = socketBufferSize,
|
||||
NoDelay = true,
|
||||
};
|
||||
socket.Bind(localEndPoint);
|
||||
socket.Listen(MaxPendingConnections);
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Poll()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
Accept();
|
||||
foreach (TcpConnection connection in connections.Values)
|
||||
connection.Receive();
|
||||
|
||||
foreach (IPEndPoint endPoint in closedConnections)
|
||||
connections.Remove(endPoint);
|
||||
|
||||
closedConnections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Accepts any pending connections.</summary>
|
||||
private void Accept()
|
||||
{
|
||||
if (socket.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
Socket acceptedSocket = socket.Accept();
|
||||
IPEndPoint fromEndPoint = (IPEndPoint)acceptedSocket.RemoteEndPoint;
|
||||
if (!connections.ContainsKey(fromEndPoint))
|
||||
{
|
||||
TcpConnection newConnection = new TcpConnection(acceptedSocket, fromEndPoint, this);
|
||||
connections.Add(fromEndPoint, newConnection);
|
||||
OnConnected(newConnection);
|
||||
}
|
||||
else
|
||||
acceptedSocket.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stops listening for connections.</summary>
|
||||
private void StopListening()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
isRunning = false;
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is TcpConnection tcpConnection)
|
||||
{
|
||||
closedConnections.Add(tcpConnection.RemoteEndPoint);
|
||||
tcpConnection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Shutdown()
|
||||
{
|
||||
StopListening();
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
/// <param name="connection">The successfully established connection.</param>
|
||||
protected virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnDataReceived(int amount, TcpConnection fromConnection)
|
||||
{
|
||||
if ((MessageHeader)(ReceiveBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect)
|
||||
{
|
||||
if (fromConnection.DidReceiveConnect)
|
||||
return;
|
||||
|
||||
fromConnection.DidReceiveConnect = true;
|
||||
}
|
||||
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(ReceiveBuffer, amount, fromConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>A client which can connect to a <see cref="UdpServer"/>.</summary>
|
||||
public class UdpClient : UdpPeer, IClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler ConnectionFailed;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <summary>The connection to the server.</summary>
|
||||
private UdpConnection udpConnection;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UdpClient(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Expects the host address to consist of an IP and port, separated by a colon. For example: <c>127.0.0.1:7777</c>.</remarks>
|
||||
public bool Connect(string hostAddress, out Connection connection, out string connectError)
|
||||
{
|
||||
connectError = $"Invalid host address '{hostAddress}'! IP and port should be separated by a colon, for example: '127.0.0.1:7777'.";
|
||||
if (!ParseHostAddress(hostAddress, out IPAddress ip, out ushort port))
|
||||
{
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((mode == SocketMode.IPv4Only && ip.AddressFamily == AddressFamily.InterNetworkV6) || (mode == SocketMode.IPv6Only && ip.AddressFamily == AddressFamily.InterNetwork))
|
||||
{
|
||||
// The IP address isn't in an acceptable format for the current socket mode
|
||||
if (mode == SocketMode.IPv4Only)
|
||||
connectError = "Connecting to IPv6 addresses is not allowed when running in IPv4 only mode!";
|
||||
else
|
||||
connectError = "Connecting to IPv4 addresses is not allowed when running in IPv6 only mode!";
|
||||
|
||||
connection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
OpenSocket();
|
||||
|
||||
connection = udpConnection = new UdpConnection(new IPEndPoint(mode == SocketMode.IPv4Only ? ip : ip.MapToIPv6(), port), this);
|
||||
OnConnected(); // UDP is connectionless, so from the transport POV everything is immediately ready to send/receive data
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parses <paramref name="hostAddress"/> into <paramref name="ip"/> and <paramref name="port"/>, if possible.</summary>
|
||||
/// <param name="hostAddress">The host address to parse.</param>
|
||||
/// <param name="ip">The retrieved IP.</param>
|
||||
/// <param name="port">The retrieved port.</param>
|
||||
/// <returns>Whether or not <paramref name="hostAddress"/> was in a valid format.</returns>
|
||||
private bool ParseHostAddress(string hostAddress, out IPAddress ip, out ushort port)
|
||||
{
|
||||
string[] ipAndPort = hostAddress.Split(':');
|
||||
string ipString = "";
|
||||
string portString = "";
|
||||
if (ipAndPort.Length > 2)
|
||||
{
|
||||
// There was more than one ':' in the host address, might be IPv6
|
||||
ipString = string.Join(":", ipAndPort.Take(ipAndPort.Length - 1));
|
||||
portString = ipAndPort[ipAndPort.Length - 1];
|
||||
}
|
||||
else if (ipAndPort.Length == 2)
|
||||
{
|
||||
// IPv4
|
||||
ipString = ipAndPort[0];
|
||||
portString = ipAndPort[1];
|
||||
}
|
||||
|
||||
port = 0; // Need to make sure a value is assigned in case IP parsing fails
|
||||
return IPAddress.TryParse(ipString, out ip) && ushort.TryParse(portString, out port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Disconnect()
|
||||
{
|
||||
CloseSocket();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
protected virtual void OnConnected()
|
||||
{
|
||||
Connected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="ConnectionFailed"/> event.</summary>
|
||||
protected virtual void OnConnectionFailed()
|
||||
{
|
||||
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
|
||||
{
|
||||
if (udpConnection.RemoteEndPoint.Equals(fromEndPoint) && !udpConnection.IsNotConnected)
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, udpConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>Represents a connection to a <see cref="UdpServer"/> or <see cref="UdpClient"/>.</summary>
|
||||
public class UdpConnection : Connection, IEquatable<UdpConnection>
|
||||
{
|
||||
/// <summary>The endpoint representing the other end of the connection.</summary>
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
|
||||
/// <summary>The local peer this connection is associated with.</summary>
|
||||
private readonly UdpPeer peer;
|
||||
|
||||
/// <summary>Initializes the connection.</summary>
|
||||
/// <param name="remoteEndPoint">The endpoint representing the other end of the connection.</param>
|
||||
/// <param name="peer">The local peer this connection is associated with.</param>
|
||||
internal UdpConnection(IPEndPoint remoteEndPoint, UdpPeer peer)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
peer.Send(dataBuffer, amount, RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => RemoteEndPoint.ToStringBasedOnIPFormat();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as UdpConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(UdpConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return RemoteEndPoint.Equals(other.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -288961498 + EqualityComparer<IPEndPoint>.Default.GetHashCode(RemoteEndPoint);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator ==(UdpConnection left, UdpConnection right)
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator !=(UdpConnection left, UdpConnection right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>The kind of socket to create.</summary>
|
||||
public enum SocketMode
|
||||
{
|
||||
/// <summary>Dual-mode. Works with both IPv4 and IPv6.</summary>
|
||||
Both,
|
||||
/// <summary>IPv4 only mode.</summary>
|
||||
IPv4Only,
|
||||
/// <summary>IPv6 only mode.</summary>
|
||||
IPv6Only
|
||||
}
|
||||
|
||||
/// <summary>Provides base send & receive functionality for <see cref="UdpServer"/> and <see cref="UdpClient"/>.</summary>
|
||||
public abstract class UdpPeer
|
||||
{
|
||||
/// <inheritdoc cref="IPeer.Disconnected"/>
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
/// <summary>The default size used for the socket's send and receive buffers.</summary>
|
||||
protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB
|
||||
/// <summary>The minimum size that may be used for the socket's send and receive buffers.</summary>
|
||||
private const int MinSocketBufferSize = 256 * 1024; // 256KB
|
||||
/// <summary>How long to wait for a packet, in microseconds.</summary>
|
||||
private const int ReceivePollingTime = 500000; // 0.5 seconds
|
||||
|
||||
/// <summary>Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</summary>
|
||||
protected readonly SocketMode mode;
|
||||
/// <summary>The size to use for the socket's send and receive buffers.</summary>
|
||||
private readonly int socketBufferSize;
|
||||
/// <summary>The array that incoming data is received into.</summary>
|
||||
private readonly byte[] receivedData;
|
||||
/// <summary>The socket to use for sending and receiving.</summary>
|
||||
private Socket socket;
|
||||
/// <summary>Whether or not the transport is running.</summary>
|
||||
private bool isRunning;
|
||||
/// <summary>A reusable endpoint.</summary>
|
||||
private EndPoint remoteEndPoint;
|
||||
|
||||
/// <summary>Initializes the transport.</summary>
|
||||
/// <param name="mode">Whether to create an IPv4 only, IPv6 only, or dual-mode socket.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
protected UdpPeer(SocketMode mode, int socketBufferSize)
|
||||
{
|
||||
if (socketBufferSize < MinSocketBufferSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!");
|
||||
|
||||
this.mode = mode;
|
||||
this.socketBufferSize = socketBufferSize;
|
||||
receivedData = new byte[Message.MaxSize];
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IPeer.Poll"/>
|
||||
public void Poll()
|
||||
{
|
||||
Receive();
|
||||
}
|
||||
|
||||
/// <summary>Opens the socket and starts the transport.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to, if any.</param>
|
||||
/// <param name="port">The port to bind the socket to.</param>
|
||||
protected void OpenSocket(IPAddress listenAddress = null, ushort port = 0)
|
||||
{
|
||||
if (isRunning)
|
||||
CloseSocket();
|
||||
|
||||
if (mode == SocketMode.IPv4Only)
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
else if (mode == SocketMode.IPv6Only)
|
||||
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) { DualMode = false };
|
||||
else
|
||||
socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
IPAddress any = socket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any;
|
||||
socket.SendBufferSize = socketBufferSize;
|
||||
socket.ReceiveBufferSize = socketBufferSize;
|
||||
socket.Bind(new IPEndPoint(listenAddress == null ? any : listenAddress, port));
|
||||
remoteEndPoint = new IPEndPoint(any, 0);
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/// <summary>Closes the socket and stops the transport.</summary>
|
||||
protected void CloseSocket()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
isRunning = false;
|
||||
socket.Close();
|
||||
}
|
||||
|
||||
/// <summary>Polls the socket and checks if any data was received.</summary>
|
||||
private void Receive()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
bool tryReceiveMore = true;
|
||||
while (tryReceiveMore)
|
||||
{
|
||||
int byteCount = 0;
|
||||
try
|
||||
{
|
||||
if (socket.Available > 0 && socket.Poll(ReceivePollingTime, SelectMode.SelectRead))
|
||||
byteCount = socket.ReceiveFrom(receivedData, SocketFlags.None, ref remoteEndPoint);
|
||||
else
|
||||
tryReceiveMore = false;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
switch (ex.SocketErrorCode)
|
||||
{
|
||||
case SocketError.Interrupted:
|
||||
case SocketError.NotSocket:
|
||||
isRunning = false;
|
||||
break;
|
||||
case SocketError.ConnectionReset:
|
||||
case SocketError.MessageSize:
|
||||
case SocketError.TimedOut:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
isRunning = false;
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
tryReceiveMore = false;
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
if (byteCount > 0)
|
||||
OnDataReceived(receivedData, byteCount, (IPEndPoint)remoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a given endpoint.</summary>
|
||||
/// <param name="dataBuffer">The array containing the data.</param>
|
||||
/// <param name="numBytes">The number of bytes in the array which should be sent.</param>
|
||||
/// <param name="toEndPoint">The endpoint to send the data to.</param>
|
||||
internal void Send(byte[] dataBuffer, int numBytes, IPEndPoint toEndPoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isRunning)
|
||||
socket.SendTo(dataBuffer, numBytes, SocketFlags.None, toEndPoint);
|
||||
}
|
||||
catch(SocketException)
|
||||
{
|
||||
// May want to consider triggering a disconnect here (perhaps depending on the type
|
||||
// of SocketException)? Timeout should catch disconnections, but disconnecting
|
||||
// explicitly might be better...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles received data.</summary>
|
||||
/// <param name="dataBuffer">A byte array containing the received data.</param>
|
||||
/// <param name="amount">The number of bytes in <paramref name="dataBuffer"/> used by the received data.</param>
|
||||
/// <param name="fromEndPoint">The endpoint from which the data was received.</param>
|
||||
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint);
|
||||
|
||||
/// <summary>Invokes the <see cref="Disconnected"/> event.</summary>
|
||||
/// <param name="connection">The closed connection.</param>
|
||||
/// <param name="reason">The reason for the disconnection.</param>
|
||||
protected virtual void OnDisconnected(Connection connection, DisconnectReason reason)
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Transports.Udp
|
||||
{
|
||||
/// <summary>A server which can accept connections from <see cref="UdpClient"/>s.</summary>
|
||||
public class UdpServer : UdpPeer, IServer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
/// <summary>The currently open connections, accessible by their endpoints.</summary>
|
||||
private Dictionary<IPEndPoint, Connection> connections;
|
||||
/// <summary>The IP address to bind the socket to, if any.</summary>
|
||||
private readonly IPAddress listenAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UdpServer(SocketMode mode = SocketMode.Both, int socketBufferSize = DefaultSocketBufferSize) : base(mode, socketBufferSize) { }
|
||||
|
||||
/// <summary>Initializes the transport, binding the socket to a specific IP address.</summary>
|
||||
/// <param name="listenAddress">The IP address to bind the socket to.</param>
|
||||
/// <param name="socketBufferSize">How big the socket's send and receive buffers should be.</param>
|
||||
public UdpServer(IPAddress listenAddress, int socketBufferSize = DefaultSocketBufferSize) : base(SocketMode.Both, socketBufferSize)
|
||||
{
|
||||
this.listenAddress = listenAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<IPEndPoint, Connection>();
|
||||
|
||||
OpenSocket(listenAddress, port);
|
||||
}
|
||||
|
||||
/// <summary>Decides what to do with a connection attempt.</summary>
|
||||
/// <param name="fromEndPoint">The endpoint the connection attempt is coming from.</param>
|
||||
/// <returns>Whether or not the connection attempt was from a new connection.</returns>
|
||||
private bool HandleConnectionAttempt(IPEndPoint fromEndPoint)
|
||||
{
|
||||
if (connections.ContainsKey(fromEndPoint))
|
||||
return false;
|
||||
|
||||
UdpConnection connection = new UdpConnection(fromEndPoint, this);
|
||||
connections.Add(fromEndPoint, connection);
|
||||
OnConnected(connection);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is UdpConnection udpConnection)
|
||||
connections.Remove(udpConnection.RemoteEndPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Shutdown()
|
||||
{
|
||||
CloseSocket();
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <see cref="Connected"/> event.</summary>
|
||||
/// <param name="connection">The successfully established connection.</param>
|
||||
protected virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint)
|
||||
{
|
||||
if ((MessageHeader)(dataBuffer[0] & Message.HeaderBitmask) == MessageHeader.Connect && !HandleConnectionAttempt(fromEndPoint))
|
||||
return;
|
||||
|
||||
if (connections.TryGetValue(fromEndPoint, out Connection connection) && !connection.IsNotConnected)
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Provides functionality for managing and manipulating a collection of bits.</summary>
|
||||
internal class Bitfield
|
||||
{
|
||||
/// <summary>The first 8 bits stored in the bitfield.</summary>
|
||||
internal byte First8 => (byte)segments[0];
|
||||
/// <summary>The first 16 bits stored in the bitfield.</summary>
|
||||
internal ushort First16 => (ushort)segments[0];
|
||||
|
||||
/// <summary>The number of bits which fit into a single segment.</summary>
|
||||
private const int SegmentSize = sizeof(uint) * 8;
|
||||
/// <summary>The segments of the bitfield.</summary>
|
||||
private readonly List<uint> segments;
|
||||
/// <summary>Whether or not the bitfield's capacity should dynamically adjust when shifting.</summary>
|
||||
private readonly bool isDynamicCapacity;
|
||||
/// <summary>The current number of bits being stored.</summary>
|
||||
private int count;
|
||||
/// <summary>The current capacity.</summary>
|
||||
private int capacity;
|
||||
|
||||
/// <summary>Creates a bitfield.</summary>
|
||||
/// <param name="isDynamicCapacity">Whether or not the bitfield's capacity should dynamically adjust when shifting.</param>
|
||||
internal Bitfield(bool isDynamicCapacity = true)
|
||||
{
|
||||
segments = new List<uint>(4) { 0 };
|
||||
capacity = segments.Count * SegmentSize;
|
||||
this.isDynamicCapacity = isDynamicCapacity;
|
||||
}
|
||||
|
||||
/// <summary>Checks if the bitfield has capacity for the given number of bits.</summary>
|
||||
/// <param name="amount">The number of bits for which to check if there is capacity.</param>
|
||||
/// <param name="overflow">The number of bits from <paramref name="amount"/> which there is no capacity for.</param>
|
||||
/// <returns>Whether or not there is sufficient capacity.</returns>
|
||||
internal bool HasCapacityFor(int amount, out int overflow)
|
||||
{
|
||||
overflow = count + amount - capacity;
|
||||
return overflow < 0;
|
||||
}
|
||||
|
||||
/// <summary>Shifts the bitfield by the given amount.</summary>
|
||||
/// <param name="amount">How much to shift by.</param>
|
||||
internal void ShiftBy(int amount)
|
||||
{
|
||||
int segmentShift = amount / SegmentSize; // How many WHOLE segments we have to shift by
|
||||
int bitShift = amount % SegmentSize; // How many bits we have to shift by
|
||||
|
||||
if (!isDynamicCapacity)
|
||||
count = Math.Min(count + amount, SegmentSize);
|
||||
else if (!HasCapacityFor(amount, out int _))
|
||||
{
|
||||
Trim();
|
||||
count += amount;
|
||||
|
||||
if (count > capacity)
|
||||
{
|
||||
int increaseBy = segmentShift + 1;
|
||||
for (int i = 0; i < increaseBy; i++)
|
||||
segments.Add(0);
|
||||
|
||||
capacity = segments.Count * SegmentSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
count += amount;
|
||||
|
||||
int s = segments.Count - 1;
|
||||
segments[s] <<= bitShift;
|
||||
s -= 1 + segmentShift;
|
||||
while (s > -1)
|
||||
{
|
||||
ulong shiftedBits = (ulong)segments[s] << bitShift;
|
||||
segments[s] = (uint)shiftedBits;
|
||||
|
||||
segments[s + 1 + segmentShift] |= (uint)(shiftedBits >> SegmentSize);
|
||||
s--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks the last bit in the bitfield, and trims it if it is set to 1.</summary>
|
||||
/// <param name="checkedPosition">The checked bit's position in the bitfield.</param>
|
||||
/// <returns>Whether or not the checked bit was set.</returns>
|
||||
internal bool CheckAndTrimLast(out int checkedPosition)
|
||||
{
|
||||
checkedPosition = count;
|
||||
uint bitToCheck = (uint)(1 << ((count - 1) % SegmentSize));
|
||||
bool isSet = (segments[segments.Count - 1] & bitToCheck) != 0;
|
||||
count--;
|
||||
return isSet;
|
||||
}
|
||||
|
||||
/// <summary>Trims all bits from the end of the bitfield until an unset bit is encountered.</summary>
|
||||
private void Trim()
|
||||
{
|
||||
while (count > 0 && IsSet(count))
|
||||
count--;
|
||||
}
|
||||
|
||||
/// <summary>Sets the given bit to 1.</summary>
|
||||
/// <param name="bit">The bit to set.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="bit"/> is less than 1.</exception>
|
||||
internal void Set(int bit)
|
||||
{
|
||||
if (bit < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(bit), $"'{nameof(bit)}' must be greater than zero!");
|
||||
|
||||
bit--;
|
||||
int s = bit / SegmentSize;
|
||||
uint bitToSet = (uint)(1 << (bit % SegmentSize));
|
||||
if (s < segments.Count)
|
||||
segments[s] |= bitToSet;
|
||||
}
|
||||
|
||||
/// <summary>Checks if the given bit is set to 1.</summary>
|
||||
/// <param name="bit">The bit to check.</param>
|
||||
/// <returns>Whether or not the bit is set.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="bit"/> is less than 1.</exception>
|
||||
internal bool IsSet(int bit)
|
||||
{
|
||||
if (bit > count)
|
||||
return true;
|
||||
|
||||
if (bit < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(bit), $"'{nameof(bit)}' must be greater than zero!");
|
||||
|
||||
bit--;
|
||||
int s = bit / SegmentSize;
|
||||
uint bitToCheck = (uint)(1 << (bit % SegmentSize));
|
||||
if (s < segments.Count)
|
||||
return (segments[s] & bitToCheck) != 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Combines this bitfield with the given bits.</summary>
|
||||
/// <param name="other">The bits to OR into the bitfield.</param>
|
||||
internal void Combine(ushort other)
|
||||
{
|
||||
segments[0] |= other;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Tracks and manages various metrics of a <see cref="Connection"/>.</summary>
|
||||
public class ConnectionMetrics
|
||||
{
|
||||
/// <summary>The total number of bytes received across all send modes since the last <see cref="Reset"/> call, including those in duplicate and, in
|
||||
/// the case of notify messages, out-of-order packets. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int BytesIn => UnreliableBytesIn + NotifyBytesIn + ReliableBytesIn;
|
||||
/// <summary>The total number of bytes sent across all send modes since the last <see cref="Reset"/> call, including those in automatic resends.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int BytesOut => UnreliableBytesOut + NotifyBytesOut + ReliableBytesOut;
|
||||
/// <summary>The total number of messages received across all send modes since the last <see cref="Reset"/> call, including duplicate and out-of-order notify messages.</summary>
|
||||
public int MessagesIn => UnreliableIn + NotifyIn + ReliableIn;
|
||||
/// <summary>The total number of messages sent across all send modes since the last <see cref="Reset"/> call, including automatic resends.</summary>
|
||||
public int MessagesOut => UnreliableOut + NotifyOut + ReliableOut;
|
||||
|
||||
/// <summary>The total number of bytes received in unreliable messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int UnreliableBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in unreliable messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int UnreliableBytesOut { get; internal set; }
|
||||
/// <summary>The number of unreliable messages received since the last <see cref="Reset"/> call.</summary>
|
||||
public int UnreliableIn { get; private set; }
|
||||
/// <summary>The number of unreliable messages sent since the last <see cref="Reset"/> call.</summary>
|
||||
public int UnreliableOut { get; internal set; }
|
||||
|
||||
/// <summary>The total number of bytes received in notify messages since the last <see cref="Reset"/> call, including those in duplicate and out-of-order packets.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int NotifyBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in notify messages since the last <see cref="Reset"/> call. Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int NotifyBytesOut { get; internal set; }
|
||||
/// <summary>The number of notify messages received since the last <see cref="Reset"/> call, including duplicate and out-of-order ones.</summary>
|
||||
public int NotifyIn { get; private set; }
|
||||
/// <summary>The number of notify messages sent since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyOut { get; internal set; }
|
||||
/// <summary>The number of duplicate or out-of-order notify messages which were received, but discarded (not handled) since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyDiscarded { get; internal set; }
|
||||
/// <summary>The number of notify messages lost since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyLost { get; private set; }
|
||||
/// <summary>The number of notify messages delivered since the last <see cref="Reset"/> call.</summary>
|
||||
public int NotifyDelivered { get; private set; }
|
||||
/// <summary>The number of notify messages lost of the last 64 notify messages to be lost or delivered.</summary>
|
||||
public int RollingNotifyLost { get; private set; }
|
||||
/// <summary>The number of notify messages delivered of the last 64 notify messages to be lost or delivered.</summary>
|
||||
public int RollingNotifyDelivered { get; private set; }
|
||||
/// <summary>The loss rate (0-1) among the last 64 notify messages.</summary>
|
||||
public float RollingNotifyLossRate => RollingNotifyLost / 64f;
|
||||
|
||||
/// <summary>The total number of bytes received in reliable messages since the last <see cref="Reset"/> call, including those in duplicate packets.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int ReliableBytesIn { get; private set; }
|
||||
/// <summary>The total number of bytes sent in reliable messages since the last <see cref="Reset"/> call, including those in automatic resends.
|
||||
/// Does <i>not</i> include packet header bytes, which may vary by transport.</summary>
|
||||
public int ReliableBytesOut { get; internal set; }
|
||||
/// <summary>The number of reliable messages received since the last <see cref="Reset"/> call, including duplicates.</summary>
|
||||
public int ReliableIn { get; private set; }
|
||||
/// <summary>The number of reliable messages sent since the last <see cref="Reset"/> call, including automatic resends (each resend adds to this value).</summary>
|
||||
public int ReliableOut { get; internal set; }
|
||||
/// <summary>The number of duplicate reliable messages which were received, but discarded (and not handled) since the last <see cref="Reset"/> call.</summary>
|
||||
public int ReliableDiscarded { get; internal set; }
|
||||
/// <summary>The number of unique reliable messages sent since the last <see cref="Reset"/> call.
|
||||
/// A message only counts towards this the first time it is sent—subsequent resends are not counted.</summary>
|
||||
public int ReliableUniques { get; internal set; }
|
||||
/// <summary>The number of send attempts that were required to deliver recent reliable messages.</summary>
|
||||
public readonly RollingStat RollingReliableSends;
|
||||
|
||||
/// <summary>The left-most bit of a <see cref="ulong"/>, used to store the oldest value in the <see cref="notifyLossTracker"/>.</summary>
|
||||
private const ulong ULongLeftBit = 1ul << 63;
|
||||
/// <summary>Which recent notify messages were lost. Each bit corresponds to a message.</summary>
|
||||
private ulong notifyLossTracker;
|
||||
/// <summary>How many of the <see cref="notifyLossTracker"/>'s bits are in use.</summary>
|
||||
private int notifyBufferCount;
|
||||
|
||||
/// <summary>Initializes metrics.</summary>
|
||||
public ConnectionMetrics()
|
||||
{
|
||||
Reset();
|
||||
RollingNotifyDelivered = 0;
|
||||
RollingNotifyLost = 0;
|
||||
notifyLossTracker = 0;
|
||||
notifyBufferCount = 0;
|
||||
RollingReliableSends = new RollingStat(64);
|
||||
}
|
||||
|
||||
/// <summary>Resets all non-rolling metrics to 0.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
UnreliableBytesIn = 0;
|
||||
UnreliableBytesOut = 0;
|
||||
UnreliableIn = 0;
|
||||
UnreliableOut = 0;
|
||||
|
||||
NotifyBytesIn = 0;
|
||||
NotifyBytesOut = 0;
|
||||
NotifyIn = 0;
|
||||
NotifyOut = 0;
|
||||
NotifyDiscarded = 0;
|
||||
NotifyLost = 0;
|
||||
NotifyDelivered = 0;
|
||||
|
||||
ReliableBytesIn = 0;
|
||||
ReliableBytesOut = 0;
|
||||
ReliableIn = 0;
|
||||
ReliableOut = 0;
|
||||
ReliableDiscarded = 0;
|
||||
ReliableUniques = 0;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving an unreliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedUnreliable(int byteCount)
|
||||
{
|
||||
UnreliableBytesIn += byteCount;
|
||||
UnreliableIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending an unreliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentUnreliable(int byteCount)
|
||||
{
|
||||
UnreliableBytesOut += byteCount;
|
||||
UnreliableOut++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving a notify message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedNotify(int byteCount)
|
||||
{
|
||||
NotifyBytesIn += byteCount;
|
||||
NotifyIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending a notify message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentNotify(int byteCount)
|
||||
{
|
||||
NotifyBytesOut += byteCount;
|
||||
NotifyOut++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with delivering a notify message.</summary>
|
||||
internal void DeliveredNotify()
|
||||
{
|
||||
NotifyDelivered++;
|
||||
|
||||
if (notifyBufferCount < 64)
|
||||
{
|
||||
RollingNotifyDelivered++;
|
||||
notifyBufferCount++;
|
||||
}
|
||||
else if ((notifyLossTracker & ULongLeftBit) == 0)
|
||||
{
|
||||
// The one being removed from the buffer was not delivered
|
||||
RollingNotifyDelivered++;
|
||||
RollingNotifyLost--;
|
||||
}
|
||||
|
||||
notifyLossTracker <<= 1;
|
||||
notifyLossTracker |= 1;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with losing a notify message.</summary>
|
||||
internal void LostNotify()
|
||||
{
|
||||
NotifyLost++;
|
||||
|
||||
if (notifyBufferCount < 64)
|
||||
{
|
||||
RollingNotifyLost++;
|
||||
notifyBufferCount++;
|
||||
}
|
||||
else if ((notifyLossTracker & ULongLeftBit) != 0)
|
||||
{
|
||||
// The one being removed from the buffer was delivered
|
||||
RollingNotifyDelivered--;
|
||||
RollingNotifyLost++;
|
||||
}
|
||||
|
||||
notifyLossTracker <<= 1;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with receiving a reliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were received.</param>
|
||||
internal void ReceivedReliable(int byteCount)
|
||||
{
|
||||
ReliableBytesIn += byteCount;
|
||||
ReliableIn++;
|
||||
}
|
||||
|
||||
/// <summary>Updates the metrics associated with sending a reliable message.</summary>
|
||||
/// <param name="byteCount">The number of bytes that were sent.</param>
|
||||
internal void SentReliable(int byteCount)
|
||||
{
|
||||
ReliableBytesOut += byteCount;
|
||||
ReliableOut++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,954 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Provides functionality for converting bits and bytes to various value types and vice versa.</summary>
|
||||
public class Converter
|
||||
{
|
||||
/// <summary>The number of bits in a byte.</summary>
|
||||
public const int BitsPerByte = 8;
|
||||
/// <summary>The number of bits in a ulong.</summary>
|
||||
public const int BitsPerULong = sizeof(ulong) * BitsPerByte;
|
||||
|
||||
#region Zig Zag Encoding
|
||||
/// <summary>Zig zag encodes <paramref name="value"/>.</summary>
|
||||
/// <param name="value">The value to encode.</param>
|
||||
/// <returns>The zig zag-encoded value.</returns>
|
||||
/// <remarks>Zig zag encoding allows small negative numbers to be represented as small positive numbers. All positive numbers are doubled and become even numbers,
|
||||
/// while all negative numbers become positive odd numbers. In contrast, simply casting a negative value to its unsigned counterpart would result in a large positive
|
||||
/// number which uses the high bit, rendering compression via <see cref="Message.AddVarULong(ulong)"/> and <see cref="Message.GetVarULong"/> ineffective.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ZigZagEncode(int value)
|
||||
{
|
||||
return (value >> 31) ^ (value << 1);
|
||||
}
|
||||
/// <inheritdoc cref="ZigZagEncode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ZigZagEncode(long value)
|
||||
{
|
||||
return (value >> 63) ^ (value << 1);
|
||||
}
|
||||
|
||||
/// <summary>Zig zag decodes <paramref name="value"/>.</summary>
|
||||
/// <param name="value">The value to decode.</param>
|
||||
/// <returns>The zig zag-decoded value.</returns>
|
||||
/// <inheritdoc cref="ZigZagEncode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ZigZagDecode(int value)
|
||||
{
|
||||
return (value >> 1) ^ -(value & 1);
|
||||
}
|
||||
/// <inheritdoc cref="ZigZagDecode(int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ZigZagDecode(long value)
|
||||
{
|
||||
return (value >> 1) ^ -(value & 1);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Bits
|
||||
/// <summary>Takes <paramref name="amount"/> bits from <paramref name="bitfield"/> and writes them into <paramref name="array"/>, starting at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="bitfield">The bitfield from which to write the bits into the array.</param>
|
||||
/// <param name="amount">The number of bits to write.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The bit position in the array at which to start writing.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(byte bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
byte mask = (byte)((1 << amount) - 1);
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
array[pos + 1] = (byte)((bitfield >> (8 - bit)) | (array[pos + 1] & (inverseMask >> (8 - bit))));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ushort bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
ushort mask = (ushort)((1 << amount) - 1);
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(uint bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
uint mask = (1u << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
uint inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
array[pos + 2] = (byte)((bitfield >> 16) | (array[pos + 2] & (inverseMask >> 16)));
|
||||
array[pos + 3] = (byte)((bitfield >> 24) | (array[pos + 3] & (inverseMask >> 24)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
array[pos + 3] = (byte)((bitfield >> 16) | (array[pos + 3] & (inverseMask >> 16)));
|
||||
array[pos + 4] = (byte)((bitfield >> 24) | (array[pos + 4] & ~(mask >> (32 - bit)))); // This one can't use inverseMask because it would have incorrectly zeroed bits
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ulong bitfield, int amount, byte[] array, int startBit)
|
||||
{
|
||||
ulong mask = (1ul << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
ulong inverseMask = ~mask;
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)(bitfield | (array[pos] & inverseMask));
|
||||
array[pos + 1] = (byte)((bitfield >> 8) | (array[pos + 1] & (inverseMask >> 8)));
|
||||
array[pos + 2] = (byte)((bitfield >> 16) | (array[pos + 2] & (inverseMask >> 16)));
|
||||
array[pos + 3] = (byte)((bitfield >> 24) | (array[pos + 3] & (inverseMask >> 24)));
|
||||
array[pos + 4] = (byte)((bitfield >> 32) | (array[pos + 4] & (inverseMask >> 32)));
|
||||
array[pos + 5] = (byte)((bitfield >> 40) | (array[pos + 5] & (inverseMask >> 40)));
|
||||
array[pos + 6] = (byte)((bitfield >> 48) | (array[pos + 6] & (inverseMask >> 48)));
|
||||
array[pos + 7] = (byte)((bitfield >> 56) | (array[pos + 7] & (inverseMask >> 56)));
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] = (byte)((bitfield << bit) | (array[pos] & ~(mask << bit)));
|
||||
bitfield >>= 8 - bit;
|
||||
inverseMask >>= 8 - bit;
|
||||
array[pos + 1] = (byte)(bitfield | (array[pos + 1] & inverseMask));
|
||||
array[pos + 2] = (byte)((bitfield >> 8) | (array[pos + 2] & (inverseMask >> 8)));
|
||||
array[pos + 3] = (byte)((bitfield >> 16) | (array[pos + 3] & (inverseMask >> 16)));
|
||||
array[pos + 4] = (byte)((bitfield >> 24) | (array[pos + 4] & (inverseMask >> 24)));
|
||||
array[pos + 5] = (byte)((bitfield >> 32) | (array[pos + 5] & (inverseMask >> 32)));
|
||||
array[pos + 6] = (byte)((bitfield >> 40) | (array[pos + 6] & (inverseMask >> 40)));
|
||||
array[pos + 7] = (byte)((bitfield >> 48) | (array[pos + 7] & (inverseMask >> 48)));
|
||||
array[pos + 8] = (byte)((bitfield >> 56) | (array[pos + 8] & ~(mask >> (64 - bit)))); // This one can't use inverseMask because it would have incorrectly zeroed bits
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="SetBits(byte, int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBits(ulong bitfield, int amount, ulong[] array, int startBit)
|
||||
{
|
||||
ulong mask = (1ul << (amount - 1) << 1) - 1; // Perform 2 shifts, doing it in 1 doesn't cause the value to wrap properly
|
||||
bitfield &= mask; // Discard any bits that are set beyond the ones we're setting
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = bitfield | array[pos] & ~mask;
|
||||
else
|
||||
{
|
||||
array[pos] = (bitfield << bit) | (array[pos] & ~(mask << bit));
|
||||
if (bit + amount >= BitsPerULong)
|
||||
array[pos + 1] = (bitfield >> (64 - bit)) | (array[pos + 1] & ~(mask >> (64 - bit)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starting at <paramref name="startBit"/>, reads <paramref name="amount"/> bits from <paramref name="array"/> into <paramref name="bitfield"/>.</summary>
|
||||
/// <param name="amount">The number of bits to read.</param>
|
||||
/// <param name="array">The array to read the bits from.</param>
|
||||
/// <param name="startBit">The bit position in the array at which to start reading.</param>
|
||||
/// <param name="bitfield">The bitfield into which to write the bits from the array.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out byte bitfield)
|
||||
{
|
||||
bitfield = ByteFromBits(array, startBit);
|
||||
bitfield &= (byte)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out ushort bitfield)
|
||||
{
|
||||
bitfield = UShortFromBits(array, startBit);
|
||||
bitfield &= (ushort)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out uint bitfield)
|
||||
{
|
||||
bitfield = UIntFromBits(array, startBit);
|
||||
bitfield &= (1u << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, byte[] array, int startBit, out ulong bitfield)
|
||||
{
|
||||
bitfield = ULongFromBits(array, startBit);
|
||||
bitfield &= (1ul << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out byte bitfield)
|
||||
{
|
||||
bitfield = ByteFromBits(array, startBit);
|
||||
bitfield &= (byte)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out ushort bitfield)
|
||||
{
|
||||
bitfield = UShortFromBits(array, startBit);
|
||||
bitfield &= (ushort)((1 << amount) - 1); // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out uint bitfield)
|
||||
{
|
||||
bitfield = UIntFromBits(array, startBit);
|
||||
bitfield &= (1u << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
/// <inheritdoc cref="GetBits(int, byte[], int, out byte)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetBits(int amount, ulong[] array, int startBit, out ulong bitfield)
|
||||
{
|
||||
bitfield = ULongFromBits(array, startBit);
|
||||
bitfield &= (1ul << (amount - 1) << 1) - 1; // Discard any bits that are set beyond the ones we're reading
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Byte/SByte
|
||||
/// <summary>Converts <paramref name="value"/> to 8 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="sbyte"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SByteToBits(sbyte value, byte[] array, int startBit) => ByteToBits((byte)value, array, startBit);
|
||||
/// <inheritdoc cref="SByteToBits(sbyte, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SByteToBits(sbyte value, ulong[] array, int startBit) => ByteToBits((byte)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 8 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="byte"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ByteToBits(byte value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
array[pos + 1] = (byte)(value >> (8 - bit));
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="ByteToBits(byte, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ByteToBits(byte value, ulong[] array, int startBit) => ToBits(value, BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 8 bits at <paramref name="startBit"/> in <paramref name="array"/> to an <see cref="sbyte"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="sbyte"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static sbyte SByteFromBits(byte[] array, int startBit) => (sbyte)ByteFromBits(array, startBit);
|
||||
/// <inheritdoc cref="SByteFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static sbyte SByteFromBits(ulong[] array, int startBit) => (sbyte)ByteFromBits(array, startBit);
|
||||
/// <summary>Converts the 8 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="byte"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="byte"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte ByteFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
byte value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return (byte)(value | (array[pos + 1] << (8 - bit)));
|
||||
}
|
||||
/// <inheritdoc cref="ByteFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte ByteFromBits(ulong[] array, int startBit) => (byte)FromBits(BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Bool
|
||||
/// <summary>Converts <paramref name="value"/> to a bit and writes it into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="bool"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bit into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bit.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BoolToBit(bool value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
array[pos] = 0;
|
||||
|
||||
if (value)
|
||||
array[pos] |= (byte)(1 << bit);
|
||||
}
|
||||
/// <inheritdoc cref="BoolToBit(bool, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BoolToBit(bool value, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = 0;
|
||||
|
||||
if (value)
|
||||
array[pos] |= 1ul << bit;
|
||||
}
|
||||
|
||||
/// <summary>Converts the bit at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="bool"/>.</summary>
|
||||
/// <param name="array">The array to convert the bit from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bit.</param>
|
||||
/// <returns>The converted <see cref="bool"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool BoolFromBit(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
return (array[pos] & (1 << bit)) != 0;
|
||||
}
|
||||
/// <inheritdoc cref="BoolFromBit(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool BoolFromBit(ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
return (array[pos] & (1ul << bit)) != 0;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Short/UShort
|
||||
/// <summary>Converts a given <see cref="short"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="short"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromShort(short value, byte[] array, int startIndex) => FromUShort((ushort)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="ushort"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="ushort"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromUShort(ushort value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 1] = (byte)value;
|
||||
array[startIndex ] = (byte)(value >> 8);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 2 bytes in the array at <paramref name="startIndex"/> to a <see cref="short"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="short"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ToShort(byte[] array, int startIndex) => (short)ToUShort(array, startIndex);
|
||||
/// <summary>Converts the 2 bytes in the array at <paramref name="startIndex"/> to a <see cref="ushort"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="ushort"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort ToUShort(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return (ushort)(array[startIndex + 1] | (array[startIndex ] << 8));
|
||||
#else
|
||||
return (ushort)(array[startIndex ] | (array[startIndex + 1] << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 16 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="short"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ShortToBits(short value, byte[] array, int startBit) => UShortToBits((ushort)value, array, startBit);
|
||||
/// <inheritdoc cref="ShortToBits(short, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ShortToBits(short value, ulong[] array, int startBit) => UShortToBits((ushort)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 16 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="ushort"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UShortToBits(ushort value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="UShortToBits(ushort, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UShortToBits(ushort value, ulong[] array, int startBit) => ToBits(value, sizeof(ushort) * BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 16 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="short"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="short"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ShortFromBits(byte[] array, int startBit) => (short)UShortFromBits(array, startBit);
|
||||
/// <inheritdoc cref="ShortFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short ShortFromBits(ulong[] array, int startBit) => (short)UShortFromBits(array, startBit);
|
||||
/// <summary>Converts the 16 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ushort"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ushort"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort UShortFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
ushort value = (ushort)(array[pos] | (array[pos + 1] << 8));
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return (ushort)(value | (array[pos + 2] << (16 - bit)));
|
||||
}
|
||||
/// <inheritdoc cref="UShortFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort UShortFromBits(ulong[] array, int startBit) => (ushort)FromBits(sizeof(ushort) * BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Int/UInt
|
||||
/// <summary>Converts a given <see cref="int"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="int"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromInt(int value, byte[] array, int startIndex) => FromUInt((uint)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="uint"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="uint"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromUInt(uint value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 3] = (byte)value;
|
||||
array[startIndex + 2] = (byte)(value >> 8);
|
||||
array[startIndex + 1] = (byte)(value >> 16);
|
||||
array[startIndex ] = (byte)(value >> 24);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
array[startIndex + 2] = (byte)(value >> 16);
|
||||
array[startIndex + 3] = (byte)(value >> 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="int"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="int"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ToInt(byte[] array, int startIndex) => (int)ToUInt(array, startIndex);
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="uint"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="uint"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint ToUInt(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return (uint)(array[startIndex + 3] | (array[startIndex + 2] << 8) | (array[startIndex + 1] << 16) | (array[startIndex ] << 24));
|
||||
#else
|
||||
return (uint)(array[startIndex ] | (array[startIndex + 1] << 8) | (array[startIndex + 2] << 16) | (array[startIndex + 3] << 24));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="int"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IntToBits(int value, byte[] array, int startBit) => UIntToBits((uint)value, array, startBit);
|
||||
/// <inheritdoc cref="IntToBits(int, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IntToBits(int value, ulong[] array, int startBit) => UIntToBits((uint)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="uint"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UIntToBits(uint value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
array[pos + 2] = (byte)(value >> 16);
|
||||
array[pos + 3] = (byte)(value >> 24);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
array[pos + 3] = (byte)(value >> 16);
|
||||
array[pos + 4] = (byte)(value >> 24);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="UIntToBits(uint, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void UIntToBits(uint value, ulong[] array, int startBit) => ToBits(value, sizeof(uint) * BitsPerByte, array, startBit);
|
||||
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to an <see cref="int"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="int"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int IntFromBits(byte[] array, int startBit) => (int)UIntFromBits(array, startBit);
|
||||
/// <inheritdoc cref="IntFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int IntFromBits(ulong[] array, int startBit) => (int)UIntFromBits(array, startBit);
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="uint"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="uint"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint UIntFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
uint value = (uint)(array[pos] | (array[pos + 1] << 8) | (array[pos + 2] << 16) | (array[pos + 3] << 24));
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | (uint)(array[pos + 4] << (32 - bit));
|
||||
}
|
||||
/// <inheritdoc cref="UIntFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint UIntFromBits(ulong[] array, int startBit) => (uint)FromBits(sizeof(uint) * BitsPerByte, array, startBit);
|
||||
#endregion
|
||||
|
||||
#region Long/ULong
|
||||
/// <summary>Converts a given <see cref="long"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="long"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromLong(long value, byte[] array, int startIndex) => FromULong((ulong)value, array, startIndex);
|
||||
/// <summary>Converts a given <see cref="ulong"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="ulong"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromULong(ulong value, byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 7] = (byte)value;
|
||||
array[startIndex + 6] = (byte)(value >> 8);
|
||||
array[startIndex + 5] = (byte)(value >> 16);
|
||||
array[startIndex + 4] = (byte)(value >> 24);
|
||||
array[startIndex + 3] = (byte)(value >> 32);
|
||||
array[startIndex + 2] = (byte)(value >> 40);
|
||||
array[startIndex + 1] = (byte)(value >> 48);
|
||||
array[startIndex ] = (byte)(value >> 56);
|
||||
#else
|
||||
array[startIndex ] = (byte)value;
|
||||
array[startIndex + 1] = (byte)(value >> 8);
|
||||
array[startIndex + 2] = (byte)(value >> 16);
|
||||
array[startIndex + 3] = (byte)(value >> 24);
|
||||
array[startIndex + 4] = (byte)(value >> 32);
|
||||
array[startIndex + 5] = (byte)(value >> 40);
|
||||
array[startIndex + 6] = (byte)(value >> 48);
|
||||
array[startIndex + 7] = (byte)(value >> 56);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="long"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="long"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long ToLong(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, longLength);
|
||||
#endif
|
||||
return BitConverter.ToInt64(array, startIndex);
|
||||
}
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="ulong"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ToULong(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, ulongLength);
|
||||
#endif
|
||||
return BitConverter.ToUInt64(array, startIndex);
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="long"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LongToBits(long value, byte[] array, int startBit) => ULongToBits((ulong)value, array, startBit);
|
||||
/// <inheritdoc cref="LongToBits(long, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LongToBits(long value, ulong[] array, int startBit) => ULongToBits((ulong)value, array, startBit);
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="ulong"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ULongToBits(ulong value, byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
if (bit == 0)
|
||||
{
|
||||
array[pos ] = (byte)value;
|
||||
array[pos + 1] = (byte)(value >> 8);
|
||||
array[pos + 2] = (byte)(value >> 16);
|
||||
array[pos + 3] = (byte)(value >> 24);
|
||||
array[pos + 4] = (byte)(value >> 32);
|
||||
array[pos + 5] = (byte)(value >> 40);
|
||||
array[pos + 6] = (byte)(value >> 48);
|
||||
array[pos + 7] = (byte)(value >> 56);
|
||||
}
|
||||
else
|
||||
{
|
||||
array[pos ] |= (byte)(value << bit);
|
||||
value >>= 8 - bit;
|
||||
array[pos + 1] = (byte)value;
|
||||
array[pos + 2] = (byte)(value >> 8);
|
||||
array[pos + 3] = (byte)(value >> 16);
|
||||
array[pos + 4] = (byte)(value >> 24);
|
||||
array[pos + 5] = (byte)(value >> 32);
|
||||
array[pos + 6] = (byte)(value >> 40);
|
||||
array[pos + 7] = (byte)(value >> 48);
|
||||
array[pos + 8] = (byte)(value >> 56);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="ULongToBits(ulong, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ULongToBits(ulong value, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else
|
||||
{
|
||||
array[pos ] |= value << bit;
|
||||
array[pos + 1] = value >> (BitsPerULong - bit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="long"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="long"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long LongFromBits(byte[] array, int startBit) => (long)ULongFromBits(array, startBit);
|
||||
/// <inheritdoc cref="LongFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long LongFromBits(ulong[] array, int startBit) => (long)ULongFromBits(array, startBit);
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ulong"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ULongFromBits(byte[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerByte;
|
||||
int bit = startBit % BitsPerByte;
|
||||
ulong value = BitConverter.ToUInt64(array, pos);
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | ((ulong)array[pos + 8] << (64 - bit));
|
||||
}
|
||||
/// <inheritdoc cref="ULongFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong ULongFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
ulong value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
return value | (array[pos + 1] << (BitsPerULong - bit));
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to <paramref name="valueSize"/> bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.
|
||||
/// Meant for values which fit into a <see cref="ulong"/>, not for <see cref="ulong"/>s themselves.</summary>
|
||||
/// <param name="value">The value to convert.</param>
|
||||
/// <param name="valueSize">The size in bits of the value being converted.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ToBits(ulong value, int valueSize, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
if (bit == 0)
|
||||
array[pos] = value;
|
||||
else if (bit + valueSize < BitsPerULong)
|
||||
array[pos] |= value << bit;
|
||||
else
|
||||
{
|
||||
array[pos] |= value << bit;
|
||||
array[pos + 1] = value >> (BitsPerULong - bit);
|
||||
}
|
||||
}
|
||||
/// <summary>Converts the <paramref name="valueSize"/> bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="ulong"/>.
|
||||
/// Meant for values which fit into a <see cref="ulong"/>, not for <see cref="ulong"/>s themselves.</summary>
|
||||
/// <param name="valueSize">The size in bits of the value being converted.</param>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="ulong"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong FromBits(int valueSize, ulong[] array, int startBit)
|
||||
{
|
||||
int pos = startBit / BitsPerULong;
|
||||
int bit = startBit % BitsPerULong;
|
||||
ulong value = array[pos];
|
||||
if (bit == 0)
|
||||
return value;
|
||||
|
||||
value >>= bit;
|
||||
if (bit + valueSize < BitsPerULong)
|
||||
return value;
|
||||
|
||||
return value | (array[pos + 1] << (BitsPerULong - bit));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Float
|
||||
/// <summary>Converts a given <see cref="float"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="float"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromFloat(float value, byte[] array, int startIndex)
|
||||
{
|
||||
FloatConverter converter = new FloatConverter { FloatValue = value };
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 3] = converter.Byte0;
|
||||
array[startIndex + 2] = converter.Byte1;
|
||||
array[startIndex + 1] = converter.Byte2;
|
||||
array[startIndex ] = converter.Byte3;
|
||||
#else
|
||||
array[startIndex ] = converter.Byte0;
|
||||
array[startIndex + 1] = converter.Byte1;
|
||||
array[startIndex + 2] = converter.Byte2;
|
||||
array[startIndex + 3] = converter.Byte3;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 4 bytes in the array at <paramref name="startIndex"/> to a <see cref="float"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="float"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float ToFloat(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
return new FloatConverter { Byte3 = array[startIndex], Byte2 = array[startIndex + 1], Byte1 = array[startIndex + 2], Byte0 = array[startIndex + 3] }.FloatValue;
|
||||
#else
|
||||
return new FloatConverter { Byte0 = array[startIndex], Byte1 = array[startIndex + 1], Byte2 = array[startIndex + 2], Byte3 = array[startIndex + 3] }.FloatValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 32 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="float"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FloatToBits(float value, byte[] array, int startBit)
|
||||
{
|
||||
UIntToBits(new FloatConverter { FloatValue = value }.UIntValue, array, startBit);
|
||||
}
|
||||
/// <inheritdoc cref="FloatToBits(float, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FloatToBits(float value, ulong[] array, int startBit)
|
||||
{
|
||||
UIntToBits(new FloatConverter { FloatValue = value }.UIntValue, array, startBit);
|
||||
}
|
||||
|
||||
/// <summary>Converts the 32 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="float"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="float"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float FloatFromBits(byte[] array, int startBit)
|
||||
{
|
||||
return new FloatConverter { UIntValue = UIntFromBits(array, startBit) }.FloatValue;
|
||||
}
|
||||
/// <inheritdoc cref="FloatFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float FloatFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
return new FloatConverter { UIntValue = UIntFromBits(array, startBit) }.FloatValue;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Double
|
||||
/// <summary>Converts a given <see cref="double"/> to bytes and writes them into the given array.</summary>
|
||||
/// <param name="value">The <see cref="double"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bytes into.</param>
|
||||
/// <param name="startIndex">The position in the array at which to write the bytes.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void FromDouble(double value, byte[] array, int startIndex)
|
||||
{
|
||||
DoubleConverter converter = new DoubleConverter { DoubleValue = value };
|
||||
#if BIG_ENDIAN
|
||||
array[startIndex + 7] = converter.Byte0;
|
||||
array[startIndex + 6] = converter.Byte1;
|
||||
array[startIndex + 5] = converter.Byte2;
|
||||
array[startIndex + 4] = converter.Byte3;
|
||||
array[startIndex + 3] = converter.Byte4;
|
||||
array[startIndex + 2] = converter.Byte5;
|
||||
array[startIndex + 1] = converter.Byte6;
|
||||
array[startIndex ] = converter.Byte7;
|
||||
#else
|
||||
array[startIndex ] = converter.Byte0;
|
||||
array[startIndex + 1] = converter.Byte1;
|
||||
array[startIndex + 2] = converter.Byte2;
|
||||
array[startIndex + 3] = converter.Byte3;
|
||||
array[startIndex + 4] = converter.Byte4;
|
||||
array[startIndex + 5] = converter.Byte5;
|
||||
array[startIndex + 6] = converter.Byte6;
|
||||
array[startIndex + 7] = converter.Byte7;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Converts the 8 bytes in the array at <paramref name="startIndex"/> to a <see cref="double"/>.</summary>
|
||||
/// <param name="array">The array to read the bytes from.</param>
|
||||
/// <param name="startIndex">The position in the array at which to read the bytes.</param>
|
||||
/// <returns>The converted <see cref="double"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double ToDouble(byte[] array, int startIndex)
|
||||
{
|
||||
#if BIG_ENDIAN
|
||||
Array.Reverse(array, startIndex, doubleLength);
|
||||
#endif
|
||||
return BitConverter.ToDouble(array, startIndex);
|
||||
}
|
||||
|
||||
/// <summary>Converts <paramref name="value"/> to 64 bits and writes them into <paramref name="array"/> at <paramref name="startBit"/>.</summary>
|
||||
/// <param name="value">The <see cref="double"/> to convert.</param>
|
||||
/// <param name="array">The array to write the bits into.</param>
|
||||
/// <param name="startBit">The position in the array at which to write the bits.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DoubleToBits(double value, byte[] array, int startBit)
|
||||
{
|
||||
ULongToBits(new DoubleConverter { DoubleValue = value }.ULongValue, array, startBit);
|
||||
}
|
||||
/// <inheritdoc cref="DoubleToBits(double, byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DoubleToBits(double value, ulong[] array, int startBit)
|
||||
{
|
||||
ULongToBits(new DoubleConverter { DoubleValue = value }.ULongValue, array, startBit);
|
||||
}
|
||||
|
||||
/// <summary>Converts the 64 bits at <paramref name="startBit"/> in <paramref name="array"/> to a <see cref="double"/>.</summary>
|
||||
/// <param name="array">The array to convert the bits from.</param>
|
||||
/// <param name="startBit">The position in the array from which to read the bits.</param>
|
||||
/// <returns>The converted <see cref="double"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double DoubleFromBits(byte[] array, int startBit)
|
||||
{
|
||||
return new DoubleConverter { ULongValue = ULongFromBits(array, startBit) }.DoubleValue;
|
||||
}
|
||||
/// <inheritdoc cref="DoubleFromBits(byte[], int)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double DoubleFromBits(ulong[] array, int startBit)
|
||||
{
|
||||
return new DoubleConverter { ULongValue = ULongFromBits(array, startBit) }.DoubleValue;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct FloatConverter
|
||||
{
|
||||
[FieldOffset(0)] public byte Byte0;
|
||||
[FieldOffset(1)] public byte Byte1;
|
||||
[FieldOffset(2)] public byte Byte2;
|
||||
[FieldOffset(3)] public byte Byte3;
|
||||
|
||||
[FieldOffset(0)] public float FloatValue;
|
||||
|
||||
[FieldOffset(0)] public uint UIntValue;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct DoubleConverter
|
||||
{
|
||||
[FieldOffset(0)] public byte Byte0;
|
||||
[FieldOffset(1)] public byte Byte1;
|
||||
[FieldOffset(2)] public byte Byte2;
|
||||
[FieldOffset(3)] public byte Byte3;
|
||||
[FieldOffset(4)] public byte Byte4;
|
||||
[FieldOffset(5)] public byte Byte5;
|
||||
[FieldOffset(6)] public byte Byte6;
|
||||
[FieldOffset(7)] public byte Byte7;
|
||||
|
||||
[FieldOffset(0)] public double DoubleValue;
|
||||
|
||||
[FieldOffset(0)] public ulong ULongValue;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using Riptide.Transports;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Executes an action when invoked.</summary>
|
||||
internal abstract class DelayedEvent
|
||||
{
|
||||
/// <summary>Executes the action.</summary>
|
||||
public abstract void Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Resends a <see cref="PendingMessage"/> when invoked.</summary>
|
||||
internal class ResendEvent : DelayedEvent
|
||||
{
|
||||
/// <summary>The message to resend.</summary>
|
||||
private readonly PendingMessage message;
|
||||
/// <summary>The time at which the resend event was queued.</summary>
|
||||
private readonly long initiatedAtTime;
|
||||
|
||||
/// <summary>Initializes the event.</summary>
|
||||
/// <param name="message">The message to resend.</param>
|
||||
/// <param name="initiatedAtTime">The time at which the resend event was queued.</param>
|
||||
public ResendEvent(PendingMessage message, long initiatedAtTime)
|
||||
{
|
||||
this.message = message;
|
||||
this.initiatedAtTime = initiatedAtTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke()
|
||||
{
|
||||
if (initiatedAtTime == message.LastSendTime) // If this isn't the case then the message has been resent already
|
||||
message.RetrySend();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Executes a heartbeat when invoked.</summary>
|
||||
internal class HeartbeatEvent : DelayedEvent
|
||||
{
|
||||
/// <summary>The peer whose heart to beat.</summary>
|
||||
private readonly Peer peer;
|
||||
|
||||
/// <summary>Initializes the event.</summary>
|
||||
/// <param name="peer">The peer whose heart to beat.</param>
|
||||
public HeartbeatEvent(Peer peer)
|
||||
{
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke()
|
||||
{
|
||||
peer.Heartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Contains extension methods for various classes.</summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>Takes the <see cref="IPEndPoint"/>'s IP address and port number and converts it to a string, accounting for whether the address is an IPv4 or IPv6 address.</summary>
|
||||
/// <returns>A string containing the IP address and port number of the endpoint.</returns>
|
||||
public static string ToStringBasedOnIPFormat(this IPEndPoint endPoint)
|
||||
{
|
||||
if (endPoint.Address.IsIPv4MappedToIPv6)
|
||||
return $"{endPoint.Address.MapToIPv4()}:{endPoint.Port}";
|
||||
|
||||
return endPoint.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Contains miscellaneous helper methods.</summary>
|
||||
internal class Helper
|
||||
{
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.NeverConnected"/>.</summary>
|
||||
private const string DCNeverConnected = "Never connected";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.TransportError"/>.</summary>
|
||||
private const string DCTransportError = "Transport error";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.TimedOut"/>.</summary>
|
||||
private const string DCTimedOut = "Timed out";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.Kicked"/>.</summary>
|
||||
private const string DCKicked = "Kicked";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.ServerStopped"/>.</summary>
|
||||
private const string DCServerStopped = "Server stopped";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.Disconnected"/>.</summary>
|
||||
private const string DCDisconnected = "Disconnected";
|
||||
/// <summary>The text to log when disconnected due to <see cref="DisconnectReason.PoorConnection"/>.</summary>
|
||||
private const string DCPoorConnection = "Poor connection";
|
||||
/// <summary>The text to log when disconnected or rejected due to an unknown reason.</summary>
|
||||
private const string UnknownReason = "Unknown reason";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.NoConnection"/>.</summary>
|
||||
private const string CRNoConnection = "No connection";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.AlreadyConnected"/>.</summary>
|
||||
private const string CRAlreadyConnected = "This client is already connected";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.ServerFull"/>.</summary>
|
||||
private const string CRServerFull = "Server is full";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.Rejected"/>.</summary>
|
||||
private const string CRRejected = "Rejected";
|
||||
/// <summary>The text to log when the connection failed due to <see cref="RejectReason.Custom"/>.</summary>
|
||||
private const string CRCustom = "Rejected (with custom data)";
|
||||
|
||||
/// <summary>Determines whether <paramref name="singular"/> or <paramref name="plural"/> form should be used based on the <paramref name="amount"/>.</summary>
|
||||
/// <param name="amount">The amount that <paramref name="singular"/> and <paramref name="plural"/> refer to.</param>
|
||||
/// <param name="singular">The singular form.</param>
|
||||
/// <param name="plural">The plural form.</param>
|
||||
/// <returns><paramref name="singular"/> if <paramref name="amount"/> is 1; otherwise <paramref name="plural"/>.</returns>
|
||||
internal static string CorrectForm(int amount, string singular, string plural = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(plural))
|
||||
plural = $"{singular}s";
|
||||
|
||||
return amount == 1 ? singular : plural;
|
||||
}
|
||||
|
||||
/// <summary>Calculates the signed gap between sequence IDs, accounting for wrapping.</summary>
|
||||
/// <param name="seqId1">The new sequence ID.</param>
|
||||
/// <param name="seqId2">The previous sequence ID.</param>
|
||||
/// <returns>The signed gap between the two given sequence IDs. A positive gap means <paramref name="seqId1"/> is newer than <paramref name="seqId2"/>. A negative gap means <paramref name="seqId1"/> is older than <paramref name="seqId2"/>.</returns>
|
||||
internal static int GetSequenceGap(ushort seqId1, ushort seqId2)
|
||||
{
|
||||
int gap = seqId1 - seqId2;
|
||||
if (Math.Abs(gap) <= 32768) // Difference is small, meaning sequence IDs are close together
|
||||
return gap;
|
||||
else // Difference is big, meaning sequence IDs are far apart
|
||||
return (seqId1 <= 32768 ? ushort.MaxValue + 1 + seqId1 : seqId1) - (seqId2 <= 32768 ? ushort.MaxValue + 1 + seqId2 : seqId2);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the appropriate reason string for the given <see cref="DisconnectReason"/>.</summary>
|
||||
/// <param name="forReason">The <see cref="DisconnectReason"/> to retrieve the string for.</param>
|
||||
/// <returns>The appropriate reason string.</returns>
|
||||
internal static string GetReasonString(DisconnectReason forReason)
|
||||
{
|
||||
switch (forReason)
|
||||
{
|
||||
case DisconnectReason.NeverConnected:
|
||||
return DCNeverConnected;
|
||||
case DisconnectReason.TransportError:
|
||||
return DCTransportError;
|
||||
case DisconnectReason.TimedOut:
|
||||
return DCTimedOut;
|
||||
case DisconnectReason.Kicked:
|
||||
return DCKicked;
|
||||
case DisconnectReason.ServerStopped:
|
||||
return DCServerStopped;
|
||||
case DisconnectReason.Disconnected:
|
||||
return DCDisconnected;
|
||||
case DisconnectReason.PoorConnection:
|
||||
return DCPoorConnection;
|
||||
default:
|
||||
return $"{UnknownReason} '{forReason}'";
|
||||
}
|
||||
}
|
||||
/// <summary>Retrieves the appropriate reason string for the given <see cref="RejectReason"/>.</summary>
|
||||
/// <param name="forReason">The <see cref="RejectReason"/> to retrieve the string for.</param>
|
||||
/// <returns>The appropriate reason string.</returns>
|
||||
internal static string GetReasonString(RejectReason forReason)
|
||||
{
|
||||
switch (forReason)
|
||||
{
|
||||
case RejectReason.NoConnection:
|
||||
return CRNoConnection;
|
||||
case RejectReason.AlreadyConnected:
|
||||
return CRAlreadyConnected;
|
||||
case RejectReason.ServerFull:
|
||||
return CRServerFull;
|
||||
case RejectReason.Rejected:
|
||||
return CRRejected;
|
||||
case RejectReason.Custom:
|
||||
return CRCustom;
|
||||
default:
|
||||
return $"{UnknownReason} '{forReason}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
// PriorityQueue unfortunately doesn't exist in .NET Standard 2.1
|
||||
/// <summary>Represents a collection of items that have a value and a priority. On dequeue, the item with the lowest priority value is removed.</summary>
|
||||
/// <typeparam name="TElement">Specifies the type of elements in the queue.</typeparam>
|
||||
/// <typeparam name="TPriority">Specifies the type of priority associated with enqueued elements.</typeparam>
|
||||
public class PriorityQueue<TElement, TPriority>
|
||||
{
|
||||
/// <summary>Gets the number of elements contained in the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
private const int DefaultCapacity = 8;
|
||||
private Entry<TElement, TPriority>[] heap;
|
||||
private readonly IComparer<TPriority> comparer;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PriorityQueue{TElement, TPriority}"/> class.</summary>
|
||||
/// <param name="capacity">Initial capacity to allocate for the underlying heap array.</param>
|
||||
public PriorityQueue(int capacity = DefaultCapacity)
|
||||
{
|
||||
heap = new Entry<TElement, TPriority>[capacity];
|
||||
comparer = Comparer<TPriority>.Default;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PriorityQueue{TElement, TPriority}"/> class with the specified custom priority comparer.</summary>
|
||||
/// <param name="comparer">Custom comparer dictating the ordering of elements.</param>
|
||||
/// <param name="capacity">Initial capacity to allocate for the underlying heap array.</param>
|
||||
public PriorityQueue(IComparer<TPriority> comparer, int capacity = DefaultCapacity)
|
||||
{
|
||||
heap = new Entry<TElement, TPriority>[capacity];
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified element and associated priority to the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
/// <param name="element">The element to add.</param>
|
||||
/// <param name="priority">The priority with which to associate the new element.</param>
|
||||
public void Enqueue(TElement element, TPriority priority)
|
||||
{
|
||||
if (Count == heap.Length)
|
||||
{
|
||||
// Resizing is necessary
|
||||
Entry<TElement, TPriority>[] temp = new Entry<TElement, TPriority>[Count * 2];
|
||||
Array.Copy(heap, temp, heap.Length);
|
||||
heap = temp;
|
||||
}
|
||||
|
||||
int index = Count;
|
||||
while (index > 0)
|
||||
{
|
||||
int parentIndex = GetParentIndex(index);
|
||||
if (comparer.Compare(priority, heap[parentIndex].Priority) < 0)
|
||||
{
|
||||
heap[index] = heap[parentIndex];
|
||||
index = parentIndex;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
heap[index] = new Entry<TElement, TPriority>(element, priority);
|
||||
Count++;
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the lowest priority element.</summary>
|
||||
public TElement Dequeue()
|
||||
{
|
||||
TElement returnValue = heap[0].Element;
|
||||
|
||||
if (Count > 1)
|
||||
{
|
||||
int parent = 0;
|
||||
int leftChild = GetLeftChildIndex(parent);
|
||||
|
||||
while (leftChild < Count)
|
||||
{
|
||||
int rightChild = leftChild + 1;
|
||||
int bestChild = (rightChild < Count && comparer.Compare(heap[rightChild].Priority, heap[leftChild].Priority) < 0) ? rightChild : leftChild;
|
||||
|
||||
heap[parent] = heap[bestChild];
|
||||
parent = bestChild;
|
||||
leftChild = GetLeftChildIndex(parent);
|
||||
}
|
||||
|
||||
heap[parent] = heap[Count - 1];
|
||||
}
|
||||
|
||||
Count--;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Removes the lowest priority element from the <see cref="PriorityQueue{TElement, TPriority}"/> and copies it and its associated priority to the <paramref name="element"/> and <paramref name="priority"/> arguments.</summary>
|
||||
/// <param name="element">When this method returns, contains the removed element.</param>
|
||||
/// <param name="priority">When this method returns, contains the priority associated with the removed element.</param>
|
||||
/// <returns>true if the element is successfully removed; false if the <see cref="PriorityQueue{TElement, TPriority}"/> is empty.</returns>
|
||||
public bool TryDequeue(out TElement element, out TPriority priority)
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
priority = heap[0].Priority;
|
||||
element = Dequeue();
|
||||
return true;
|
||||
}
|
||||
{
|
||||
element = default(TElement);
|
||||
priority = default(TPriority);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the lowest priority element.</summary>
|
||||
public TElement Peek()
|
||||
{
|
||||
return heap[0].Element;
|
||||
}
|
||||
|
||||
/// <summary>Returns the priority of the lowest priority element.</summary>
|
||||
public TPriority PeekPriority()
|
||||
{
|
||||
return heap[0].Priority;
|
||||
}
|
||||
|
||||
/// <summary>Removes all elements from the <see cref="PriorityQueue{TElement, TPriority}"/>.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(heap, 0, heap.Length);
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
private static int GetParentIndex(int index)
|
||||
{
|
||||
return (index - 1) / 2;
|
||||
}
|
||||
|
||||
private static int GetLeftChildIndex(int index)
|
||||
{
|
||||
return (index * 2) + 1;
|
||||
}
|
||||
|
||||
private struct Entry<TEle, TPrio>
|
||||
{
|
||||
internal readonly TEle Element;
|
||||
internal readonly TPrio Priority;
|
||||
|
||||
public Entry(TEle element, TPrio priority)
|
||||
{
|
||||
Element = element;
|
||||
Priority = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Defines log message types.</summary>
|
||||
public enum LogType
|
||||
{
|
||||
/// <summary>Logs that are used for investigation during development.</summary>
|
||||
Debug,
|
||||
/// <summary>Logs that provide general information about application flow.</summary>
|
||||
Info,
|
||||
/// <summary>Logs that highlight abnormal or unexpected events in the application flow.</summary>
|
||||
Warning,
|
||||
/// <summary>Logs that highlight problematic events in the application flow which will cause unexpected behavior if not planned for.</summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>Provides functionality for logging messages.</summary>
|
||||
public class RiptideLogger
|
||||
{
|
||||
/// <summary>Whether or not <see cref="LogType.Debug"/> messages will be logged.</summary>
|
||||
public static bool IsDebugLoggingEnabled => logMethods.ContainsKey(LogType.Debug);
|
||||
/// <summary>Whether or not <see cref="LogType.Info"/> messages will be logged.</summary>
|
||||
public static bool IsInfoLoggingEnabled => logMethods.ContainsKey(LogType.Info);
|
||||
/// <summary>Whether or not <see cref="LogType.Warning"/> messages will be logged.</summary>
|
||||
public static bool IsWarningLoggingEnabled => logMethods.ContainsKey(LogType.Warning);
|
||||
/// <summary>Whether or not <see cref="LogType.Error"/> messages will be logged.</summary>
|
||||
public static bool IsErrorLoggingEnabled => logMethods.ContainsKey(LogType.Error);
|
||||
/// <summary>Encapsulates a method used to log messages.</summary>
|
||||
/// <param name="log">The message to log.</param>
|
||||
public delegate void LogMethod(string log);
|
||||
|
||||
/// <summary>Log methods, accessible by their <see cref="LogType"/></summary>
|
||||
private static readonly Dictionary<LogType, LogMethod> logMethods = new Dictionary<LogType, LogMethod>(4);
|
||||
/// <summary>Whether or not to include timestamps when logging messages.</summary>
|
||||
private static bool includeTimestamps;
|
||||
/// <summary>The format to use for timestamps.</summary>
|
||||
private static string timestampFormat;
|
||||
|
||||
/// <summary>Initializes <see cref="RiptideLogger"/> with all log types enabled.</summary>
|
||||
/// <param name="logMethod">The method to use when logging all types of messages.</param>
|
||||
/// <param name="includeTimestamps">Whether or not to include timestamps when logging messages.</param>
|
||||
/// <param name="timestampFormat">The format to use for timestamps.</param>
|
||||
public static void Initialize(LogMethod logMethod, bool includeTimestamps, string timestampFormat = "HH:mm:ss") => Initialize(logMethod, logMethod, logMethod, logMethod, includeTimestamps, timestampFormat);
|
||||
/// <summary>Initializes <see cref="RiptideLogger"/> with the supplied log methods.</summary>
|
||||
/// <param name="debugMethod">The method to use when logging debug messages. Set to <see langword="null"/> to disable debug logs.</param>
|
||||
/// <param name="infoMethod">The method to use when logging info messages. Set to <see langword="null"/> to disable info logs.</param>
|
||||
/// <param name="warningMethod">The method to use when logging warning messages. Set to <see langword="null"/> to disable warning logs.</param>
|
||||
/// <param name="errorMethod">The method to use when logging error messages. Set to <see langword="null"/> to disable error logs.</param>
|
||||
/// <param name="includeTimestamps">Whether or not to include timestamps when logging messages.</param>
|
||||
/// <param name="timestampFormat">The format to use for timestamps.</param>
|
||||
public static void Initialize(LogMethod debugMethod, LogMethod infoMethod, LogMethod warningMethod, LogMethod errorMethod, bool includeTimestamps, string timestampFormat = "HH:mm:ss")
|
||||
{
|
||||
logMethods.Clear();
|
||||
|
||||
if (debugMethod != null)
|
||||
logMethods.Add(LogType.Debug, debugMethod);
|
||||
if (infoMethod != null)
|
||||
logMethods.Add(LogType.Info, infoMethod);
|
||||
if (warningMethod != null)
|
||||
logMethods.Add(LogType.Warning, warningMethod);
|
||||
if (errorMethod != null)
|
||||
logMethods.Add(LogType.Error, errorMethod);
|
||||
|
||||
RiptideLogger.includeTimestamps = includeTimestamps;
|
||||
RiptideLogger.timestampFormat = timestampFormat;
|
||||
}
|
||||
|
||||
/// <summary>Enables logging for messages of the given <see cref="LogType"/>.</summary>
|
||||
/// <param name="logType">The type of message to enable logging for.</param>
|
||||
/// <param name="logMethod">The method to use when logging this type of message.</param>
|
||||
public static void EnableLoggingFor(LogType logType, LogMethod logMethod)
|
||||
{
|
||||
if (logMethods.ContainsKey(logType))
|
||||
logMethods[logType] = logMethod;
|
||||
else
|
||||
logMethods.Add(logType, logMethod);
|
||||
}
|
||||
|
||||
/// <summary>Disables logging for messages of the given <see cref="LogType"/>.</summary>
|
||||
/// <param name="logType">The type of message to enable logging for.</param>
|
||||
public static void DisableLoggingFor(LogType logType) => logMethods.Remove(logType);
|
||||
|
||||
/// <summary>Logs a message.</summary>
|
||||
/// <param name="logType">The type of log message that is being logged.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void Log(LogType logType, string message)
|
||||
{
|
||||
if (logMethods.TryGetValue(logType, out LogMethod logMethod))
|
||||
{
|
||||
if (includeTimestamps)
|
||||
logMethod($"[{GetTimestamp(DateTime.Now)}]: {message}");
|
||||
else
|
||||
logMethod(message);
|
||||
}
|
||||
}
|
||||
/// <summary>Logs a message.</summary>
|
||||
/// <param name="logType">The type of log message that is being logged.</param>
|
||||
/// <param name="logName">Who is logging this message.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void Log(LogType logType, string logName, string message)
|
||||
{
|
||||
if (logMethods.TryGetValue(logType, out LogMethod logMethod))
|
||||
{
|
||||
if (includeTimestamps)
|
||||
logMethod($"[{GetTimestamp(DateTime.Now)}] ({logName}): {message}");
|
||||
else
|
||||
logMethod($"({logName}): {message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts a <see cref="DateTime"/> object to a formatted timestamp string.</summary>
|
||||
/// <param name="time">The time to format.</param>
|
||||
/// <returns>The formatted timestamp.</returns>
|
||||
private static string GetTimestamp(DateTime time)
|
||||
{
|
||||
return time.ToString(timestampFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// This file is provided under The MIT License as part of RiptideNetworking.
|
||||
// Copyright (c) Tom Weiland
|
||||
// For additional information please see the included LICENSE.md file or view it on GitHub:
|
||||
// https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Riptide.Utils
|
||||
{
|
||||
/// <summary>Represents a rolling series of numbers.</summary>
|
||||
public class RollingStat
|
||||
{
|
||||
/// <summary>The position in the array of the latest item.</summary>
|
||||
private int index;
|
||||
/// <summary>How many of the array's slots are in use.</summary>
|
||||
private int slotsFilled;
|
||||
/// <inheritdoc cref="Mean"/>
|
||||
private double mean;
|
||||
/// <summary>The sum of the mean subtracted from each value in the array.</summary>
|
||||
private double sumOfSquares;
|
||||
/// <summary>The array used to store the values.</summary>
|
||||
private readonly double[] array;
|
||||
|
||||
/// <summary>The mean of the stat's values.</summary>
|
||||
public double Mean => mean;
|
||||
/// <summary>The variance of the stat's values.</summary>
|
||||
public double Variance => slotsFilled > 1 ? sumOfSquares / (slotsFilled - 1) : 0;
|
||||
/// <summary>The standard deviation of the stat's values.</summary>
|
||||
public double StandardDev
|
||||
{
|
||||
get
|
||||
{
|
||||
double variance = Variance;
|
||||
if (variance >= double.Epsilon)
|
||||
{
|
||||
double root = Math.Sqrt(variance);
|
||||
return double.IsNaN(root) ? 0 : root;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes the stat.</summary>
|
||||
/// <param name="sampleSize">The number of values to store.</param>
|
||||
public RollingStat(int sampleSize)
|
||||
{
|
||||
index = 0;
|
||||
slotsFilled = 0;
|
||||
mean = 0;
|
||||
sumOfSquares = 0;
|
||||
array = new double[sampleSize];
|
||||
}
|
||||
|
||||
/// <summary>Adds a new value to the stat.</summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public void Add(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
return;
|
||||
|
||||
index %= array.Length;
|
||||
double oldMean = mean;
|
||||
double oldValue = array[index];
|
||||
array[index] = value;
|
||||
index++;
|
||||
|
||||
if (slotsFilled == array.Length)
|
||||
{
|
||||
double delta = value - oldValue;
|
||||
mean += delta / slotsFilled;
|
||||
sumOfSquares += delta * (value - mean + (oldValue - oldMean));
|
||||
}
|
||||
else
|
||||
{
|
||||
slotsFilled++;
|
||||
double delta = value - oldMean;
|
||||
mean += delta / slotsFilled;
|
||||
sumOfSquares += delta * (value - mean);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
if (slotsFilled == array.Length)
|
||||
return string.Join(",", array);
|
||||
|
||||
return string.Join(",", array.Take(slotsFilled));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user