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;
+ }
+ }
+ }
+ }
+}