]> git.deb.at Git - rhonda/impostor.git/commitdiff
Re-add Hazel client
authorAeonLucid <aeonlucid@gmail.com>
Mon, 19 Oct 2020 14:18:49 +0000 (16:18 +0200)
committerAeonLucid <aeonlucid@gmail.com>
Mon, 19 Oct 2020 14:18:49 +0000 (16:18 +0200)
src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs
src/Impostor.Client.App/Impostor.Client.App.csproj [new file with mode: 0644]
src/Impostor.Client.App/Program.cs [new file with mode: 0644]
src/Impostor.Client/Impostor.Client.csproj [new file with mode: 0644]
src/Impostor.Hazel/Connection.cs
src/Impostor.Hazel/NetworkConnection.cs
src/Impostor.Hazel/Udp/UdpClientConnection.cs [new file with mode: 0644]
src/Impostor.Hazel/Udp/UdpConnection.cs
src/Impostor.Hazel/Udp/UdpServerConnection.cs
src/Impostor.Server/Net/Manager/GameManager.cs
src/Impostor.sln

index 7245db245cc8b229381415712c70f1819194d110..1ee8b7681059e45489d5c5dcc226200bf0952d4a 100644 (file)
@@ -7,12 +7,16 @@ namespace Impostor.Api.Net.Messages.C2S
     {
         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)
diff --git a/src/Impostor.Client.App/Impostor.Client.App.csproj b/src/Impostor.Client.App/Impostor.Client.App.csproj
new file mode 100644 (file)
index 0000000..886e26e
--- /dev/null
@@ -0,0 +1,16 @@
+<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>
diff --git a/src/Impostor.Client.App/Program.cs b/src/Impostor.Client.App/Program.cs
new file mode 100644 (file)
index 0000000..f5f1cf3
--- /dev/null
@@ -0,0 +1,70 @@
+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
diff --git a/src/Impostor.Client/Impostor.Client.csproj b/src/Impostor.Client/Impostor.Client.csproj
new file mode 100644 (file)
index 0000000..28b6ed3
--- /dev/null
@@ -0,0 +1,12 @@
+<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>
index c2ab579908f745b775563a5a9530d26ba1152d17..6b6c87ef890881da1d82fe7586b8fd251baf4c09 100644 (file)
@@ -155,21 +155,13 @@ namespace Hazel
         ///     </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.
index 68966bad00378e192dde51a856bf466740897321..fc1c3204d7b539e108b83015f98044b465ccb0ee 100644 (file)
@@ -52,14 +52,14 @@ namespace Hazel
         /// <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
                 {
@@ -107,7 +107,7 @@ namespace Hazel
         /// </summary>
         public override async ValueTask Disconnect(string reason, MessageWriter writer = null)
         {
-            if (this.SendDisconnect(writer))
+            if (await SendDisconnect(writer))
             {
                 try
                 {
diff --git a/src/Impostor.Hazel/Udp/UdpClientConnection.cs b/src/Impostor.Hazel/Udp/UdpClientConnection.cs
new file mode 100644 (file)
index 0000000..1bb5722
--- /dev/null
@@ -0,0 +1,252 @@
+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
index c33ffb3d8bfd2db34934265e2eb198e6b9e7bfb2..39de8a76da6602076fef78b7c787d46e1bfdd44b 100644 (file)
@@ -187,7 +187,10 @@ namespace Hazel.Udp
                 _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])
@@ -256,6 +259,28 @@ namespace Hazel.Udp
 
             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)
index c95cdb20b66708885d96e6065175b291fefe93ba..d6aa107752c85fca2136f5aa6e4d87ab1a479767 100644 (file)
@@ -47,16 +47,7 @@ namespace Hazel.Udp
         /// <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?");
         }
@@ -64,11 +55,11 @@ namespace Hazel.Udp
         /// <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;
             }
             
@@ -87,7 +78,7 @@ namespace Hazel.Udp
             }
             catch { }
 
-            return true;
+            return ValueTask.FromResult(true);
         }
 
         protected override void Dispose(bool disposing)
index d05eac8c90e6bb779d8f4c795fd0af57805d9924..c71a56d88fe77571139edb5808ff98f965d13ba3 100644 (file)
@@ -51,7 +51,7 @@ namespace Impostor.Server.Net.Manager
             }
 
             _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));
 
index e62127176974dade3f312f5c937dd00ef798de45..3d60abbcaf4e6edbd533f2d183ecd3a410ad0fde 100644 (file)
@@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger",
 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
@@ -103,6 +109,22 @@ Global
                {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
@@ -116,5 +138,7 @@ Global
                {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