mirror of
https://gitlab.com/Syroot/Worms.git
synced 2025-01-13 15:27:59 +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.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
#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
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
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)
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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