Add quick Worms 2 server PoC. Makes BinaryStream FixedString extensions globally available.

This commit is contained in:
Ray Koopa 2020-07-07 02:45:00 +02:00
parent 420810fad2
commit a308ae49b1
7 changed files with 291 additions and 35 deletions

View File

@ -47,7 +47,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = " Solution Items", " Solut
test.xml = test.xml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.SchemeEditor", "tool\Syroot.Worms.SchemeEditor\Syroot.Worms.SchemeEditor.csproj", "{06FC5993-E3F0-4826-AE88-5639446A14B6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.Worms2.GameServer", "tool\Syroot.Worms.Worms2.GameServer\Syroot.Worms.Worms2.GameServer.csproj", "{13ABF717-5809-441D-A5D8-66E1EE75A390}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -119,10 +119,10 @@ Global
{1FAB6B9F-2585-46DC-81C0-579DC808C389}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FAB6B9F-2585-46DC-81C0-579DC808C389}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FAB6B9F-2585-46DC-81C0-579DC808C389}.Release|Any CPU.Build.0 = Release|Any CPU
{06FC5993-E3F0-4826-AE88-5639446A14B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06FC5993-E3F0-4826-AE88-5639446A14B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06FC5993-E3F0-4826-AE88-5639446A14B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06FC5993-E3F0-4826-AE88-5639446A14B6}.Release|Any CPU.Build.0 = Release|Any CPU
{13ABF717-5809-441D-A5D8-66E1EE75A390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13ABF717-5809-441D-A5D8-66E1EE75A390}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13ABF717-5809-441D-A5D8-66E1EE75A390}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13ABF717-5809-441D-A5D8-66E1EE75A390}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -144,7 +144,7 @@ Global
{392E4CA2-61D9-4BE1-B065-8801A9F102B8} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
{212F8090-9775-4098-BD44-9ABC01FBE553} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
{1FAB6B9F-2585-46DC-81C0-579DC808C389} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
{06FC5993-E3F0-4826-AE88-5639446A14B6} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
{13ABF717-5809-441D-A5D8-66E1EE75A390} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1CD4EDE2-A5FB-4A58-A850-3506AB7E7B69}

View File

@ -35,21 +35,6 @@ namespace Syroot.Worms.IO
return values;
}
/// <summary>
/// Reads a 0-terminated string which is stored in a fixed-size block of <paramref name="length"/> bytes.
/// </summary>
/// <param name="self">The extended <see cref="BinaryStream"/> instance.</param>
/// <param name="length">The number of bytes the fixed-size blocks takes.</param>
/// <returns>The read string.</returns>
public static string ReadFixedString(this BinaryStream self, int length)
{
// Ensure to not try to decode any bytes after the 0 termination.
byte[] bytes = self.ReadBytes(length);
for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ;
return self.Encoding.GetString(bytes, 0, length);
}
/// <summary>
/// Returns the current position of the stream at which a 4-byte placeholder has been written which can be
/// filled later with <see cref="SatisfyOffset"/>.
/// </summary>
@ -95,20 +80,6 @@ namespace Syroot.Worms.IO
foreach (Color color in colors)
WriteColor(self, color);
}
/// <summary>
/// Writes the given string into a fixed-size block of <paramref name="length"/> bytes and 0-terminates it.
/// </summary>
/// <param name="self">The extended <see cref="BinaryStream"/> instance.</param>
/// <param name="value">The string to write.</param>
/// <param name="length">The number of bytes the fixed-size block takes.</param>
public static void WriteFixedString(this BinaryStream self, string value, int length)
{
byte[] bytes = new byte[length];
if (value != null)
self.Encoding.GetBytes(value, 0, value.Length, bytes, 0);
self.WriteBytes(bytes);
}
}
}

View File

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

View File

@ -2,6 +2,8 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Syroot.Worms.IO
{
@ -12,6 +14,37 @@ namespace Syroot.Worms.IO
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
/// <summary>
/// Reads a 0-terminated string which is stored in a fixed-size block of <paramref name="length"/> bytes.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to read with.</param>
/// <param name="length">The number of bytes the fixed-size blocks takes.</param>
/// <returns>The read string.</returns>
public static unsafe string ReadFixedString(this Stream stream, int length)
{
// 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.ASCII.GetString(bytes.Slice(0, length));
}
/// <summary>
/// Reads a 0-terminated string which is stored in a fixed-size block of <paramref name="length"/> bytes.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to read with.</param>
/// <param name="length">The number of bytes the fixed-size blocks takes.</param>
/// <returns>The read string.</returns>
public static async Task<string> ReadFixedStringAsync(this Stream stream, int length)
{
// Ensure to not try to decode any bytes after the 0 termination.
byte[] bytes = new byte[length];
await stream.ReadAsync(bytes, 0, length);
for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ;
return Encoding.ASCII.GetString(bytes, 0, length);
}
/// <s
/// <summary>
/// Reads the unmanaged representation of a struct of type <typeparamref name="T"/>.
/// </summary>
@ -34,6 +67,34 @@ namespace Syroot.Worms.IO
public static void ReadStructs<T>(this Stream stream, Span<T> span) where T : unmanaged
=> stream.Read(MemoryMarshal.Cast<T, byte>(span));
/// <summary>
/// Writes the given string into a fixed-size block of <paramref name="length"/> bytes and 0-terminates it.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="value">The string to write.</param>
/// <param name="length">The number of bytes the fixed-size block takes.</param>
public static void WriteFixedString(this Stream stream, string value, int length)
{
Span<byte> bytes = stackalloc byte[length];
if (value != null)
Encoding.ASCII.GetBytes(value.AsSpan(), bytes);
stream.Write(bytes);
}
/// <summary>
/// Writes the given string into a fixed-size block of <paramref name="length"/> bytes and 0-terminates it.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="value">The string to write.</param>
/// <param name="length">The number of bytes the fixed-size block takes.</param>
public static async Task WriteFixedStringAsync(this Stream stream, string value, int length)
{
byte[] bytes = new byte[length];
if (value != null)
Encoding.ASCII.GetBytes(value.AsSpan(), bytes);
await stream.WriteAsync(bytes, 0, length);
}
/// <summary>
/// Writes the unmanaged representation of a struct of type <typeparamref name="T"/>.
/// </summary>

View File

@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Syroot.BinaryData;
using Syroot.Worms.IO;
namespace Syroot.Worms.Worms2.GameServer
{
internal class Packet
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
internal int Value10;
internal PacketCode Code;
internal PacketFlags Flags;
internal int Value4;
internal int Value0;
internal int Value3;
internal int Value1;
internal int Value2;
internal int Value7;
internal string String20 = String.Empty;
internal string String50 = String.Empty;
internal byte[] Data = Array.Empty<byte>();
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal async Task ReceiveAsync(Stream stream)
{
int dataLength = 0;
Code = (PacketCode)await stream.ReadInt32Async();
Flags = (PacketFlags)await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue0)) Value0 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue1)) Value1 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue2)) Value2 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue3)) Value3 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue4)) Value4 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasValue10)) Value10 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasDataLength)) dataLength = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasData) && dataLength != 0) Data = await stream.ReadBytesAsync(dataLength);
if (Flags.HasFlag(PacketFlags.HasValue7)) Value7 = await stream.ReadInt32Async();
if (Flags.HasFlag(PacketFlags.HasString20)) String20 = await stream.ReadFixedStringAsync(20);
if (Flags.HasFlag(PacketFlags.HasString50)) String50 = await stream.ReadFixedStringAsync(50);
}
internal async Task SendAsync(Stream stream)
{
await stream.WriteInt32Async((int)Code);
await stream.WriteInt32Async((int)Flags);
if (Flags.HasFlag(PacketFlags.HasValue0)) await stream.WriteInt32Async(Value0);
if (Flags.HasFlag(PacketFlags.HasValue1)) await stream.WriteInt32Async(Value1);
if (Flags.HasFlag(PacketFlags.HasValue2)) await stream.WriteInt32Async(Value2);
if (Flags.HasFlag(PacketFlags.HasValue3)) await stream.WriteInt32Async(Value3);
if (Flags.HasFlag(PacketFlags.HasValue4)) await stream.WriteInt32Async(Value4);
if (Flags.HasFlag(PacketFlags.HasValue10)) await stream.WriteInt32Async(Value10);
if (Flags.HasFlag(PacketFlags.HasDataLength)) await stream.WriteInt32Async(Data.Length);
if (Flags.HasFlag(PacketFlags.HasData) && Data.Length != 0) await stream.WriteBytesAsync(Data);
if (Flags.HasFlag(PacketFlags.HasValue7)) await stream.WriteInt32Async(Value7);
if (Flags.HasFlag(PacketFlags.HasString20)) await stream.WriteFixedStringAsync(String20, 20);
if (Flags.HasFlag(PacketFlags.HasString50)) await stream.WriteFixedStringAsync(String50, 50);
}
}
/// <summary>
/// Represents the description of the packet contents, as seen from client-side (thus a "reply" comes from the
/// server).
/// </summary>
internal enum PacketCode : int
{
Login = 600,
LoginReply = 601,
CreateRoom = 700,
CreateRoomReply = 701
}
/// <summary>
/// Represents which <see cref="Packet"/> fields are available, as specified at the start of each.
/// </summary>
[Flags]
internal enum PacketFlags
{
None,
HasValue0 = 1 << 0,
HasValue1 = 1 << 1,
HasValue2 = 1 << 2,
HasValue3 = 1 << 3,
HasValue4 = 1 << 4,
HasValue10 = 1 << 10,
HasDataLength = 1 << 5,
HasData = 1 << 6,
HasValue7 = 1 << 7,
HasString20 = 1 << 8,
HasString50 = 1 << 9
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Syroot.Worms.Worms2.GameServer
{
internal class Program
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly object _logLock = new object();
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static async Task Main(string[] args)
{
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Any, 17000);
TcpListener listener = new TcpListener(serverEndPoint);
listener.Start();
Console.WriteLine($"Listening under {serverEndPoint}...");
TcpClient? client;
while ((client = await listener.AcceptTcpClientAsync()) != null)
{
_ = HandleClient(client);
}
}
private static async Task HandleClient(TcpClient client)
{
IPEndPoint clientEndPoint = (IPEndPoint)client.Client.RemoteEndPoint;
Stream stream = client.GetStream();
Packet query = new Packet();
while (true)
{
// Receive query.
await query.ReceiveAsync(stream);
LogPacket(query, true, clientEndPoint);
// Send reply.
switch (query.Code)
{
case PacketCode.Login:
query.Code = PacketCode.LoginReply;
break;
case PacketCode.CreateRoom:
break;
}
LogPacket(query, false, clientEndPoint);
await query.SendAsync(stream);
}
}
private static void LogPacket(Packet packet, bool received, IPEndPoint endPoint)
{
static string arrayToString(byte[] array)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in array)
sb.Append($"{b:X2} ");
return sb.ToString();
}
lock (_logLock) // if I'd only get laid for lines like this
{
ConsoleColor prevColor = Console.ForegroundColor;
Console.ForegroundColor = received ? ConsoleColor.Cyan : ConsoleColor.Magenta;
Console.WriteLine($"{endPoint} {(received ? ">>" : "<<")} {packet.Code}");
if (packet.Flags.HasFlag(PacketFlags.HasValue0)) Console.WriteLine($" Value0 = {packet.Value0:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasValue1)) Console.WriteLine($" Value1 = {packet.Value1:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasValue2)) Console.WriteLine($" Value2 = {packet.Value2:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasValue3)) Console.WriteLine($" Value3 = {packet.Value3:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasValue4)) Console.WriteLine($" Value4 = {packet.Value4:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasValue10)) Console.WriteLine($" Value10 = {packet.Value10:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasDataLength)) Console.WriteLine($" DataLength = {packet.Data.Length}");
if (packet.Flags.HasFlag(PacketFlags.HasData)) Console.WriteLine($" Data = {arrayToString(packet.Data)}");
if (packet.Flags.HasFlag(PacketFlags.HasValue7)) Console.WriteLine($" Value7 = {packet.Value7:X8}");
if (packet.Flags.HasFlag(PacketFlags.HasString20)) Console.WriteLine($" String20 = {packet.String20}");
if (packet.Flags.HasFlag(PacketFlags.HasString50)) Console.WriteLine($" String50 = {packet.String50}");
Console.ForegroundColor = prevColor;
}
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\library\Syroot.Worms\Syroot.Worms.csproj" />
</ItemGroup>
</Project>