{
public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData)
{
+ writer.StartMessage(MessageFlags.HostGame);
+
using (var memory = new MemoryStream())
using (var writerBin = new BinaryWriter(memory))
{
gameOptionsData.Serialize(writerBin, GameOptionsData.LatestVersion);
writer.WriteBytesAndSize(memory.ToArray());
}
+
+ writer.EndMessage();
}
public static GameOptionsData Deserialize(IMessageReader reader)
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Client\Impostor.Client.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
+ </ItemGroup>
+
+</Project>
--- /dev/null
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Hazel;
+using Hazel.Udp;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.C2S;
+using Serilog;
+
+namespace Impostor.Client.App
+{
+ internal static class Program
+ {
+ private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false);
+
+ private static async Task Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateLogger();
+
+ var writeHandshake = MessageWriter.Get(MessageType.Reliable);
+
+ writeHandshake.Write(50516550);
+ writeHandshake.Write("AeonLucid");
+
+ var writeGameCreate = MessageWriter.Get(MessageType.Reliable);
+
+ Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData
+ {
+ MaxPlayers = 4,
+ NumImpostors = 2
+ });
+
+ using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023)))
+ {
+ var e = new ManualResetEvent(false);
+
+ // Register events.
+ connection.DataReceived = DataReceived;
+ connection.Disconnected = Disconnected;
+
+ // Connect and send handshake.
+ await connection.ConnectAsync(writeHandshake.ToByteArray(false));
+ Log.Information("Connected.");
+
+ // Create a game.
+ await connection.Send(writeGameCreate);
+ Log.Information("Requested game creation.");
+
+ e.WaitOne();
+ }
+ }
+
+ private static ValueTask DataReceived(DataReceivedEventArgs e)
+ {
+ Log.Information("Received data.");
+ return default;
+ }
+
+ private static ValueTask Disconnected(DisconnectedEventArgs e)
+ {
+ Log.Information("Disconnected: " + e.Reason);
+ QuitEvent.Set();
+ return default;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" />
+ </ItemGroup>
+
+</Project>
/// </para>
/// </remarks>
public abstract ValueTask SendBytes(byte[] bytes, MessageType messageType = MessageType.Unreliable);
-
- /// <summary>
- /// Connects the connection to a server and begins listening.
- /// This method blocks and may thrown if there is a problem connecting.
- /// </summary>
- /// <param name="bytes">The bytes of data to send in the handshake.</param>
- /// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
- public abstract void Connect(byte[] bytes = null, int timeout = 5000);
/// <summary>
/// Connects the connection to a server and begins listening.
/// This method does not block.
/// </summary>
/// <param name="bytes">The bytes of data to send in the handshake.</param>
- public abstract void ConnectAsync(byte[] bytes = null);
+ public abstract ValueTask ConnectAsync(byte[] bytes = null);
/// <summary>
/// Invokes the DataReceived event.
/// <summary>
/// Sends a disconnect message to the end point.
/// </summary>
- protected abstract bool SendDisconnect(MessageWriter writer);
+ protected abstract ValueTask<bool> SendDisconnect(MessageWriter writer);
/// <summary>
/// Called when the socket has been disconnected at the remote host.
/// </summary>
protected async ValueTask DisconnectRemote(string reason, IMessageReader reader)
{
- if (this.SendDisconnect(null))
+ if (await SendDisconnect(null))
{
try
{
/// </summary>
public override async ValueTask Disconnect(string reason, MessageWriter writer = null)
{
- if (this.SendDisconnect(writer))
+ if (await SendDisconnect(writer))
{
try
{
--- /dev/null
+using System;
+using System.Buffers;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Serilog;
+
+namespace Hazel.Udp
+{
+ /// <summary>
+ /// Represents a client's connection to a server that uses the UDP protocol.
+ /// </summary>
+ /// <inheritdoc/>
+ public sealed class UdpClientConnection : UdpConnection
+ {
+ private static readonly ILogger Logger = Log.ForContext<UdpClientConnection>();
+
+ /// <summary>
+ /// The socket we're connected via.
+ /// </summary>
+ private readonly UdpClient _socket;
+
+ private readonly Timer _reliablePacketTimer;
+ private readonly SemaphoreSlim _connectWaitLock;
+ private readonly MemoryPool<byte> _pool;
+ private readonly Channel<MessageData> _channel;
+ private Task _listenTask;
+ private Task _handleTask;
+
+ /// <summary>
+ /// Creates a new UdpClientConnection.
+ /// </summary>
+ /// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
+ public UdpClientConnection(IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4) : base(null)
+ {
+ EndPoint = remoteEndPoint;
+ RemoteEndPoint = remoteEndPoint;
+ IPMode = ipMode;
+
+ _socket = new UdpClient
+ {
+ DontFragment = false
+ };
+
+ _reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite);
+ _connectWaitLock = new SemaphoreSlim(1, 1);
+ _pool = MemoryPool<byte>.Shared;
+ _channel = Channel.CreateUnbounded<MessageData>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = true
+ });
+ }
+
+ ~UdpClientConnection()
+ {
+ Dispose(false);
+ }
+
+ private async void ManageReliablePacketsInternal(object state)
+ {
+ await ManageReliablePackets();
+
+ try
+ {
+ _reliablePacketTimer.Change(100, Timeout.Infinite);
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ /// <inheritdoc />
+ protected override ValueTask WriteBytesToConnection(byte[] bytes, int length)
+ {
+ return WriteBytesToConnectionReal(bytes, length);
+ }
+
+ private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length)
+ {
+ try
+ {
+ await _socket.SendAsync(bytes, length);
+ }
+ catch (NullReferenceException) { }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed and disconnected...
+ }
+ catch (SocketException ex)
+ {
+ await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message);
+ }
+ }
+
+ /// <inheritdoc />
+ public override async ValueTask ConnectAsync(byte[] bytes = null)
+ {
+ State = ConnectionState.Connecting;
+
+ try
+ {
+ _socket.Connect(RemoteEndPoint);
+ }
+ catch (SocketException e)
+ {
+ State = ConnectionState.NotConnected;
+ throw new HazelException("A SocketException occurred while binding to the port.", e);
+ }
+
+ try
+ {
+ _listenTask = ListenAsync();
+ }
+ catch (ObjectDisposedException)
+ {
+ // If the socket's been disposed then we can just end there but make sure we're in NotConnected state.
+ // If we end up here I'm really lost...
+ State = ConnectionState.NotConnected;
+ return;
+ }
+ catch (SocketException e)
+ {
+ Dispose();
+ throw new HazelException("A SocketException occurred while initiating a receive operation.", e);
+ }
+
+ // Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present)
+ // When acknowledged set the state to connected
+ await SendHello(bytes, () =>
+ {
+ State = ConnectionState.Connected;
+ InitializeKeepAliveTimer();
+ });
+
+ await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10));
+ }
+
+ private async Task ListenAsync()
+ {
+ // Start packet handler.
+ await StartAsync();
+
+ // Listen.
+ while (State != ConnectionState.NotConnected)
+ {
+ UdpReceiveResult data;
+
+ try
+ {
+ data = await _socket.ReceiveAsync();
+ }
+ catch (SocketException e)
+ {
+ await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message);
+ return;
+ }
+ catch (Exception)
+ {
+ return;
+ }
+
+ if (data.Buffer.Length == 0)
+ {
+ await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes");
+ return;
+ }
+
+ await HandleAsync(data.Buffer);
+ }
+ }
+
+ private async ValueTask HandleAsync(ReadOnlyMemory<byte> memory)
+ {
+ // Rent memory.
+ var dest = _pool.Rent(memory.Length);
+
+ // Copy data.
+ memory.CopyTo(dest.Memory);
+
+ try
+ {
+ // Write to client.
+ await Pipeline.Writer.WriteAsync(new MessageData(dest, memory.Length));
+ }
+ catch (ChannelClosedException)
+ {
+ // Clean up.
+ dest.Dispose();
+ }
+ }
+
+ protected override void SetState(ConnectionState state)
+ {
+ if (state == ConnectionState.Connected)
+ {
+ _connectWaitLock.Release();
+ }
+ }
+
+ /// <summary>
+ /// Sends a disconnect message to the end point.
+ /// You may include optional disconnect data. The SendOption must be unreliable.
+ /// </summary>
+ protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null)
+ {
+ lock (this)
+ {
+ if (_state == ConnectionState.NotConnected) return false;
+ _state = ConnectionState.NotConnected;
+ }
+
+ var bytes = EmptyDisconnectBytes;
+ if (data != null && data.Length > 0)
+ {
+ if (data.SendOption != MessageType.Unreliable)
+ {
+ throw new ArgumentException("Disconnect messages can only be unreliable.");
+ }
+
+ bytes = data.ToByteArray(true);
+ bytes[0] = (byte)UdpSendOption.Disconnect;
+ }
+
+ try
+ {
+ await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint);
+ }
+ catch { }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ State = ConnectionState.NotConnected;
+
+ try { _socket.Close(); } catch { }
+ try { _socket.Dispose(); } catch { }
+
+ _reliablePacketTimer.Dispose();
+ _connectWaitLock.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+}
\ No newline at end of file
_isFirst = false;
// Slice 4 bytes to get handshake data.
- await _listener.InvokeNewConnection(message.Slice(4), this);
+ if (_listener != null)
+ {
+ await _listener.InvokeNewConnection(message.Slice(4), this);
+ }
}
switch (message.Buffer.Span[0])
Statistics.LogUnreliableSend(length, bytes.Length);
}
+
+ /// <summary>
+ /// Sends a hello packet to the remote endpoint.
+ /// </summary>
+ /// <param name="bytes"></param>
+ /// <param name="acknowledgeCallback">The callback to invoke when the hello packet is acknowledged.</param>
+ protected ValueTask SendHello(byte[] bytes, Action acknowledgeCallback)
+ {
+ //First byte of handshake is version indicator so add data after
+ byte[] actualBytes;
+ if (bytes == null)
+ {
+ actualBytes = new byte[1];
+ }
+ else
+ {
+ actualBytes = new byte[bytes.Length + 1];
+ Buffer.BlockCopy(bytes, 0, actualBytes, 1, bytes.Length);
+ }
+
+ return HandleSend(actualBytes, (byte)UdpSendOption.Hello, acknowledgeCallback);
+ }
/// <inheritdoc/>
protected override void Dispose(bool disposing)
/// <remarks>
/// This will always throw a HazelException.
/// </remarks>
- public override void Connect(byte[] bytes = null, int timeout = 5000)
- {
- throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
- }
-
- /// <inheritdoc />
- /// <remarks>
- /// This will always throw a HazelException.
- /// </remarks>
- public override void ConnectAsync(byte[] bytes = null)
+ public override ValueTask ConnectAsync(byte[] bytes = null)
{
throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
}
/// <summary>
/// Sends a disconnect message to the end point.
/// </summary>
- protected override bool SendDisconnect(MessageWriter data = null)
+ protected override ValueTask<bool> SendDisconnect(MessageWriter data = null)
{
lock (this)
{
- if (this._state != ConnectionState.Connected) return false;
+ if (this._state != ConnectionState.Connected) return ValueTask.FromResult(false);
this._state = ConnectionState.NotConnected;
}
}
catch { }
- return true;
+ return ValueTask.FromResult(true);
}
protected override void Dispose(bool disposing)
}
_nodeLocator.Save(gameCodeStr, _publicIp);
- _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);
+ _logger.LogDebug("Created game with code {0}.", game.Code);
await _eventManager.CallAsync(new GameCreatedEvent(game));
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Hazel", "Impostor.Hazel\Impostor.Hazel.csproj", "{671B753B-31AE-4C36-AD71-09CF00FA17CA}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{9F1919B0-915B-4749-9944-697DF7E7F67F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.Build.0 = Release|Any CPU
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.ActiveCfg = Release|Any CPU
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.Build.0 = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.Build.0 = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.ActiveCfg = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.Build.0 = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.Build.0 = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
{7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
+ {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
EndGlobalSection
EndGlobal