first commit

This commit is contained in:
2025-12-13 14:28:35 +01:00
commit 679c3c9a52
113 changed files with 715750 additions and 0 deletions

418
Riptide/Client.cs Normal file
View File

@@ -0,0 +1,418 @@
// 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
}
}

648
Riptide/Connection.cs Normal file
View File

@@ -0,0 +1,648 @@
// 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
}
}

135
Riptide/EventArgs.cs Normal file
View File

@@ -0,0 +1,135 @@
// 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;
}
}

197
Riptide/Exceptions.cs Normal file
View File

@@ -0,0 +1,197 @@
// 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!";
}
}
}

View File

@@ -0,0 +1,18 @@
// 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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
// 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;
}
}
}

View File

@@ -0,0 +1,106 @@
// 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 Normal file
View File

@@ -0,0 +1,242 @@
// 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;
}
}
}

136
Riptide/PendingMessage.cs Normal file
View File

@@ -0,0 +1,136 @@
// 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();
}
}
}

598
Riptide/Server.cs Normal file
View File

@@ -0,0 +1,598 @@
// 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
}
}

View File

@@ -0,0 +1,61 @@
// 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;
}
}
}

View File

@@ -0,0 +1,28 @@
// 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();
}
}

View File

@@ -0,0 +1,50 @@
// 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();
}
}

View File

@@ -0,0 +1,30 @@
// 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();
}
}

View File

@@ -0,0 +1,120 @@
// 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));
}
}
}

View File

@@ -0,0 +1,195 @@
// 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
}
}

View File

@@ -0,0 +1,56 @@
// 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 &#38; 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));
}
}
}

View File

@@ -0,0 +1,157 @@
// 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));
}
}
}

View File

@@ -0,0 +1,111 @@
// 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));
}
}
}

View File

@@ -0,0 +1,80 @@
// 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
}
}

View File

@@ -0,0 +1,185 @@
// 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 &#38; 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));
}
}
}

View File

@@ -0,0 +1,93 @@
// 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));
}
}
}

150
Riptide/Utils/Bitfield.cs Normal file
View File

@@ -0,0 +1,150 @@
// 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;
}
}
}

View File

@@ -0,0 +1,202 @@
// 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++;
}
}
}

954
Riptide/Utils/Converter.cs Normal file
View File

@@ -0,0 +1,954 @@
// 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;
}
}

View File

@@ -0,0 +1,61 @@
// 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();
}
}
}

View File

@@ -0,0 +1,23 @@
// 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();
}
}
}

113
Riptide/Utils/Helper.cs Normal file
View File

@@ -0,0 +1,113 @@
// 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}'";
}
}
}
}

View File

@@ -0,0 +1,158 @@
// 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;
}
}
}
}

View File

@@ -0,0 +1,126 @@
// 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);
}
}
}

View File

@@ -0,0 +1,93 @@
// 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));
}
}
}