From bb2b7e72b5f949408ddd055af20ab6004274b4c5 Mon Sep 17 00:00:00 2001 From: Ray Koopa Date: Tue, 14 Jul 2020 20:53:11 +0200 Subject: [PATCH] Rewrite server netcode to be asynchronous and support timeouts. Allow multiple proxy connections. --- .../Syroot.Worms/Core/EncodingExtensions.cs | 23 ++ .../Syroot.Worms/IO/StreamExtensions.cs | 46 +--- .../Syroot.Worms/Shims/System.IO.Stream.cs | 41 +++ .../System.Text.Encoding.cs} | 16 +- src/library/Syroot.Worms/Syroot.Worms.csproj | 1 + .../Syroot.Worms.Worms2.GameServer/Packet.cs | 124 +-------- .../PacketConnection.cs | 243 ++++++++++++++++-- .../PacketField.cs | 25 ++ .../Syroot.Worms.Worms2.GameServer/Program.cs | 7 +- .../Properties/launchSettings.json | 2 +- .../Syroot.Worms.Worms2.GameServer/Proxy.cs | 46 ++-- .../Syroot.Worms.Worms2.GameServer/Server.cs | 235 +++++++++-------- .../Syroot.Worms.Worms2.GameServer.csproj | 2 + .../TcpListenerExtensions.cs | 38 +++ 14 files changed, 537 insertions(+), 312 deletions(-) create mode 100644 src/library/Syroot.Worms/Core/EncodingExtensions.cs create mode 100644 src/library/Syroot.Worms/Shims/System.IO.Stream.cs rename src/library/Syroot.Worms/{IO/EncodingExtensions.cs => Shims/System.Text.Encoding.cs} (69%) create mode 100644 src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs create mode 100644 src/tool/Syroot.Worms.Worms2.GameServer/TcpListenerExtensions.cs diff --git a/src/library/Syroot.Worms/Core/EncodingExtensions.cs b/src/library/Syroot.Worms/Core/EncodingExtensions.cs new file mode 100644 index 0000000..3dc81f6 --- /dev/null +++ b/src/library/Syroot.Worms/Core/EncodingExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Text; + +namespace Syroot.Worms.Core +{ + /// + /// Represents extension methods for instances. + /// + public static class EncodingExtensions + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + public static string GetZeroTerminatedString(this Encoding encoding, ReadOnlySpan bytes) + => encoding.GetString(bytes.Slice(0, Math.Max(0, bytes.IndexOf((byte)0)))); + + public static int GetZeroTerminatedBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes) + { + int length = encoding.GetBytes(chars, bytes); + bytes[length] = 0; + return ++length; + } + } +} diff --git a/src/library/Syroot.Worms/IO/StreamExtensions.cs b/src/library/Syroot.Worms/IO/StreamExtensions.cs index c0f1c42..f04578c 100644 --- a/src/library/Syroot.Worms/IO/StreamExtensions.cs +++ b/src/library/Syroot.Worms/IO/StreamExtensions.cs @@ -3,6 +3,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using Syroot.Worms.Core; namespace Syroot.Worms.IO { @@ -21,13 +22,12 @@ namespace Syroot.Worms.IO /// The 1-byte to use or to use /// . /// The read string. - public static unsafe string ReadFixedString(this Stream stream, int length, Encoding? encoding = null) + public static string ReadFixedString(this Stream stream, int length, Encoding? encoding = null) { // Ensure to not try to decode any bytes after the 0 termination. Span bytes = stackalloc byte[length]; stream.Read(bytes); - for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ; - return (encoding ?? Encoding.ASCII).GetString(bytes.Slice(0, length)); + return (encoding ?? Encoding.ASCII).GetZeroTerminatedString(bytes); } /// @@ -63,8 +63,7 @@ namespace Syroot.Worms.IO public static void WriteFixedString(this Stream stream, string value, int length, Encoding? encoding = null) { Span bytes = stackalloc byte[length]; - if (value != null) - (encoding ?? Encoding.ASCII).GetBytes(value.AsSpan(), bytes); + (encoding ?? Encoding.ASCII).GetBytes(value.AsSpan(), bytes); stream.Write(bytes); } @@ -117,42 +116,5 @@ namespace Syroot.Worms.IO /// The instance to write with. /// The instance to write into the current stream. public static void Save(this Stream stream, T value) where T : ISaveable => value.Save(stream); - -#if NETSTANDARD2_0 - // ---- Backports ---- - - /// - /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the - /// position within the stream by the number of bytes read. - /// - /// The instance to write with. - /// A region of memory. When this method returns, the contents of this region are replaced - /// by the bytes read from the current source. - /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated - /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream has been - /// reached. - /// - /// This .NET Standard 2.0 backport requires a temporary copy. - /// - public static int Read(this Stream stream, Span buffer) - { - byte[] bytes = new byte[buffer.Length]; - int bytesRead = stream.Read(bytes); - bytes.AsSpan(0, bytesRead).CopyTo(buffer); - return bytesRead; - } - - /// - /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the - /// current position within this stream by the number of bytes written. - /// - /// The instance. - /// A region of memory. This method copies the contents of this region to the current - /// stream. - /// - /// This .NET Standard 2.0 backport requires a temporary copy. - /// - public static void Write(this Stream stream, ReadOnlySpan value) => stream.Write(value.ToArray()); -#endif } } diff --git a/src/library/Syroot.Worms/Shims/System.IO.Stream.cs b/src/library/Syroot.Worms/Shims/System.IO.Stream.cs new file mode 100644 index 0000000..febd22e --- /dev/null +++ b/src/library/Syroot.Worms/Shims/System.IO.Stream.cs @@ -0,0 +1,41 @@ +#if NETSTANDARD2_0 +namespace System.IO +{ + /// + /// Represents extension methods for instances. + /// + public static class StreamShims + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the + /// position within the stream by the number of bytes read. + /// + /// The instance to write with. + /// A region of memory. When this method returns, the contents of this region are replaced + /// by the bytes read from the current source. + /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated + /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream has been + /// reached. + /// This .NET Standard 2.0 backport requires a temporary copy. + public static int Read(this Stream stream, Span buffer) + { + byte[] bytes = new byte[buffer.Length]; + int bytesRead = stream.Read(bytes); + bytes.AsSpan(0, bytesRead).CopyTo(buffer); + return bytesRead; + } + + /// + /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the + /// current position within this stream by the number of bytes written. + /// + /// The instance. + /// A region of memory. This method copies the contents of this region to the current + /// stream. + /// This .NET Standard 2.0 backport requires a temporary copy. + public static void Write(this Stream stream, ReadOnlySpan value) => stream.Write(value.ToArray()); + } +} +#endif diff --git a/src/library/Syroot.Worms/IO/EncodingExtensions.cs b/src/library/Syroot.Worms/Shims/System.Text.Encoding.cs similarity index 69% rename from src/library/Syroot.Worms/IO/EncodingExtensions.cs rename to src/library/Syroot.Worms/Shims/System.Text.Encoding.cs index acb501d..3423176 100644 --- a/src/library/Syroot.Worms/IO/EncodingExtensions.cs +++ b/src/library/Syroot.Worms/Shims/System.Text.Encoding.cs @@ -1,18 +1,10 @@ -using System; -using System.Text; - -namespace Syroot.Worms.IO +#if NETSTANDARD2_0 +namespace System.Text { - /// - /// Represents extension methods for instances. - /// - public static class EncodingExtensions + public static class EncodingShims { // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- -#if NETSTANDARD2_0 - // ---- Backports ---- - public unsafe static int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes) { fixed (byte* pBytes = bytes) @@ -25,6 +17,6 @@ namespace Syroot.Worms.IO fixed (byte* pBytes = bytes) return encoding.GetString(pBytes, bytes.Length); } -#endif } } +#endif diff --git a/src/library/Syroot.Worms/Syroot.Worms.csproj b/src/library/Syroot.Worms/Syroot.Worms.csproj index 28ba016..46b2d66 100644 --- a/src/library/Syroot.Worms/Syroot.Worms.csproj +++ b/src/library/Syroot.Worms/Syroot.Worms.csproj @@ -16,6 +16,7 @@ + \ No newline at end of file diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs index b6d7f73..5578935 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; -using System.Text; -using Syroot.BinaryData; -using Syroot.Worms.IO; +using System.Text; namespace Syroot.Worms.Worms2.GameServer { @@ -11,10 +7,6 @@ namespace Syroot.Worms.Worms2.GameServer /// internal class Packet { - // ---- CONSTANTS ---------------------------------------------------------------------------------------------- - - private const int _maxDataSize = 0x1000; - // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ /// @@ -59,57 +51,57 @@ namespace Syroot.Worms.Worms2.GameServer /// /// Gets or sets describing the action of the packet. /// - internal PacketCode Code { get; set; } + internal PacketCode Code; /// /// Gets or sets a parameter for the action. /// - internal int? Value0 { get; set; } + internal int? Value0; /// /// Gets or sets a parameter for the action. /// - internal int? Value1 { get; set; } + internal int? Value1; /// /// Gets or sets a parameter for the action. /// - internal int? Value2 { get; set; } + internal int? Value2; /// /// Gets or sets a parameter for the action. /// - internal int? Value3 { get; set; } + internal int? Value3; /// /// Gets or sets a parameter for the action. /// - internal int? Value4 { get; set; } + internal int? Value4; /// /// Gets or sets a parameter for the action. /// - internal int? Value10 { get; set; } + internal int? Value10; /// /// Gets or sets a textual parameter for the action. /// - internal string? Data { get; set; } + internal string? Data; /// /// Gets or sets an error code returned from the server after executing the action. /// - internal int? Error { get; set; } + internal int? Error; /// /// Gets or sets a named parameter for the action. /// - internal string? Name { get; set; } + internal string? Name; /// /// Gets or sets a for the action. /// - internal SessionInfo? Session { get; set; } + internal SessionInfo? Session; // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- @@ -117,7 +109,6 @@ namespace Syroot.Worms.Worms2.GameServer public override string ToString() { StringBuilder sb = new StringBuilder(); - sb.AppendLine($"{Code:D} {Code}"); if (Value0.HasValue) sb.AppendLine($" {nameof(Value0),7}: {Value0:X8}"); if (Value1.HasValue) sb.AppendLine($" {nameof(Value1),7}: {Value1:X8}"); @@ -129,98 +120,7 @@ namespace Syroot.Worms.Worms2.GameServer if (Error.HasValue) sb.AppendLine($" {nameof(Error),7}: {Error:X8}"); if (Name != null) sb.AppendLine($" {nameof(Name),7}: {Name}"); if (Session.HasValue) sb.AppendLine($" {nameof(Session),7}: {Session}"); - return sb.ToString().TrimEnd(); } - - // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- - - /// - /// Blocks and reads the packet data from the given . - /// - /// The to read the packet data from. - internal void Receive(Stream stream) - { - int dataLength = 0; - Code = stream.ReadEnum(true); - Flags flags = stream.ReadEnum(true); - if (flags.HasFlag(Flags.Value0)) Value0 = stream.ReadInt32(); - if (flags.HasFlag(Flags.Value1)) Value1 = stream.ReadInt32(); - if (flags.HasFlag(Flags.Value2)) Value2 = stream.ReadInt32(); - if (flags.HasFlag(Flags.Value3)) Value3 = stream.ReadInt32(); - if (flags.HasFlag(Flags.Value4)) Value4 = stream.ReadInt32(); - if (flags.HasFlag(Flags.Value10)) Value10 = stream.ReadInt32(); - if (flags.HasFlag(Flags.DataLength)) dataLength = stream.ReadInt32(); - if (flags.HasFlag(Flags.Data) && dataLength >= 0 && dataLength <= _maxDataSize) - Data = stream.ReadFixedString(dataLength, Encodings.Windows1252); - if (flags.HasFlag(Flags.Error)) Error = stream.ReadInt32(); - if (flags.HasFlag(Flags.Name)) Name = stream.ReadFixedString(20, Encodings.Windows1252); - if (flags.HasFlag(Flags.Session)) Session = stream.ReadStruct(); - } - - /// - /// Blocks and writes the packet data to the given . - /// - /// The to write the packet data to. - internal void Send(Stream stream) - { - stream.WriteEnum(Code); - stream.WriteEnum(GetFlags()); - if (Value0.HasValue) stream.WriteInt32(Value0.Value); - if (Value1.HasValue) stream.WriteInt32(Value1.Value); - if (Value2.HasValue) stream.WriteInt32(Value2.Value); - if (Value3.HasValue) stream.WriteInt32(Value3.Value); - if (Value4.HasValue) stream.WriteInt32(Value4.Value); - if (Value10.HasValue) stream.WriteInt32(Value10.Value); - if (Data != null) - { - stream.WriteInt32(Data.Length + 1); - stream.WriteFixedString(Data, Data.Length + 1, Encodings.Windows1252); - } - if (Error.HasValue) stream.WriteInt32(Error.Value); - if (Name != null) stream.WriteFixedString(Name, 20, Encodings.Windows1252); - if (Session.HasValue) stream.WriteStruct(Session.Value); - } - - // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- - - private Flags GetFlags() - { - Flags flags = Flags.None; - if (Value0.HasValue) flags |= Flags.Value0; - if (Value1.HasValue) flags |= Flags.Value1; - if (Value2.HasValue) flags |= Flags.Value2; - if (Value3.HasValue) flags |= Flags.Value3; - if (Value4.HasValue) flags |= Flags.Value4; - if (Value10.HasValue) flags |= Flags.Value10; - if (Data != null) - { - flags |= Flags.DataLength; - flags |= Flags.Data; - } - if (Error.HasValue) flags |= Flags.Error; - if (Name != null) flags |= Flags.Name; - if (Session.HasValue) flags |= Flags.Session; - return flags; - } - - // ---- CLASSES, STRUCTS & ENUMS ------------------------------------------------------------------------------- - - [Flags] - private enum Flags : int - { - None, - Value0 = 1 << 0, - Value1 = 1 << 1, - Value2 = 1 << 2, - Value3 = 1 << 3, - Value4 = 1 << 4, - Value10 = 1 << 10, - DataLength = 1 << 5, - Data = 1 << 6, - Error = 1 << 7, - Name = 1 << 8, - Session = 1 << 9 - } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs b/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs index 1c2dd5a..180090d 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs @@ -1,19 +1,34 @@ -using System.IO; +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.IO.Pipelines; using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Syroot.BinaryData.Core; +using Syroot.Worms.Core; namespace Syroot.Worms.Worms2.GameServer { /// /// Represents a duplex connection to a client, allowing to receive and send instances. /// - internal class PacketConnection + internal sealed class PacketConnection { + // ---- CONSTANTS ---------------------------------------------------------------------------------------------- + + private const int _maxDataSize = 0x1000; + // ---- FIELDS ------------------------------------------------------------------------------------------------- - private readonly Stream _stream; - private readonly object _recvLock = new object(); - private readonly object _sendLock = new object(); + private readonly PipeReader _reader; + private readonly PipeWriter _writer; + private readonly SemaphoreSlim _recvLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ @@ -24,7 +39,9 @@ namespace Syroot.Worms.Worms2.GameServer /// The to communicate with. internal PacketConnection(TcpClient client) { - _stream = client.GetStream(); + Stream stream = client.GetStream(); + _reader = PipeReader.Create(stream); + _writer = PipeWriter.Create(stream); RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint; } @@ -38,27 +55,217 @@ namespace Syroot.Worms.Worms2.GameServer // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- /// - /// Blocks until a was received, and returns it. + /// Receives a instance asynchronously. /// - /// The received . - internal Packet Receive() + /// The read instance. + internal async ValueTask Read(CancellationToken ct) { - lock (_recvLock) + Packet packet = new Packet(); + PacketField at = PacketField.None; + PacketField fields = PacketField.None; + int dataLength = 0; + bool get(in ReadOnlySequence buffer, out SequencePosition consumedTo) { - Packet packet = new Packet(); - packet.Receive(_stream); - return packet; + consumedTo = default; + SequenceReader reader = new SequenceReader(buffer); + PacketField prevAt = at; + switch (at) + { + case PacketField.None: + if (!reader.TryReadLittleEndian(out int codeValue) + || !reader.TryReadLittleEndian(out int fieldsValue)) break; + if (!Enum.IsDefined(typeof(PacketCode), codeValue)) + throw new InvalidDataException($"Bad packet code {codeValue}."); + if (!EnumTools.Validate(typeof(PacketField), fieldsValue)) + throw new InvalidDataException($"Bad packet fields 0b{Convert.ToString(fieldsValue, 2)}."); + packet.Code = (PacketCode)codeValue; + fields = (PacketField)fieldsValue; + consumedTo = reader.Position; + goto case PacketField.Value0; + + case PacketField.Value0: + if (!fields.HasFlag(at = PacketField.Value0)) goto case PacketField.Value1; + if (!reader.TryReadLittleEndian(out int value0)) break; + packet.Value0 = value0; + consumedTo = reader.Position; + goto case PacketField.Value1; + + case PacketField.Value1: + if (!fields.HasFlag(at = PacketField.Value1)) goto case PacketField.Value2; + if (!reader.TryReadLittleEndian(out int value1)) break; + packet.Value1 = value1; + consumedTo = reader.Position; + goto case PacketField.Value2; + + case PacketField.Value2: + if (!fields.HasFlag(at = PacketField.Value2)) goto case PacketField.Value3; + if (!reader.TryReadLittleEndian(out int value2)) break; + packet.Value2 = value2; + consumedTo = reader.Position; + goto case PacketField.Value3; + + case PacketField.Value3: + if (!fields.HasFlag(at = PacketField.Value3)) goto case PacketField.Value4; + if (!reader.TryReadLittleEndian(out int value3)) break; + packet.Value3 = value3; + consumedTo = reader.Position; + goto case PacketField.Value4; + + case PacketField.Value4: + if (!fields.HasFlag(at = PacketField.Value4)) goto case PacketField.Value10; + if (!reader.TryReadLittleEndian(out int value4)) break; + packet.Value4 = value4; + consumedTo = reader.Position; + goto case PacketField.Value10; + + case PacketField.Value10: + if (!fields.HasFlag(at = PacketField.Value10)) goto case PacketField.DataLength; + if (!reader.TryReadLittleEndian(out int value10)) break; + packet.Value10 = value10; + consumedTo = reader.Position; + goto case PacketField.DataLength; + + case PacketField.DataLength: + if (!fields.HasFlag(at = PacketField.DataLength)) goto case PacketField.Error; + if (!reader.TryReadLittleEndian(out dataLength)) break; + if (dataLength > _maxDataSize) + throw new InvalidDataException($"Data too large by {dataLength - _maxDataSize} bytes."); + consumedTo = reader.Position; + goto case PacketField.Data; + + case PacketField.Data: + if (!fields.HasFlag(at = PacketField.Data)) goto case PacketField.Error; + Span dataBytes = stackalloc byte[dataLength]; + if (!reader.TryCopyTo(dataBytes)) break; + reader.Advance(dataLength); + packet.Data = Encodings.Windows1252.GetString(dataBytes); + consumedTo = reader.Position; + goto case PacketField.Error; + + case PacketField.Error: + if (!fields.HasFlag(at = PacketField.Error)) goto case PacketField.Name; + if (!reader.TryReadLittleEndian(out int error)) break; + packet.Error = error; + consumedTo = reader.Position; + goto case PacketField.Name; + + case PacketField.Name: + if (!fields.HasFlag(at = PacketField.Name)) goto case PacketField.Session; + Span nameBytes = stackalloc byte[20]; + if (!reader.TryCopyTo(nameBytes)) break; + reader.Advance(20); + packet.Name = Encodings.Windows1252.GetZeroTerminatedString(nameBytes); + consumedTo = reader.Position; + goto case PacketField.Session; + + case PacketField.Session: + if (!fields.HasFlag(at = PacketField.Session)) goto case PacketField.All; + Span sessionBytes = stackalloc byte[Unsafe.SizeOf()]; + if (!reader.TryCopyTo(sessionBytes)) break; + reader.Advance(sessionBytes.Length); + packet.Session = MemoryMarshal.Cast(sessionBytes)[0]; + consumedTo = reader.Position; + goto case PacketField.All; + + case PacketField.All: + at = PacketField.All; + break; + + default: + throw new InvalidOperationException("Invalid packet read state."); + } + return prevAt < at; + } + + while (true) + { + ReadResult read = await _reader.ReadAsync(ct); + if (read.IsCanceled) + throw new OperationCanceledException("Packet read was canceled."); + + ReadOnlySequence buffer = read.Buffer; + if (get(buffer, out SequencePosition consumedTo)) + { + _reader.AdvanceTo(consumedTo); + if (at == PacketField.All) + return packet; + } + + _reader.AdvanceTo(buffer.Start, buffer.End); + if (read.IsCompleted) + throw new EndOfStreamException("No more data."); } } /// - /// Blocks until the given was sent. + /// Sends a instance asynchronously. /// - /// The to send. - internal void Send(Packet packet) + /// The instance to write. + /// Whether the instance was written successfully. + internal async ValueTask Write(Packet packet, CancellationToken ct) { - lock (_sendLock) - packet.Send(_stream); + unsafe int set() + { + // Calculate the (exact) length of the packet. + PacketField fields = PacketField.None; + int add(PacketField field, object? value, int size) + { + if (value == null) + return 0; + fields |= field; + return size; + } + int size = sizeof(PacketCode) + sizeof(PacketField) + + add(PacketField.Value0, packet.Value0, sizeof(int)) + + add(PacketField.Value1, packet.Value1, sizeof(int)) + + add(PacketField.Value2, packet.Value2, sizeof(int)) + + add(PacketField.Value3, packet.Value3, sizeof(int)) + + add(PacketField.Value4, packet.Value4, sizeof(int)) + + add(PacketField.Value10, packet.Value10, sizeof(int)) + + add(PacketField.DataLength, packet.Data, sizeof(int)) + + add(PacketField.Data, packet.Data, packet.Data?.Length ?? 0) + + add(PacketField.Error, packet.Error, sizeof(int)) + + add(PacketField.Name, packet.Name, 20) + + add(PacketField.Session, packet.Session, Unsafe.SizeOf()); + + // Write the data. + Span span = _writer.GetSpan(size); + static void writeInt(ref Span span, int value) + { + BinaryPrimitives.WriteInt32LittleEndian(span, value); + span = span.Slice(sizeof(int)); + } + writeInt(ref span, (int)packet.Code); + writeInt(ref span, (int)fields); + if (packet.Value0 != null) writeInt(ref span, packet.Value0.Value); + if (packet.Value1 != null) writeInt(ref span, packet.Value1.Value); + if (packet.Value2 != null) writeInt(ref span, packet.Value2.Value); + if (packet.Value3 != null) writeInt(ref span, packet.Value3.Value); + if (packet.Value4 != null) writeInt(ref span, packet.Value4.Value); + if (packet.Value10 != null) writeInt(ref span, packet.Value10.Value); + if (packet.Data != null) + { + writeInt(ref span, packet.Data.Length); + span = span.Slice(Encodings.Windows1252.GetBytes(packet.Data, span)); + } + if (packet.Error != null) writeInt(ref span, packet.Error.Value); + if (packet.Name != null) + { + span[Encodings.Windows1252.GetBytes(packet.Name, span)..20].Clear(); + span = span.Slice(20); + } + if (packet.Session != null) + { + SessionInfo session = packet.Session.Value; + new ReadOnlySpan(Unsafe.AsPointer(ref session), Unsafe.SizeOf()).CopyTo(span); + } + + return size; + } + + _writer.Advance(set()); + FlushResult flush = await _writer.FlushAsync(ct); + return !flush.IsCanceled && !flush.IsCompleted; } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs b/src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs new file mode 100644 index 0000000..d905565 --- /dev/null +++ b/src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs @@ -0,0 +1,25 @@ +using System; + +namespace Syroot.Worms.Worms2.GameServer +{ + /// + /// Represents a bitset determining which fields are available in a instance. + /// + [Flags] + internal enum PacketField : int + { + None, + Value0 = 1 << 0, + Value1 = 1 << 1, + Value2 = 1 << 2, + Value3 = 1 << 3, + Value4 = 1 << 4, + Value10 = 1 << 10, + DataLength = 1 << 5, + Data = 1 << 6, + Error = 1 << 7, + Name = 1 << 8, + Session = 1 << 9, + All = Int32.MaxValue + } +} diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs index 7843ced..0f0290c 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Threading.Tasks; namespace Syroot.Worms.Worms2.GameServer { @@ -10,14 +11,14 @@ namespace Syroot.Worms.Worms2.GameServer { // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- - private static void Main(string[] args) + private static async Task Main(string[] args) { string? argEndPoint = args.Length > 0 ? args[0] : null; Server server = new Server(); - server.Run(ParseEndPoint(argEndPoint, new IPEndPoint(IPAddress.Any, 17000))); + await server.Run(ParseEndPoint(argEndPoint, new IPEndPoint(IPAddress.Any, 17000))); - //Proxy.Run(ParseEndPoint(argEndPoint, new IPEndPoint(IPAddress.Any, 17001))); + //await Proxy.Run(ParseEndPoint(argEndPoint, new IPEndPoint(IPAddress.Any, 17001))); } private static IPEndPoint ParseEndPoint(string? s, IPEndPoint fallback) diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Properties/launchSettings.json b/src/tool/Syroot.Worms.Worms2.GameServer/Properties/launchSettings.json index 34d78c0..adb6a86 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Properties/launchSettings.json +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Syroot.Worms.Worms2.GameServer": { "commandName": "Project", - "commandLineArgs": "127.0.0.1:17002" + "commandLineArgs": "17001" } } } \ No newline at end of file diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs index efc1f15..29a47d2 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs @@ -1,6 +1,8 @@ -using System.Drawing; +using System; +using System.Drawing; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using Syroot.ColoredConsole; @@ -13,15 +15,15 @@ namespace Syroot.Worms.Worms2.GameServer { // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- - internal static void Run(IPEndPoint localEndPoint) + internal static async Task Run(IPEndPoint localEndPoint, CancellationToken ct = default) { // Start listening for clients to intercept. TcpListener listener = new TcpListener(localEndPoint); listener.Start(); - ColorConsole.WriteLine(Color.Orange, $"Proxy listening under {localEndPoint}..."); + Log(Color.Orange, $"Proxy listening under {localEndPoint}..."); TcpClient? client; - while ((client = listener.AcceptTcpClient()) != null) + while ((client = await listener.AcceptTcpClientAsync(ct)) != null) { // Connect to server. TcpClient server = new TcpClient(); @@ -29,25 +31,39 @@ namespace Syroot.Worms.Worms2.GameServer PacketConnection clientConnection = new PacketConnection(client); PacketConnection serverConnection = new PacketConnection(server); - ColorConsole.WriteLine(Color.Green, $"{clientConnection.RemoteEndPoint} connected."); + Log(Color.Green, $"{clientConnection.RemoteEndPoint} connected."); - Task.Run(() => Forward(clientConnection, serverConnection, true)); - Task.Run(() => Forward(serverConnection, clientConnection, false)); + CancellationTokenSource disconnectCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + _ = Task.WhenAny( + Forward(clientConnection, serverConnection, true, disconnectCts.Token), + Forward(serverConnection, clientConnection, false, disconnectCts.Token)) + .ContinueWith((antecedent) => disconnectCts.Cancel()); } } // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- - private static void Forward(PacketConnection from, PacketConnection to, bool fromClient) + private static void Log(Color color, string message) + => ColorConsole.WriteLine(color, $"{DateTime.Now:HH:mm:ss} {message}"); + + private static async Task Forward(PacketConnection from, PacketConnection to, bool fromClient, + CancellationToken ct) { - while (true) + string prefix = fromClient + ? $"{from.RemoteEndPoint} >> {to.RemoteEndPoint}" + : $"{to.RemoteEndPoint} << {from.RemoteEndPoint}"; + try { - Packet packet = from.Receive(); - if (fromClient) - ColorConsole.WriteLine(Color.Cyan, $"{from.RemoteEndPoint} >> {to.RemoteEndPoint} | {packet}"); - else - ColorConsole.WriteLine(Color.Magenta, $"{to.RemoteEndPoint} << {from.RemoteEndPoint} | {packet}"); - to.Send(packet); + while (true) + { + Packet packet = await from.Read(ct); + Log(fromClient ? Color.Cyan : Color.Magenta, $"{prefix} {packet}"); + await to.Write(packet, ct); + } + } + catch (Exception ex) + { + Log(Color.Red, $"{prefix} closed. {ex.Message}"); } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs index 5736980..51825c8 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs @@ -1,10 +1,11 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Syroot.ColoredConsole; @@ -15,34 +16,43 @@ namespace Syroot.Worms.Worms2.GameServer /// internal class Server { + // ---- CONSTANTS ---------------------------------------------------------------------------------------------- + + private const int _authorizedTimeout = 10 * 60 * 1000; + private const int _unauthorizedTimeout = 3 * 1000; + // ---- FIELDS ------------------------------------------------------------------------------------------------- private int _lastID = 0x1000; // start at an offset to prevent bugs with chat private readonly List _users = new List(); private readonly List _rooms = new List(); private readonly List _games = new List(); - private readonly BlockingCollection _jobs = new BlockingCollection(); - private readonly Dictionary> _packetHandlers; + private readonly Channel> _jobs; + private readonly Dictionary _packetHandlers; // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ /// /// Initializes a new instance of the class. /// - internal Server() => _packetHandlers = new Dictionary> + internal Server() { - [PacketCode.ListRooms] = OnListRooms, - [PacketCode.ListUsers] = OnListUsers, - [PacketCode.ListGames] = OnListGames, - [PacketCode.Login] = OnLogin, - [PacketCode.CreateRoom] = OnCreateRoom, - [PacketCode.Join] = OnJoin, - [PacketCode.Leave] = OnLeave, - [PacketCode.Close] = OnClose, - [PacketCode.CreateGame] = OnCreateGame, - [PacketCode.ChatRoom] = OnChatRoom, - [PacketCode.ConnectGame] = OnConnectGame, - }; + _jobs = Channel.CreateUnbounded>(new UnboundedChannelOptions { SingleReader = true }); + _packetHandlers = new Dictionary + { + [PacketCode.ListRooms] = OnListRooms, + [PacketCode.ListUsers] = OnListUsers, + [PacketCode.ListGames] = OnListGames, + [PacketCode.Login] = OnLogin, + [PacketCode.CreateRoom] = OnCreateRoom, + [PacketCode.Join] = OnJoin, + [PacketCode.Leave] = OnLeave, + [PacketCode.Close] = OnClose, + [PacketCode.CreateGame] = OnCreateGame, + [PacketCode.ChatRoom] = OnChatRoom, + [PacketCode.ConnectGame] = OnConnectGame, + }; + } // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- @@ -50,28 +60,31 @@ namespace Syroot.Worms.Worms2.GameServer /// Begins listening for new clients connecting to the given and dispatches /// them into their own threads. /// - internal void Run(IPEndPoint localEndPoint) + internal async Task Run(IPEndPoint localEndPoint, CancellationToken ct = default) { // Begin handling any queued jobs. - Task.Run(() => HandleJobs()); - // Begin listening for new connections. Currently synchronous and blocking. - HandleConnections(localEndPoint); + _ = HandleJobs(ct); + // Begin listening for new connections. + await HandleConnections(localEndPoint, ct); } // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- - private static void SendPacket(PacketConnection connection, Packet packet) - { - LogPacket(connection, packet, false); - connection.Send(packet); - } + private static void Log(Color color, string message) + => ColorConsole.WriteLine(color, $"{DateTime.Now:HH:mm:ss} {message}"); private static void LogPacket(PacketConnection connection, Packet packet, bool fromClient) { if (fromClient) - ColorConsole.WriteLine(Color.Cyan, $"{DateTime.Now:HH:mm:ss} {connection.RemoteEndPoint} >> {packet}"); + Log(Color.Cyan, $"{connection.RemoteEndPoint} >> {packet}"); else - ColorConsole.WriteLine(Color.Magenta, $"{DateTime.Now:HH:mm:ss} {connection.RemoteEndPoint} << {packet}"); + Log(Color.Magenta, $"{connection.RemoteEndPoint} << {packet}"); + } + + private static async ValueTask SendPacket(PacketConnection connection, CancellationToken ct, Packet packet) + { + LogPacket(connection, packet, false); + await connection.Write(packet, ct); } private User? GetUser(PacketConnection connection) @@ -82,53 +95,62 @@ namespace Syroot.Worms.Worms2.GameServer return null; } - private void HandleJobs() + private async ValueTask HandleJobs(CancellationToken ct) { - foreach (Action job in _jobs.GetConsumingEnumerable()) - job(); + await foreach (Func job in _jobs.Reader.ReadAllAsync(ct)) + await job(); } - private void HandleConnections(IPEndPoint localEndPoint) + private async ValueTask HandleConnections(IPEndPoint localEndPoint, CancellationToken ct) { // Start a new listener for new incoming connections. TcpListener listener = new TcpListener(localEndPoint); listener.Start(); - ColorConsole.WriteLine(Color.Orange, $"Server listening under {listener.LocalEndpoint}..."); + Log(Color.Orange, $"Server listening under {listener.LocalEndpoint}..."); // Dispatch each connection into its own thread. TcpClient? client; - while ((client = listener.AcceptTcpClient()) != null) - Task.Run(() => HandleClient(client)); + while ((client = await listener.AcceptTcpClientAsync(ct)) != null) + _ = HandleClient(client, ct); } - private void HandleClient(TcpClient client) + private async Task HandleClient(TcpClient client, CancellationToken ct) { PacketConnection connection = new PacketConnection(client); - ColorConsole.WriteLine(Color.Green, $"{connection.RemoteEndPoint} connected."); + Log(Color.Green, $"{connection.RemoteEndPoint} connected."); + bool loggedIn = false; try { while (true) { - // Receive and log query. - Packet packet = connection.Receive(); + // Receive packet during a valid time frame. + CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(loggedIn ? _authorizedTimeout : _unauthorizedTimeout); + Packet packet = await connection.Read(timeoutCts.Token); + + // Log packet. LogPacket(connection, packet, true); + if (packet.Code == PacketCode.Login) + loggedIn = true; // Queue handling of known queries. - if (_packetHandlers.TryGetValue(packet.Code, out Action? handler)) - _jobs.Add(() => handler(connection, packet)); + if (_packetHandlers.TryGetValue(packet.Code, out PacketHandler? handler)) + await _jobs.Writer.WriteAsync(() => handler(connection, packet, ct), ct); else - ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}."); + Log(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}."); } } catch (Exception ex) { - ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}"); - _jobs.Add(() => OnDisconnectUser(connection)); + Log(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}"); + + ct.ThrowIfCancellationRequested(); + await _jobs.Writer.WriteAsync(() => OnDisconnectUserAsync(connection, ct), ct); } } - private void LeaveRoom(Room? room, int leftID) + private async ValueTask LeaveRoom(Room? room, int leftID, CancellationToken ct) { // Close an abandoned room. bool roomClosed = room != null @@ -143,19 +165,19 @@ namespace Syroot.Worms.Worms2.GameServer // Notify room leave, if any. if (room != null) { - SendPacket(user.Connection, new Packet(PacketCode.Leave, + await SendPacket(user.Connection, ct, new Packet(PacketCode.Leave, value2: room.ID, value10: leftID)); } // Notify room close, if any. if (roomClosed) - SendPacket(user.Connection, new Packet(PacketCode.Close, value10: room!.ID)); + await SendPacket(user.Connection, ct, new Packet(PacketCode.Close, value10: room!.ID)); } } // ---- Handlers ---- - private void OnListRooms(PacketConnection connection, Packet packet) + private async ValueTask OnListRooms(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value4 != 0) @@ -163,16 +185,16 @@ namespace Syroot.Worms.Worms2.GameServer foreach (Room room in _rooms) { - SendPacket(connection, new Packet(PacketCode.ListItem, + await SendPacket(connection, ct, new Packet(PacketCode.ListItem, value1: room.ID, data: String.Empty, // do not report creator IP name: room.Name, session: room.Session)); } - SendPacket(connection, new Packet(PacketCode.ListEnd)); + await SendPacket(connection, ct, new Packet(PacketCode.ListEnd)); } - private void OnListUsers(PacketConnection connection, Packet packet) + private async ValueTask OnListUsers(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0) @@ -180,15 +202,15 @@ namespace Syroot.Worms.Worms2.GameServer foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID)) // notably includes the user itself { - SendPacket(connection, new Packet(PacketCode.ListItem, + await SendPacket(connection, ct, new Packet(PacketCode.ListItem, value1: user.ID, name: user.Name, session: user.Session)); } - SendPacket(connection, new Packet(PacketCode.ListEnd)); + await SendPacket(connection, ct, new Packet(PacketCode.ListEnd)); } - private void OnListGames(PacketConnection connection, Packet packet) + private async ValueTask OnListGames(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0) @@ -196,16 +218,16 @@ namespace Syroot.Worms.Worms2.GameServer foreach (Game game in _games.Where(x => x.RoomID == fromUser.RoomID)) { - SendPacket(connection, new Packet(PacketCode.ListItem, + await SendPacket(connection, ct, new Packet(PacketCode.ListItem, value1: game.ID, data: game.IPAddress.ToString(), name: game.Name, session: game.Session)); } - SendPacket(connection, new Packet(PacketCode.ListEnd)); + await SendPacket(connection, ct, new Packet(PacketCode.ListEnd)); } - private void OnLogin(PacketConnection connection, Packet packet) + private async ValueTask OnLogin(PacketConnection connection, Packet packet, CancellationToken ct) { if (packet.Value1 == null || packet.Value4 == null || packet.Name == null || packet.Session == null) return; @@ -213,7 +235,7 @@ namespace Syroot.Worms.Worms2.GameServer // Check if user name is valid and not already taken. if (_users.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase))) { - SendPacket(connection, new Packet(PacketCode.LoginReply, value1: 0, error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.LoginReply, value1: 0, error: 1)); } else { @@ -222,20 +244,17 @@ namespace Syroot.Worms.Worms2.GameServer // Notify other users about new user. foreach (User user in _users) { - SendPacket(user.Connection, new Packet(PacketCode.Login, - value1: newUser.ID, - value4: 0, - name: newUser.Name, - session: newUser.Session)); + await SendPacket(user.Connection, ct, new Packet(PacketCode.Login, + value1: newUser.ID, value4: 0, name: newUser.Name, session: newUser.Session)); } // Register new user and send reply to him. _users.Add(newUser); - SendPacket(connection, new Packet(PacketCode.LoginReply, value1: newUser.ID, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.LoginReply, value1: newUser.ID, error: 0)); } } - private void OnCreateRoom(PacketConnection connection, Packet packet) + private async ValueTask OnCreateRoom(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value1 != 0 || packet.Value4 != 0 || packet.Data == null @@ -252,7 +271,7 @@ namespace Syroot.Worms.Worms2.GameServer // Notify other users about new room. foreach (User user in _users.Where(x => x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.CreateRoom, + await SendPacket(user.Connection, ct, new Packet(PacketCode.CreateRoom, value1: newRoom.ID, value4: 0, data: String.Empty, // do not report creator IP @@ -261,19 +280,15 @@ namespace Syroot.Worms.Worms2.GameServer } // Send reply to creator. - SendPacket(connection, new Packet(PacketCode.CreateRoomReply, - value1: newRoom.ID, - error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.CreateRoomReply, value1: newRoom.ID, error: 0)); } else { - SendPacket(connection, new Packet(PacketCode.CreateRoomReply, - value1: 0, - error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.CreateRoomReply, value1: 0, error: 1)); } } - private void OnJoin(PacketConnection connection, Packet packet) + private async ValueTask OnJoin(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID) @@ -287,34 +302,34 @@ namespace Syroot.Worms.Worms2.GameServer // Notify other users about the join. foreach (User user in _users.Where(x => x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.Join, + await SendPacket(user.Connection, ct, new Packet(PacketCode.Join, value2: fromUser.RoomID, value10: fromUser.ID)); } // Send reply to joiner. - SendPacket(connection, new Packet(PacketCode.JoinReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.JoinReply, error: 0)); } else if (_games.Any(x => x.ID == packet.Value2 && x.RoomID == fromUser.RoomID)) { // Notify other users about the join. foreach (User user in _users.Where(x => x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.Join, + await SendPacket(user.Connection, ct, new Packet(PacketCode.Join, value2: fromUser.RoomID, value10: fromUser.ID)); } // Send reply to joiner. - SendPacket(connection, new Packet(PacketCode.JoinReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.JoinReply, error: 0)); } else { - SendPacket(connection, new Packet(PacketCode.JoinReply, error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.JoinReply, error: 1)); } } - private void OnLeave(PacketConnection connection, Packet packet) + private async ValueTask OnLeave(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID) @@ -323,20 +338,20 @@ namespace Syroot.Worms.Worms2.GameServer // Require valid room ID (never sent for games, users disconnect if leaving a game). if (packet.Value2 == fromUser.RoomID) { - LeaveRoom(_rooms.FirstOrDefault(x => x.ID == fromUser.RoomID), fromUser.ID); + await LeaveRoom(_rooms.FirstOrDefault(x => x.ID == fromUser.RoomID), fromUser.ID, ct); fromUser.RoomID = 0; // Reply to leaver. - SendPacket(connection, new Packet(PacketCode.LeaveReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.LeaveReply, error: 0)); } else { // Reply to leaver. - SendPacket(connection, new Packet(PacketCode.LeaveReply, error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.LeaveReply, error: 1)); } } - private void OnDisconnectUser(PacketConnection connection) + private async ValueTask OnDisconnectUserAsync(PacketConnection connection, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null) @@ -356,23 +371,22 @@ namespace Syroot.Worms.Worms2.GameServer // Notify other users. foreach (User user in _users.Where(x => x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.Leave, value2: game.ID, value10: fromUser.ID)); - SendPacket(user.Connection, new Packet(PacketCode.Close, value10: game.ID)); + await SendPacket(user.Connection, ct, new Packet(PacketCode.Leave, + value2: game.ID, value10: fromUser.ID)); + await SendPacket(user.Connection, ct, new Packet(PacketCode.Close, + value10: game.ID)); } } // Close any abandoned room. - LeaveRoom(_rooms.FirstOrDefault(x => x.ID == roomID), leftID); + await LeaveRoom(_rooms.FirstOrDefault(x => x.ID == roomID), leftID, ct); // Notify user disconnect. foreach (User user in _users) - { - SendPacket(user.Connection, new Packet(PacketCode.DisconnectUser, - value10: fromUser.ID)); - } + await SendPacket(user.Connection, ct, new Packet(PacketCode.DisconnectUser, value10: fromUser.ID)); } - private void OnClose(PacketConnection connection, Packet packet) + private async ValueTask OnClose(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value10 == null) @@ -380,10 +394,10 @@ namespace Syroot.Worms.Worms2.GameServer // Never sent for games, users disconnect if leaving a game. // Simply reply success to client, the server decides when to actually close rooms. - SendPacket(connection, new Packet(PacketCode.CloseReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.CloseReply, error: 0)); } - private void OnCreateGame(PacketConnection connection, Packet packet) + private async ValueTask OnCreateGame(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value1 != 0 || packet.Value2 != fromUser.RoomID || packet.Value4 != 0x800 @@ -401,7 +415,7 @@ namespace Syroot.Worms.Worms2.GameServer // Notify other users about new game, even those in other rooms. foreach (User user in _users.Where(x => x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.CreateGame, + await SendPacket(user.Connection, ct, new Packet(PacketCode.CreateGame, value1: newGame.ID, value2: newGame.RoomID, value4: 0x800, @@ -411,21 +425,20 @@ namespace Syroot.Worms.Worms2.GameServer } // Send reply to host. - SendPacket(connection, new Packet(PacketCode.CreateGameReply, value1: newGame.ID, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.CreateGameReply, value1: newGame.ID, error: 0)); } else { - SendPacket(connection, new Packet(PacketCode.CreateGameReply, value1: 0, error: 2)); - SendPacket(connection, new Packet(PacketCode.ChatRoom, + await SendPacket(connection, ct, new Packet(PacketCode.CreateGameReply, value1: 0, error: 2)); + await SendPacket(connection, ct, new Packet(PacketCode.ChatRoom, value0: fromUser.ID, value3: fromUser.RoomID, - data: $"GRP:Cannot host your game. Please use the Worms 2 Memory Changer to set your IP " - + $"{fromUser.Connection.RemoteEndPoint.Address}. For more information, visit " - + "worms2d.info/Worms_2_Memory_Changer")); + data: $"GRP:Cannot host your game. Please use FrontendKitWS with fkNetcode. More information at " + + "worms2d.info/fkNetcode")); } } - private void OnChatRoom(PacketConnection connection, Packet packet) + private async ValueTask OnChatRoom(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value0 != fromUser.ID || packet.Value3 == null || packet.Data == null) @@ -442,17 +455,17 @@ namespace Syroot.Worms.Worms2.GameServer string message = packet.Data.Substring(prefix.Length); foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID && x != fromUser)) { - SendPacket(user.Connection, new Packet(PacketCode.ChatRoom, + await SendPacket(user.Connection, ct, new Packet(PacketCode.ChatRoom, value0: fromUser.ID, value3: user.RoomID, data: prefix + message)); } // Notify sender. - SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 0)); } else { - SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 1)); } } else if (packet.Data.StartsWith(prefix = $"PRV:[ {fromUser.Name} ] ", StringComparison.InvariantCulture)) @@ -461,23 +474,23 @@ namespace Syroot.Worms.Worms2.GameServer User? user = _users.FirstOrDefault(x => x.RoomID == fromUser.RoomID && x.ID == targetID); if (user == null) { - SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1)); + await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 1)); } else { // Notify receiver of the message. string message = packet.Data.Substring(prefix.Length); - SendPacket(user.Connection, new Packet(PacketCode.ChatRoom, + await SendPacket(user.Connection, ct, new Packet(PacketCode.ChatRoom, value0: fromUser.ID, value3: user.ID, data: prefix + message)); // Notify sender. - SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0)); + await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 0)); } } } - private void OnConnectGame(PacketConnection connection, Packet packet) + private async ValueTask OnConnectGame(PacketConnection connection, Packet packet, CancellationToken ct) { User? fromUser = GetUser(connection); if (fromUser == null || packet.Value0 == null) @@ -487,16 +500,20 @@ namespace Syroot.Worms.Worms2.GameServer Game? game = _games.FirstOrDefault(x => x.ID == packet.Value0 && x.RoomID == fromUser.RoomID); if (game == null) { - SendPacket(connection, new Packet(PacketCode.ConnectGameReply, + await SendPacket(connection, ct, new Packet(PacketCode.ConnectGameReply, data: String.Empty, error: 1)); } else { - SendPacket(connection, new Packet(PacketCode.ConnectGameReply, + await SendPacket(connection, ct, new Packet(PacketCode.ConnectGameReply, data: game.IPAddress.ToString(), error: 0)); } } + + // ---- CLASSES, STRUCTS & ENUMS ------------------------------------------------------------------------------- + + private delegate ValueTask PacketHandler(PacketConnection connection, Packet packet, CancellationToken ct); } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Syroot.Worms.Worms2.GameServer.csproj b/src/tool/Syroot.Worms.Worms2.GameServer/Syroot.Worms.Worms2.GameServer.csproj index ac75ecb..45688b7 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Syroot.Worms.Worms2.GameServer.csproj +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Syroot.Worms.Worms2.GameServer.csproj @@ -2,6 +2,7 @@ + true ..\..\..\res\icon.ico w2server Syroot @@ -13,6 +14,7 @@ + diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/TcpListenerExtensions.cs b/src/tool/Syroot.Worms.Worms2.GameServer/TcpListenerExtensions.cs new file mode 100644 index 0000000..57f83a4 --- /dev/null +++ b/src/tool/Syroot.Worms.Worms2.GameServer/TcpListenerExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Syroot.Worms.Worms2.GameServer +{ + /// + /// Represents extension methods for instances. + /// + internal static class TcpListenerExtensions + { + // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- + + /// + /// Accepts a pending connection request as an asynchronous operation. + /// + /// The instance. + /// The task object representing the asynchronous operation. The + /// property on the task object returns a used to send and receive data. + internal static async Task AcceptTcpClientAsync(this TcpListener tcpListener, + CancellationToken cancellationToken = default) + { + using (cancellationToken.Register(() => tcpListener.Stop())) + { + try + { + return await tcpListener.AcceptTcpClientAsync(); + } + catch (InvalidOperationException) + { + cancellationToken.ThrowIfCancellationRequested(); + throw; + } + } + } + } +}