mirror of
https://gitlab.com/Syroot/Worms.git
synced 2025-01-13 07:18:00 +03:00
Rewrite server netcode to be asynchronous and support timeouts. Allow multiple proxy connections.
This commit is contained in:
parent
65522b6af5
commit
bb2b7e72b5
23
src/library/Syroot.Worms/Core/EncodingExtensions.cs
Normal file
23
src/library/Syroot.Worms/Core/EncodingExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <param name="encoding">The 1-byte <see cref="Encoding"/> to use or <see langword="null"/> to use
|
||||
/// <see cref="Encoding.ASCII"/>.</param>
|
||||
/// <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.
|
||||
Span<byte> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -63,8 +63,7 @@ namespace Syroot.Worms.IO
|
||||
public static void WriteFixedString(this Stream stream, string value, int length, Encoding? encoding = null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@ -117,42 +116,5 @@ namespace Syroot.Worms.IO
|
||||
/// <param name="stream">The <see cref="Stream"/> instance to write with.</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);
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
|
41
src/library/Syroot.Worms/Shims/System.IO.Stream.cs
Normal file
41
src/library/Syroot.Worms/Shims/System.IO.Stream.cs
Normal 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
|
@ -1,18 +1,10 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Syroot.Worms.IO
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Text
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents extension methods for <see cref="Encoding"/> instances.
|
||||
/// </summary>
|
||||
public static class EncodingExtensions
|
||||
public static class EncodingShims
|
||||
{
|
||||
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
// ---- Backports ----
|
||||
|
||||
public unsafe static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> 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
|
@ -16,6 +16,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.0.0" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -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
|
||||
/// </summary>
|
||||
internal class Packet
|
||||
{
|
||||
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
|
||||
|
||||
private const int _maxDataSize = 0x1000;
|
||||
|
||||
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
@ -59,57 +51,57 @@ namespace Syroot.Worms.Worms2.GameServer
|
||||
/// <summary>
|
||||
/// Gets or sets <see cref="PacketCode"/> describing the action of the packet.
|
||||
/// </summary>
|
||||
internal PacketCode Code { get; set; }
|
||||
internal PacketCode Code;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value0 { get; set; }
|
||||
internal int? Value0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value1 { get; set; }
|
||||
internal int? Value1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value2 { get; set; }
|
||||
internal int? Value2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value3 { get; set; }
|
||||
internal int? Value3;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value4 { get; set; }
|
||||
internal int? Value4;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a parameter for the action.
|
||||
/// </summary>
|
||||
internal int? Value10 { get; set; }
|
||||
internal int? Value10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a textual parameter for the action.
|
||||
/// </summary>
|
||||
internal string? Data { get; set; }
|
||||
internal string? Data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an error code returned from the server after executing the action.
|
||||
/// </summary>
|
||||
internal int? Error { get; set; }
|
||||
internal int? Error;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a named parameter for the action.
|
||||
/// </summary>
|
||||
internal string? Name { get; set; }
|
||||
internal string? Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a <see cref="SessionInfo"/> for the action.
|
||||
/// </summary>
|
||||
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) -------------------------------------------------------------------------------------
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a duplex connection to a client, allowing to receive and send <see cref="Packet"/> instances.
|
||||
/// </summary>
|
||||
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
|
||||
/// <param name="client">The <see cref="TcpClient"/> to communicate with.</param>
|
||||
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) -------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Blocks until a <see cref="Packet"/> was received, and returns it.
|
||||
/// Receives a <see cref="Packet"/> instance asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>The received <see cref="Packet"/>.</returns>
|
||||
internal Packet Receive()
|
||||
/// <returns>The read <see cref="Packet"/> instance.</returns>
|
||||
internal async ValueTask<Packet> Read(CancellationToken ct)
|
||||
{
|
||||
lock (_recvLock)
|
||||
Packet packet = new Packet();
|
||||
PacketField at = PacketField.None;
|
||||
PacketField fields = PacketField.None;
|
||||
int dataLength = 0;
|
||||
bool get(in ReadOnlySequence<byte> buffer, out SequencePosition consumedTo)
|
||||
{
|
||||
Packet packet = new Packet();
|
||||
packet.Receive(_stream);
|
||||
return packet;
|
||||
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;
|
||||
}
|
||||
|
||||
_reader.AdvanceTo(buffer.Start, buffer.End);
|
||||
if (read.IsCompleted)
|
||||
throw new EndOfStreamException("No more data.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocks until the given <paramref name="packet"/> was sent.
|
||||
/// Sends a <see cref="Packet"/> instance asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="packet">The <see cref="Packet"/> to send.</param>
|
||||
internal void Send(Packet packet)
|
||||
/// <param name="packet">The <see cref="Packet"/> instance to write.</param>
|
||||
/// <returns>Whether the instance was written successfully.</returns>
|
||||
internal async ValueTask<bool> 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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs
Normal file
25
src/tool/Syroot.Worms.Worms2.GameServer/PacketField.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"Syroot.Worms.Worms2.GameServer": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "127.0.0.1:17002"
|
||||
"commandLineArgs": "17001"
|
||||
}
|
||||
}
|
||||
}
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
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<User> _users = new List<User>();
|
||||
private readonly List<Room> _rooms = new List<Room>();
|
||||
private readonly List<Game> _games = new List<Game>();
|
||||
private readonly BlockingCollection<Action> _jobs = new BlockingCollection<Action>();
|
||||
private readonly Dictionary<PacketCode, Action<PacketConnection, Packet>> _packetHandlers;
|
||||
private readonly Channel<Func<ValueTask>> _jobs;
|
||||
private readonly Dictionary<PacketCode, PacketHandler> _packetHandlers;
|
||||
|
||||
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Server"/> class.
|
||||
/// </summary>
|
||||
internal Server() => _packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
|
||||
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<Func<ValueTask>>(new UnboundedChannelOptions { SingleReader = true });
|
||||
_packetHandlers = new Dictionary<PacketCode, PacketHandler>
|
||||
{
|
||||
[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 <paramref name="localEndPoint"/> and dispatches
|
||||
/// them into their own threads.
|
||||
/// </summary>
|
||||
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<ValueTask> 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<PacketConnection, Packet>? 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);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<!-- Metadata -->
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationIcon>..\..\..\res\icon.ico</ApplicationIcon>
|
||||
<AssemblyName>w2server</AssemblyName>
|
||||
<Authors>Syroot</Authors>
|
||||
@ -13,6 +14,7 @@
|
||||
<!-- References -->
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<ProjectReference Include="..\..\library\Syroot.Worms\Syroot.Worms.csproj" />
|
||||
</ItemGroup>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user