Rewrite server netcode to be asynchronous and support timeouts. Allow multiple proxy connections.

This commit is contained in:
Ray Koopa 2020-07-14 20:53:11 +02:00
parent 65522b6af5
commit bb2b7e72b5
14 changed files with 537 additions and 312 deletions

View File

@ -0,0 +1,23 @@
using System;
using System.Text;
namespace Syroot.Worms.Core
{
/// <summary>
/// Represents extension methods for <see cref="Encoding"/> instances.
/// </summary>
public static class EncodingExtensions
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public static string GetZeroTerminatedString(this Encoding encoding, ReadOnlySpan<byte> bytes)
=> encoding.GetString(bytes.Slice(0, Math.Max(0, bytes.IndexOf((byte)0))));
public static int GetZeroTerminatedBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> bytes)
{
int length = encoding.GetBytes(chars, bytes);
bytes[length] = 0;
return ++length;
}
}
}

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Syroot.Worms.Core;
namespace Syroot.Worms.IO namespace Syroot.Worms.IO
{ {
@ -21,13 +22,12 @@ namespace Syroot.Worms.IO
/// <param name="encoding">The 1-byte <see cref="Encoding"/> to use or <see langword="null"/> to use /// <param name="encoding">The 1-byte <see cref="Encoding"/> to use or <see langword="null"/> to use
/// <see cref="Encoding.ASCII"/>.</param> /// <see cref="Encoding.ASCII"/>.</param>
/// <returns>The read string.</returns> /// <returns>The read string.</returns>
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. // Ensure to not try to decode any bytes after the 0 termination.
Span<byte> bytes = stackalloc byte[length]; Span<byte> bytes = stackalloc byte[length];
stream.Read(bytes); stream.Read(bytes);
for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ; return (encoding ?? Encoding.ASCII).GetZeroTerminatedString(bytes);
return (encoding ?? Encoding.ASCII).GetString(bytes.Slice(0, length));
} }
/// <summary> /// <summary>
@ -63,7 +63,6 @@ namespace Syroot.Worms.IO
public static void WriteFixedString(this Stream stream, string value, int length, Encoding? encoding = null) public static void WriteFixedString(this Stream stream, string value, int length, Encoding? encoding = null)
{ {
Span<byte> bytes = stackalloc byte[length]; Span<byte> bytes = stackalloc byte[length];
if (value != null)
(encoding ?? Encoding.ASCII).GetBytes(value.AsSpan(), bytes); (encoding ?? Encoding.ASCII).GetBytes(value.AsSpan(), bytes);
stream.Write(bytes); stream.Write(bytes);
} }
@ -117,42 +116,5 @@ namespace Syroot.Worms.IO
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param> /// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="value">The instance to write into the current stream.</param> /// <param name="value">The instance to write into the current stream.</param>
public static void Save<T>(this Stream stream, T value) where T : ISaveable => value.Save(stream); public static void Save<T>(this Stream stream, T value) where T : ISaveable => value.Save(stream);
#if NETSTANDARD2_0
// ---- Backports ----
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="buffer">A region of memory. When this method returns, the contents of this region are replaced
/// by the bytes read from the current source.</param>
/// <returns>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.</returns>
/// <remarks>
/// This .NET Standard 2.0 backport requires a temporary copy.
/// </remarks>
public static int Read(this Stream stream, Span<byte> buffer)
{
byte[] bytes = new byte[buffer.Length];
int bytesRead = stream.Read(bytes);
bytes.AsSpan(0, bytesRead).CopyTo(buffer);
return bytesRead;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance.</param>
/// <param name="value">A region of memory. This method copies the contents of this region to the current
/// stream.</param>
/// <remarks>
/// This .NET Standard 2.0 backport requires a temporary copy.
/// </remarks>
public static void Write(this Stream stream, ReadOnlySpan<byte> value) => stream.Write(value.ToArray());
#endif
} }
} }

View File

@ -0,0 +1,41 @@
#if NETSTANDARD2_0
namespace System.IO
{
/// <summary>
/// Represents extension methods for <see cref="Stream"/> instances.
/// </summary>
public static class StreamShims
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="buffer">A region of memory. When this method returns, the contents of this region are replaced
/// by the bytes read from the current source.</param>
/// <returns>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.</returns>
/// <remarks>This .NET Standard 2.0 backport requires a temporary copy.</remarks>
public static int Read(this Stream stream, Span<byte> buffer)
{
byte[] bytes = new byte[buffer.Length];
int bytesRead = stream.Read(bytes);
bytes.AsSpan(0, bytesRead).CopyTo(buffer);
return bytesRead;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance.</param>
/// <param name="value">A region of memory. This method copies the contents of this region to the current
/// stream.</param>
/// <remarks>This .NET Standard 2.0 backport requires a temporary copy.</remarks>
public static void Write(this Stream stream, ReadOnlySpan<byte> value) => stream.Write(value.ToArray());
}
}
#endif

View File

@ -1,18 +1,10 @@
using System; #if NETSTANDARD2_0
using System.Text; namespace System.Text
namespace Syroot.Worms.IO
{ {
/// <summary> public static class EncodingShims
/// Represents extension methods for <see cref="Encoding"/> instances.
/// </summary>
public static class EncodingExtensions
{ {
// ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- // ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
#if NETSTANDARD2_0
// ---- Backports ----
public unsafe static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> bytes) public unsafe static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> bytes)
{ {
fixed (byte* pBytes = bytes) fixed (byte* pBytes = bytes)
@ -25,6 +17,6 @@ namespace Syroot.Worms.IO
fixed (byte* pBytes = bytes) fixed (byte* pBytes = bytes)
return encoding.GetString(pBytes, bytes.Length); return encoding.GetString(pBytes, bytes.Length);
} }
}
}
#endif #endif
}
}

View File

@ -16,6 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" /> <PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,8 +1,4 @@
using System; using System.Text;
using System.IO;
using System.Text;
using Syroot.BinaryData;
using Syroot.Worms.IO;
namespace Syroot.Worms.Worms2.GameServer namespace Syroot.Worms.Worms2.GameServer
{ {
@ -11,10 +7,6 @@ namespace Syroot.Worms.Worms2.GameServer
/// </summary> /// </summary>
internal class Packet internal class Packet
{ {
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxDataSize = 0x1000;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary> /// <summary>
@ -59,57 +51,57 @@ namespace Syroot.Worms.Worms2.GameServer
/// <summary> /// <summary>
/// Gets or sets <see cref="PacketCode"/> describing the action of the packet. /// Gets or sets <see cref="PacketCode"/> describing the action of the packet.
/// </summary> /// </summary>
internal PacketCode Code { get; set; } internal PacketCode Code;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value0 { get; set; } internal int? Value0;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value1 { get; set; } internal int? Value1;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value2 { get; set; } internal int? Value2;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value3 { get; set; } internal int? Value3;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value4 { get; set; } internal int? Value4;
/// <summary> /// <summary>
/// Gets or sets a parameter for the action. /// Gets or sets a parameter for the action.
/// </summary> /// </summary>
internal int? Value10 { get; set; } internal int? Value10;
/// <summary> /// <summary>
/// Gets or sets a textual parameter for the action. /// Gets or sets a textual parameter for the action.
/// </summary> /// </summary>
internal string? Data { get; set; } internal string? Data;
/// <summary> /// <summary>
/// Gets or sets an error code returned from the server after executing the action. /// Gets or sets an error code returned from the server after executing the action.
/// </summary> /// </summary>
internal int? Error { get; set; } internal int? Error;
/// <summary> /// <summary>
/// Gets or sets a named parameter for the action. /// Gets or sets a named parameter for the action.
/// </summary> /// </summary>
internal string? Name { get; set; } internal string? Name;
/// <summary> /// <summary>
/// Gets or sets a <see cref="SessionInfo"/> for the action. /// Gets or sets a <see cref="SessionInfo"/> for the action.
/// </summary> /// </summary>
internal SessionInfo? Session { get; set; } internal SessionInfo? Session;
// ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- // ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
@ -117,7 +109,6 @@ namespace Syroot.Worms.Worms2.GameServer
public override string ToString() public override string ToString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.AppendLine($"{Code:D} {Code}"); sb.AppendLine($"{Code:D} {Code}");
if (Value0.HasValue) sb.AppendLine($" {nameof(Value0),7}: {Value0:X8}"); if (Value0.HasValue) sb.AppendLine($" {nameof(Value0),7}: {Value0:X8}");
if (Value1.HasValue) sb.AppendLine($" {nameof(Value1),7}: {Value1: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 (Error.HasValue) sb.AppendLine($" {nameof(Error),7}: {Error:X8}");
if (Name != null) sb.AppendLine($" {nameof(Name),7}: {Name}"); if (Name != null) sb.AppendLine($" {nameof(Name),7}: {Name}");
if (Session.HasValue) sb.AppendLine($" {nameof(Session),7}: {Session}"); if (Session.HasValue) sb.AppendLine($" {nameof(Session),7}: {Session}");
return sb.ToString().TrimEnd(); return sb.ToString().TrimEnd();
} }
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Blocks and reads the packet data from the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to read the packet data from.</param>
internal void Receive(Stream stream)
{
int dataLength = 0;
Code = stream.ReadEnum<PacketCode>(true);
Flags flags = stream.ReadEnum<Flags>(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<SessionInfo>();
}
/// <summary>
/// Blocks and writes the packet data to the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write the packet data to.</param>
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
}
} }
} }

View File

@ -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;
using System.Net.Sockets; 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 namespace Syroot.Worms.Worms2.GameServer
{ {
/// <summary> /// <summary>
/// Represents a duplex connection to a client, allowing to receive and send <see cref="Packet"/> instances. /// Represents a duplex connection to a client, allowing to receive and send <see cref="Packet"/> instances.
/// </summary> /// </summary>
internal class PacketConnection internal sealed class PacketConnection
{ {
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxDataSize = 0x1000;
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly Stream _stream; private readonly PipeReader _reader;
private readonly object _recvLock = new object(); private readonly PipeWriter _writer;
private readonly object _sendLock = new object(); private readonly SemaphoreSlim _recvLock = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
@ -24,7 +39,9 @@ namespace Syroot.Worms.Worms2.GameServer
/// <param name="client">The <see cref="TcpClient"/> to communicate with.</param> /// <param name="client">The <see cref="TcpClient"/> to communicate with.</param>
internal PacketConnection(TcpClient client) internal PacketConnection(TcpClient client)
{ {
_stream = client.GetStream(); Stream stream = client.GetStream();
_reader = PipeReader.Create(stream);
_writer = PipeWriter.Create(stream);
RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint; RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint;
} }
@ -38,27 +55,217 @@ namespace Syroot.Worms.Worms2.GameServer
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary> /// <summary>
/// Blocks until a <see cref="Packet"/> was received, and returns it. /// Receives a <see cref="Packet"/> instance asynchronously.
/// </summary> /// </summary>
/// <returns>The received <see cref="Packet"/>.</returns> /// <returns>The read <see cref="Packet"/> instance.</returns>
internal Packet Receive() internal async ValueTask<Packet> Read(CancellationToken ct)
{
lock (_recvLock)
{ {
Packet packet = new Packet(); Packet packet = new Packet();
packet.Receive(_stream); PacketField at = PacketField.None;
PacketField fields = PacketField.None;
int dataLength = 0;
bool get(in ReadOnlySequence<byte> buffer, out SequencePosition consumedTo)
{
consumedTo = default;
SequenceReader<byte> reader = new SequenceReader<byte>(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<byte> 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<byte> 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<byte> sessionBytes = stackalloc byte[Unsafe.SizeOf<SessionInfo>()];
if (!reader.TryCopyTo(sessionBytes)) break;
reader.Advance(sessionBytes.Length);
packet.Session = MemoryMarshal.Cast<byte, SessionInfo>(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<byte> buffer = read.Buffer;
if (get(buffer, out SequencePosition consumedTo))
{
_reader.AdvanceTo(consumedTo);
if (at == PacketField.All)
return packet; return packet;
} }
_reader.AdvanceTo(buffer.Start, buffer.End);
if (read.IsCompleted)
throw new EndOfStreamException("No more data.");
}
} }
/// <summary> /// <summary>
/// Blocks until the given <paramref name="packet"/> was sent. /// Sends a <see cref="Packet"/> instance asynchronously.
/// </summary> /// </summary>
/// <param name="packet">The <see cref="Packet"/> to send.</param> /// <param name="packet">The <see cref="Packet"/> instance to write.</param>
internal void Send(Packet packet) /// <returns>Whether the instance was written successfully.</returns>
internal async ValueTask<bool> Write(Packet packet, CancellationToken ct)
{ {
lock (_sendLock) unsafe int set()
packet.Send(_stream); {
// 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<SessionInfo>());
// Write the data.
Span<byte> span = _writer.GetSpan(size);
static void writeInt(ref Span<byte> 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<byte>(Unsafe.AsPointer(ref session), Unsafe.SizeOf<SessionInfo>()).CopyTo(span);
}
return size;
}
_writer.Advance(set());
FlushResult flush = await _writer.FlushAsync(ct);
return !flush.IsCanceled && !flush.IsCompleted;
} }
} }
} }

View File

@ -0,0 +1,25 @@
using System;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a bitset determining which fields are available in a <see cref="Packet"/> instance.
/// </summary>
[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
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Net; using System.Net;
using System.Threading.Tasks;
namespace Syroot.Worms.Worms2.GameServer namespace Syroot.Worms.Worms2.GameServer
{ {
@ -10,14 +11,14 @@ namespace Syroot.Worms.Worms2.GameServer
{ {
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Main(string[] args) private static async Task Main(string[] args)
{ {
string? argEndPoint = args.Length > 0 ? args[0] : null; string? argEndPoint = args.Length > 0 ? args[0] : null;
Server server = new Server(); 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) private static IPEndPoint ParseEndPoint(string? s, IPEndPoint fallback)

View File

@ -2,7 +2,7 @@
"profiles": { "profiles": {
"Syroot.Worms.Worms2.GameServer": { "Syroot.Worms.Worms2.GameServer": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "127.0.0.1:17002" "commandLineArgs": "17001"
} }
} }
} }

View File

@ -1,6 +1,8 @@
using System.Drawing; using System;
using System.Drawing;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Syroot.ColoredConsole; using Syroot.ColoredConsole;
@ -13,15 +15,15 @@ namespace Syroot.Worms.Worms2.GameServer
{ {
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal static void Run(IPEndPoint localEndPoint) internal static async Task Run(IPEndPoint localEndPoint, CancellationToken ct = default)
{ {
// Start listening for clients to intercept. // Start listening for clients to intercept.
TcpListener listener = new TcpListener(localEndPoint); TcpListener listener = new TcpListener(localEndPoint);
listener.Start(); listener.Start();
ColorConsole.WriteLine(Color.Orange, $"Proxy listening under {localEndPoint}..."); Log(Color.Orange, $"Proxy listening under {localEndPoint}...");
TcpClient? client; TcpClient? client;
while ((client = listener.AcceptTcpClient()) != null) while ((client = await listener.AcceptTcpClientAsync(ct)) != null)
{ {
// Connect to server. // Connect to server.
TcpClient server = new TcpClient(); TcpClient server = new TcpClient();
@ -29,25 +31,39 @@ namespace Syroot.Worms.Worms2.GameServer
PacketConnection clientConnection = new PacketConnection(client); PacketConnection clientConnection = new PacketConnection(client);
PacketConnection serverConnection = new PacketConnection(server); PacketConnection serverConnection = new PacketConnection(server);
ColorConsole.WriteLine(Color.Green, $"{clientConnection.RemoteEndPoint} connected."); Log(Color.Green, $"{clientConnection.RemoteEndPoint} connected.");
Task.Run(() => Forward(clientConnection, serverConnection, true)); CancellationTokenSource disconnectCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task.Run(() => Forward(serverConnection, clientConnection, false)); _ = Task.WhenAny(
Forward(clientConnection, serverConnection, true, disconnectCts.Token),
Forward(serverConnection, clientConnection, false, disconnectCts.Token))
.ContinueWith((antecedent) => disconnectCts.Cancel());
} }
} }
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- 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)
{
string prefix = fromClient
? $"{from.RemoteEndPoint} >> {to.RemoteEndPoint}"
: $"{to.RemoteEndPoint} << {from.RemoteEndPoint}";
try
{ {
while (true) while (true)
{ {
Packet packet = from.Receive(); Packet packet = await from.Read(ct);
if (fromClient) Log(fromClient ? Color.Cyan : Color.Magenta, $"{prefix} {packet}");
ColorConsole.WriteLine(Color.Cyan, $"{from.RemoteEndPoint} >> {to.RemoteEndPoint} | {packet}"); await to.Write(packet, ct);
else }
ColorConsole.WriteLine(Color.Magenta, $"{to.RemoteEndPoint} << {from.RemoteEndPoint} | {packet}"); }
to.Send(packet); catch (Exception ex)
{
Log(Color.Red, $"{prefix} closed. {ex.Message}");
} }
} }
} }

View File

@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using Syroot.ColoredConsole; using Syroot.ColoredConsole;
@ -15,21 +16,29 @@ namespace Syroot.Worms.Worms2.GameServer
/// </summary> /// </summary>
internal class Server internal class Server
{ {
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _authorizedTimeout = 10 * 60 * 1000;
private const int _unauthorizedTimeout = 3 * 1000;
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------
private int _lastID = 0x1000; // start at an offset to prevent bugs with chat private int _lastID = 0x1000; // start at an offset to prevent bugs with chat
private readonly List<User> _users = new List<User>(); private readonly List<User> _users = new List<User>();
private readonly List<Room> _rooms = new List<Room>(); private readonly List<Room> _rooms = new List<Room>();
private readonly List<Game> _games = new List<Game>(); private readonly List<Game> _games = new List<Game>();
private readonly BlockingCollection<Action> _jobs = new BlockingCollection<Action>(); private readonly Channel<Func<ValueTask>> _jobs;
private readonly Dictionary<PacketCode, Action<PacketConnection, Packet>> _packetHandlers; private readonly Dictionary<PacketCode, PacketHandler> _packetHandlers;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Server"/> class. /// Initializes a new instance of the <see cref="Server"/> class.
/// </summary> /// </summary>
internal Server() => _packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>> internal Server()
{
_jobs = Channel.CreateUnbounded<Func<ValueTask>>(new UnboundedChannelOptions { SingleReader = true });
_packetHandlers = new Dictionary<PacketCode, PacketHandler>
{ {
[PacketCode.ListRooms] = OnListRooms, [PacketCode.ListRooms] = OnListRooms,
[PacketCode.ListUsers] = OnListUsers, [PacketCode.ListUsers] = OnListUsers,
@ -43,6 +52,7 @@ namespace Syroot.Worms.Worms2.GameServer
[PacketCode.ChatRoom] = OnChatRoom, [PacketCode.ChatRoom] = OnChatRoom,
[PacketCode.ConnectGame] = OnConnectGame, [PacketCode.ConnectGame] = OnConnectGame,
}; };
}
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
@ -50,28 +60,31 @@ namespace Syroot.Worms.Worms2.GameServer
/// Begins listening for new clients connecting to the given <paramref name="localEndPoint"/> and dispatches /// Begins listening for new clients connecting to the given <paramref name="localEndPoint"/> and dispatches
/// them into their own threads. /// them into their own threads.
/// </summary> /// </summary>
internal void Run(IPEndPoint localEndPoint) internal async Task Run(IPEndPoint localEndPoint, CancellationToken ct = default)
{ {
// Begin handling any queued jobs. // Begin handling any queued jobs.
Task.Run(() => HandleJobs()); _ = HandleJobs(ct);
// Begin listening for new connections. Currently synchronous and blocking. // Begin listening for new connections.
HandleConnections(localEndPoint); await HandleConnections(localEndPoint, ct);
} }
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void SendPacket(PacketConnection connection, Packet packet) private static void Log(Color color, string message)
{ => ColorConsole.WriteLine(color, $"{DateTime.Now:HH:mm:ss} {message}");
LogPacket(connection, packet, false);
connection.Send(packet);
}
private static void LogPacket(PacketConnection connection, Packet packet, bool fromClient) private static void LogPacket(PacketConnection connection, Packet packet, bool fromClient)
{ {
if (fromClient) if (fromClient)
ColorConsole.WriteLine(Color.Cyan, $"{DateTime.Now:HH:mm:ss} {connection.RemoteEndPoint} >> {packet}"); Log(Color.Cyan, $"{connection.RemoteEndPoint} >> {packet}");
else 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) private User? GetUser(PacketConnection connection)
@ -82,53 +95,62 @@ namespace Syroot.Worms.Worms2.GameServer
return null; return null;
} }
private void HandleJobs() private async ValueTask HandleJobs(CancellationToken ct)
{ {
foreach (Action job in _jobs.GetConsumingEnumerable()) await foreach (Func<ValueTask> job in _jobs.Reader.ReadAllAsync(ct))
job(); await job();
} }
private void HandleConnections(IPEndPoint localEndPoint) private async ValueTask HandleConnections(IPEndPoint localEndPoint, CancellationToken ct)
{ {
// Start a new listener for new incoming connections. // Start a new listener for new incoming connections.
TcpListener listener = new TcpListener(localEndPoint); TcpListener listener = new TcpListener(localEndPoint);
listener.Start(); 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. // Dispatch each connection into its own thread.
TcpClient? client; TcpClient? client;
while ((client = listener.AcceptTcpClient()) != null) while ((client = await listener.AcceptTcpClientAsync(ct)) != null)
Task.Run(() => HandleClient(client)); _ = HandleClient(client, ct);
} }
private void HandleClient(TcpClient client) private async Task HandleClient(TcpClient client, CancellationToken ct)
{ {
PacketConnection connection = new PacketConnection(client); PacketConnection connection = new PacketConnection(client);
ColorConsole.WriteLine(Color.Green, $"{connection.RemoteEndPoint} connected."); Log(Color.Green, $"{connection.RemoteEndPoint} connected.");
bool loggedIn = false;
try try
{ {
while (true) while (true)
{ {
// Receive and log query. // Receive packet during a valid time frame.
Packet packet = connection.Receive(); CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(loggedIn ? _authorizedTimeout : _unauthorizedTimeout);
Packet packet = await connection.Read(timeoutCts.Token);
// Log packet.
LogPacket(connection, packet, true); LogPacket(connection, packet, true);
if (packet.Code == PacketCode.Login)
loggedIn = true;
// Queue handling of known queries. // Queue handling of known queries.
if (_packetHandlers.TryGetValue(packet.Code, out Action<PacketConnection, Packet>? handler)) if (_packetHandlers.TryGetValue(packet.Code, out PacketHandler? handler))
_jobs.Add(() => handler(connection, packet)); await _jobs.Writer.WriteAsync(() => handler(connection, packet, ct), ct);
else else
ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}."); Log(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}"); Log(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}");
_jobs.Add(() => OnDisconnectUser(connection));
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. // Close an abandoned room.
bool roomClosed = room != null bool roomClosed = room != null
@ -143,19 +165,19 @@ namespace Syroot.Worms.Worms2.GameServer
// Notify room leave, if any. // Notify room leave, if any.
if (room != null) if (room != null)
{ {
SendPacket(user.Connection, new Packet(PacketCode.Leave, await SendPacket(user.Connection, ct, new Packet(PacketCode.Leave,
value2: room.ID, value2: room.ID,
value10: leftID)); value10: leftID));
} }
// Notify room close, if any. // Notify room close, if any.
if (roomClosed) 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 ---- // ---- Handlers ----
private void OnListRooms(PacketConnection connection, Packet packet) private async ValueTask OnListRooms(PacketConnection connection, Packet packet, CancellationToken ct)
{ {
User? fromUser = GetUser(connection); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value4 != 0) if (fromUser == null || packet.Value4 != 0)
@ -163,16 +185,16 @@ namespace Syroot.Worms.Worms2.GameServer
foreach (Room room in _rooms) foreach (Room room in _rooms)
{ {
SendPacket(connection, new Packet(PacketCode.ListItem, await SendPacket(connection, ct, new Packet(PacketCode.ListItem,
value1: room.ID, value1: room.ID,
data: String.Empty, // do not report creator IP data: String.Empty, // do not report creator IP
name: room.Name, name: room.Name,
session: room.Session)); 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0) 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 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, value1: user.ID,
name: user.Name, name: user.Name,
session: user.Session)); 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0) 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)) 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, value1: game.ID,
data: game.IPAddress.ToString(), data: game.IPAddress.ToString(),
name: game.Name, name: game.Name,
session: game.Session)); 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) if (packet.Value1 == null || packet.Value4 == null || packet.Name == null || packet.Session == null)
return; return;
@ -213,7 +235,7 @@ namespace Syroot.Worms.Worms2.GameServer
// Check if user name is valid and not already taken. // Check if user name is valid and not already taken.
if (_users.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase))) 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 else
{ {
@ -222,20 +244,17 @@ namespace Syroot.Worms.Worms2.GameServer
// Notify other users about new user. // Notify other users about new user.
foreach (User user in _users) foreach (User user in _users)
{ {
SendPacket(user.Connection, new Packet(PacketCode.Login, await SendPacket(user.Connection, ct, new Packet(PacketCode.Login,
value1: newUser.ID, value1: newUser.ID, value4: 0, name: newUser.Name, session: newUser.Session));
value4: 0,
name: newUser.Name,
session: newUser.Session));
} }
// Register new user and send reply to him. // Register new user and send reply to him.
_users.Add(newUser); _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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value1 != 0 || packet.Value4 != 0 || packet.Data == null 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. // Notify other users about new room.
foreach (User user in _users.Where(x => x != fromUser)) 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, value1: newRoom.ID,
value4: 0, value4: 0,
data: String.Empty, // do not report creator IP data: String.Empty, // do not report creator IP
@ -261,19 +280,15 @@ namespace Syroot.Worms.Worms2.GameServer
} }
// Send reply to creator. // Send reply to creator.
SendPacket(connection, new Packet(PacketCode.CreateRoomReply, await SendPacket(connection, ct, new Packet(PacketCode.CreateRoomReply, value1: newRoom.ID, error: 0));
value1: newRoom.ID,
error: 0));
} }
else else
{ {
SendPacket(connection, new Packet(PacketCode.CreateRoomReply, await SendPacket(connection, ct, new Packet(PacketCode.CreateRoomReply, value1: 0, error: 1));
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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID) 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. // Notify other users about the join.
foreach (User user in _users.Where(x => x != fromUser)) 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, value2: fromUser.RoomID,
value10: fromUser.ID)); value10: fromUser.ID));
} }
// Send reply to joiner. // 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)) else if (_games.Any(x => x.ID == packet.Value2 && x.RoomID == fromUser.RoomID))
{ {
// Notify other users about the join. // Notify other users about the join.
foreach (User user in _users.Where(x => x != fromUser)) 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, value2: fromUser.RoomID,
value10: fromUser.ID)); value10: fromUser.ID));
} }
// Send reply to joiner. // Send reply to joiner.
SendPacket(connection, new Packet(PacketCode.JoinReply, error: 0)); await SendPacket(connection, ct, new Packet(PacketCode.JoinReply, error: 0));
} }
else 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID) 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). // Require valid room ID (never sent for games, users disconnect if leaving a game).
if (packet.Value2 == fromUser.RoomID) 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; fromUser.RoomID = 0;
// Reply to leaver. // Reply to leaver.
SendPacket(connection, new Packet(PacketCode.LeaveReply, error: 0)); await SendPacket(connection, ct, new Packet(PacketCode.LeaveReply, error: 0));
} }
else else
{ {
// Reply to leaver. // 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); User? fromUser = GetUser(connection);
if (fromUser == null) if (fromUser == null)
@ -356,23 +371,22 @@ namespace Syroot.Worms.Worms2.GameServer
// Notify other users. // Notify other users.
foreach (User user in _users.Where(x => x != fromUser)) foreach (User user in _users.Where(x => x != fromUser))
{ {
SendPacket(user.Connection, new Packet(PacketCode.Leave, value2: game.ID, value10: fromUser.ID)); await SendPacket(user.Connection, ct, new Packet(PacketCode.Leave,
SendPacket(user.Connection, new Packet(PacketCode.Close, value10: game.ID)); value2: game.ID, value10: fromUser.ID));
await SendPacket(user.Connection, ct, new Packet(PacketCode.Close,
value10: game.ID));
} }
} }
// Close any abandoned room. // 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. // Notify user disconnect.
foreach (User user in _users) foreach (User user in _users)
{ await SendPacket(user.Connection, ct, new Packet(PacketCode.DisconnectUser, value10: fromUser.ID));
SendPacket(user.Connection, 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value10 == null) 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. // Never sent for games, users disconnect if leaving a game.
// Simply reply success to client, the server decides when to actually close rooms. // 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value1 != 0 || packet.Value2 != fromUser.RoomID || packet.Value4 != 0x800 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. // Notify other users about new game, even those in other rooms.
foreach (User user in _users.Where(x => x != fromUser)) 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, value1: newGame.ID,
value2: newGame.RoomID, value2: newGame.RoomID,
value4: 0x800, value4: 0x800,
@ -411,21 +425,20 @@ namespace Syroot.Worms.Worms2.GameServer
} }
// Send reply to host. // 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 else
{ {
SendPacket(connection, new Packet(PacketCode.CreateGameReply, value1: 0, error: 2)); await SendPacket(connection, ct, new Packet(PacketCode.CreateGameReply, value1: 0, error: 2));
SendPacket(connection, new Packet(PacketCode.ChatRoom, await SendPacket(connection, ct, new Packet(PacketCode.ChatRoom,
value0: fromUser.ID, value0: fromUser.ID,
value3: fromUser.RoomID, value3: fromUser.RoomID,
data: $"GRP:Cannot host your game. Please use the Worms 2 Memory Changer to set your IP " data: $"GRP:Cannot host your game. Please use FrontendKitWS with fkNetcode. More information at "
+ $"{fromUser.Connection.RemoteEndPoint.Address}. For more information, visit " + "worms2d.info/fkNetcode"));
+ "worms2d.info/Worms_2_Memory_Changer"));
} }
} }
private void OnChatRoom(PacketConnection connection, Packet packet) private async ValueTask OnChatRoom(PacketConnection connection, Packet packet, CancellationToken ct)
{ {
User? fromUser = GetUser(connection); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value0 != fromUser.ID || packet.Value3 == null || packet.Data == null) 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); string message = packet.Data.Substring(prefix.Length);
foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID && x != fromUser)) 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, value0: fromUser.ID,
value3: user.RoomID, value3: user.RoomID,
data: prefix + message)); data: prefix + message));
} }
// Notify sender. // Notify sender.
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0)); await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 0));
} }
else 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)) 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); User? user = _users.FirstOrDefault(x => x.RoomID == fromUser.RoomID && x.ID == targetID);
if (user == null) if (user == null)
{ {
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1)); await SendPacket(connection, ct, new Packet(PacketCode.ChatRoomReply, error: 1));
} }
else else
{ {
// Notify receiver of the message. // Notify receiver of the message.
string message = packet.Data.Substring(prefix.Length); 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, value0: fromUser.ID,
value3: user.ID, value3: user.ID,
data: prefix + message)); data: prefix + message));
// Notify sender. // 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); User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value0 == null) 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); Game? game = _games.FirstOrDefault(x => x.ID == packet.Value0 && x.RoomID == fromUser.RoomID);
if (game == null) if (game == null)
{ {
SendPacket(connection, new Packet(PacketCode.ConnectGameReply, await SendPacket(connection, ct, new Packet(PacketCode.ConnectGameReply,
data: String.Empty, data: String.Empty,
error: 1)); error: 1));
} }
else else
{ {
SendPacket(connection, new Packet(PacketCode.ConnectGameReply, await SendPacket(connection, ct, new Packet(PacketCode.ConnectGameReply,
data: game.IPAddress.ToString(), data: game.IPAddress.ToString(),
error: 0)); error: 0));
} }
} }
// ---- CLASSES, STRUCTS & ENUMS -------------------------------------------------------------------------------
private delegate ValueTask PacketHandler(PacketConnection connection, Packet packet, CancellationToken ct);
} }
} }

View File

@ -2,6 +2,7 @@
<!-- Metadata --> <!-- Metadata -->
<PropertyGroup> <PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationIcon>..\..\..\res\icon.ico</ApplicationIcon> <ApplicationIcon>..\..\..\res\icon.ico</ApplicationIcon>
<AssemblyName>w2server</AssemblyName> <AssemblyName>w2server</AssemblyName>
<Authors>Syroot</Authors> <Authors>Syroot</Authors>
@ -13,6 +14,7 @@
<!-- References --> <!-- References -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Syroot.ColoredConsole" Version="1.0.1" /> <PackageReference Include="Syroot.ColoredConsole" Version="1.0.1" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
<ProjectReference Include="..\..\library\Syroot.Worms\Syroot.Worms.csproj" /> <ProjectReference Include="..\..\library\Syroot.Worms\Syroot.Worms.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,38 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents extension methods for <see cref="TcpListener"/> instances.
/// </summary>
internal static class TcpListenerExtensions
{
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Accepts a pending connection request as an asynchronous operation.
/// </summary>
/// <param name="tcpListener">The <see cref="TcpListener"/> instance.</param>
/// <returns>The task object representing the asynchronous operation. The <see cref="Task{TcpClient}.Result"/>
/// property on the task object returns a <see cref="TcpClient"/> used to send and receive data.</returns>
internal static async Task<TcpClient> AcceptTcpClientAsync(this TcpListener tcpListener,
CancellationToken cancellationToken = default)
{
using (cancellationToken.Register(() => tcpListener.Stop()))
{
try
{
return await tcpListener.AcceptTcpClientAsync();
}
catch (InvalidOperationException)
{
cancellationToken.ThrowIfCancellationRequested();
throw;
}
}
}
}
}