Implement real packet handling.

This commit is contained in:
Ray Koopa 2020-07-10 02:15:06 +02:00
parent ac2d12c1b6
commit 9aad0faf6b
9 changed files with 455 additions and 211 deletions

View File

@ -0,0 +1,57 @@
using System.Net;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a game hosted in a room which users can join.
/// </summary>
internal class Game
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="Game"/> class with the given identification.
/// </summary>
/// <param name="id">The unique numerical identifier of the game.</param>
/// <param name="name">The name of the room being the name of the creator.</param>
/// <param name="nation">The flag displayed with the game.</param>
/// <param name="roomID">The ID of the room the game is hosted in.</param>
/// <param name="ipAddress">The IP address of the host of the game.</param>
/// <param name="access">The access modifier of the game.</param>
internal Game(int id, string name, Nation nation, int roomID, IPAddress ipAddress, SessionAccess access)
{
ID = id;
Name = name;
RoomID = roomID;
IPAddress = ipAddress;
Session = new SessionInfo(nation, SessionType.Game, access);
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the unique numerical identifier of the game.
/// </summary>
internal int ID { get; }
/// <summary>
/// Gets the name of the room being the name of the creator.
/// </summary>
internal string Name { get; set; }
/// <summary>
/// Gets the ID of the room the game is hosted in.
/// </summary>
internal int RoomID { get; }
/// <summary>
/// Gets the IP address of the host of the game.
/// </summary>
internal IPAddress IPAddress { get; set; }
/// <summary>
/// Gets the <see cref="SessionInfo"/> describing the game.
/// </summary>
internal SessionInfo Session { get; }
}
}

View File

@ -1,7 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Syroot.BinaryData; using Syroot.BinaryData;
using Syroot.Worms.IO; using Syroot.Worms.IO;
@ -15,7 +14,7 @@ namespace Syroot.Worms.Worms2.GameServer
internal Packet(PacketCode code, internal Packet(PacketCode code,
int? value0 = null, int? value1 = null, int? value2 = null, int? value3 = null, int? value4 = null, int? value0 = null, int? value1 = null, int? value2 = null, int? value3 = null, int? value4 = null,
int? value10 = null, byte[]? data = null, int? error = null, int? value10 = null, string? data = null, int? error = null,
string? name = null, SessionInfo? session = null) string? name = null, SessionInfo? session = null)
{ {
Code = code; Code = code;
@ -40,7 +39,7 @@ namespace Syroot.Worms.Worms2.GameServer
internal int? Value3 { get; set; } internal int? Value3 { get; set; }
internal int? Value4 { get; set; } internal int? Value4 { get; set; }
internal int? Value10 { get; set; } internal int? Value10 { get; set; }
internal byte[]? Data { get; set; } internal string? Data { get; set; }
internal int? Error { get; set; } internal int? Error { get; set; }
internal string? Name { get; set; } internal string? Name { get; set; }
internal SessionInfo? Session { get; set; } internal SessionInfo? Session { get; set; }
@ -53,24 +52,18 @@ namespace Syroot.Worms.Worms2.GameServer
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)} = {Value0:X8}"); if (Value0.HasValue) sb.AppendLine($" {nameof(Value0),7}: {Value0:X8}");
if (Value1.HasValue) sb.AppendLine($"{nameof(Value1)} = {Value1:X8}"); if (Value1.HasValue) sb.AppendLine($" {nameof(Value1),7}: {Value1:X8}");
if (Value2.HasValue) sb.AppendLine($"{nameof(Value2)} = {Value2:X8}"); if (Value2.HasValue) sb.AppendLine($" {nameof(Value2),7}: {Value2:X8}");
if (Value3.HasValue) sb.AppendLine($"{nameof(Value3)} = {Value3:X8}"); if (Value3.HasValue) sb.AppendLine($" {nameof(Value3),7}: {Value3:X8}");
if (Value4.HasValue) sb.AppendLine($"{nameof(Value4)} = {Value4:X8}"); if (Value4.HasValue) sb.AppendLine($" {nameof(Value4),7}: {Value4:X8}");
if (Value10.HasValue) sb.AppendLine($"{nameof(Value10)} = {Value10:X8}"); if (Value10.HasValue) sb.AppendLine($" {nameof(Value10),7}: {Value10:X8}");
if (Data != null) if (Data != null) sb.AppendLine($" {nameof(Data),7}: {Data}");
{ if (Error.HasValue) sb.AppendLine($" {nameof(Error),7}: {Error:X8}");
sb.Append($"{nameof(Data)}[{Data.Length}] = "); if (Name != null) sb.AppendLine($" {nameof(Name),7}: {Name}");
foreach (byte b in Data) if (Session.HasValue) sb.AppendLine($" {nameof(Session),7}: {Session}");
sb.Append($"{b:X2} ");
sb.AppendLine();
}
if (Error.HasValue) sb.AppendLine($"{nameof(Error)} = {Error:X8}");
if (Name != null) sb.AppendLine($"{nameof(Name)} = {Name}");
if (Session.HasValue) sb.AppendLine($"{nameof(Session)} = {Session}");
return sb.ToString(); return sb.ToString().TrimEnd();
} }
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
@ -87,30 +80,12 @@ namespace Syroot.Worms.Worms2.GameServer
if (flags.HasFlag(Flags.HasValue4)) Value4 = stream.ReadInt32(); if (flags.HasFlag(Flags.HasValue4)) Value4 = stream.ReadInt32();
if (flags.HasFlag(Flags.HasValue10)) Value10 = stream.ReadInt32(); if (flags.HasFlag(Flags.HasValue10)) Value10 = stream.ReadInt32();
if (flags.HasFlag(Flags.HasDataLength)) dataLength = stream.ReadInt32(); if (flags.HasFlag(Flags.HasDataLength)) dataLength = stream.ReadInt32();
if (flags.HasFlag(Flags.HasData) && dataLength != 0) Data = stream.ReadBytes(dataLength); if (flags.HasFlag(Flags.HasData) && dataLength != 0) Data = stream.ReadFixedString(dataLength);
if (flags.HasFlag(Flags.HasError)) Error = stream.ReadInt32(); if (flags.HasFlag(Flags.HasError)) Error = stream.ReadInt32();
if (flags.HasFlag(Flags.HasUserName)) Name = stream.ReadFixedString(20); if (flags.HasFlag(Flags.HasUserName)) Name = stream.ReadFixedString(20);
if (flags.HasFlag(Flags.HasUserInfo)) Session = stream.ReadStruct<SessionInfo>(); if (flags.HasFlag(Flags.HasUserInfo)) Session = stream.ReadStruct<SessionInfo>();
} }
internal async Task ReceiveAsync(Stream stream)
{
int dataLength = 0;
Code = (PacketCode)await stream.ReadInt32Async();
Flags flags = (Flags)await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue0)) Value0 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue1)) Value1 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue2)) Value2 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue3)) Value3 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue4)) Value4 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasValue10)) Value10 = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasDataLength)) dataLength = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasData) && dataLength != 0) Data = await stream.ReadBytesAsync(dataLength);
if (flags.HasFlag(Flags.HasError)) Error = await stream.ReadInt32Async();
if (flags.HasFlag(Flags.HasUserName)) Name = await stream.ReadFixedStringAsync(20);
if (flags.HasFlag(Flags.HasUserInfo)) Session = stream.ReadStruct<SessionInfo>();
}
internal void Send(Stream stream) internal void Send(Stream stream)
{ {
stream.WriteInt32((int)Code); stream.WriteInt32((int)Code);
@ -124,33 +99,13 @@ namespace Syroot.Worms.Worms2.GameServer
if (Data != null) if (Data != null)
{ {
stream.WriteInt32(Data.Length); stream.WriteInt32(Data.Length);
stream.WriteBytes(Data); stream.WriteFixedString(Data, Data.Length);
} }
if (Error.HasValue) stream.WriteInt32(Error.Value); if (Error.HasValue) stream.WriteInt32(Error.Value);
if (Name != null) stream.WriteFixedString(Name, 20); if (Name != null) stream.WriteFixedString(Name, 20);
if (Session.HasValue) stream.WriteStruct(Session.Value); if (Session.HasValue) stream.WriteStruct(Session.Value);
} }
internal async Task SendAsync(Stream stream)
{
await stream.WriteInt32Async((int)Code);
await stream.WriteInt32Async((int)GetFlags());
if (Value0.HasValue) await stream.WriteInt32Async(Value0.Value);
if (Value1.HasValue) await stream.WriteInt32Async(Value1.Value);
if (Value2.HasValue) await stream.WriteInt32Async(Value2.Value);
if (Value3.HasValue) await stream.WriteInt32Async(Value3.Value);
if (Value4.HasValue) await stream.WriteInt32Async(Value4.Value);
if (Value10.HasValue) await stream.WriteInt32Async(Value10.Value);
if (Data != null)
{
await stream.WriteInt32Async(Data.Length);
await stream.WriteBytesAsync(Data);
}
if (Error.HasValue) await stream.WriteInt32Async(Error.Value);
if (Name != null) await stream.WriteFixedStringAsync(Name, 20);
if (Session.HasValue) stream.WriteStruct(Session.Value);
}
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private Flags GetFlags() private Flags GetFlags()

View File

@ -1,87 +1,64 @@
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Syroot.Worms.Worms2.GameServer 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 class PacketConnection
{ {
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly Stream _stream; private readonly Stream _stream;
private readonly SemaphoreSlim _recvSemaphore = new SemaphoreSlim(1, 1); private readonly object _recvLock = new object();
private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1); private readonly object _sendLock = new object();
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal PacketConnection(TcpClient tcpClient) /// <summary>
/// Initializes a new instance of the <see cref="PacketConnection"/> class communicating with the given
/// <paramref name="client"/>.
/// </summary>
/// <param name="client">The <see cref="TcpClient"/> to communicate with.</param>
internal PacketConnection(TcpClient client)
{ {
_stream = tcpClient.GetStream(); _stream = client.GetStream();
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint; RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint;
} }
// ---- PROPERTIES --------------------------------------------------------------------------------------------- // ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the client's <see cref="IPEndPoint"/>.
/// </summary>
internal IPEndPoint RemoteEndPoint { get; } internal IPEndPoint RemoteEndPoint { get; }
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Blocks until a <see cref="Packet"/> was received, and returns it.
/// </summary>
/// <returns>The received <see cref="Packet"/>.</returns>
internal Packet Receive() internal Packet Receive()
{ {
_recvSemaphore.Wait(); lock (_recvLock)
try
{ {
Packet packet = new Packet(); Packet packet = new Packet();
packet.Receive(_stream); packet.Receive(_stream);
return packet; return packet;
} }
finally
{
_recvSemaphore.Release();
}
}
internal async Task<Packet> ReceiveAsync()
{
await _recvSemaphore.WaitAsync();
try
{
Packet packet = new Packet();
await packet.ReceiveAsync(_stream);
return packet;
}
finally
{
_recvSemaphore.Release();
}
} }
/// <summary>
/// Blocks until the given <paramref name="packet"/> was sent.
/// </summary>
/// <param name="packet">The <see cref="Packet"/> to send.</param>
internal void Send(Packet packet) internal void Send(Packet packet)
{ {
_sendSemaphore.Wait(); lock (_sendLock)
try
{
packet.Send(_stream); packet.Send(_stream);
} }
finally
{
_sendSemaphore.Release();
}
}
internal async Task SendAsync(Packet packet)
{
await _sendSemaphore.WaitAsync();
try
{
await packet.SendAsync(_stream);
}
finally
{
_sendSemaphore.Release();
}
}
} }
} }

View File

@ -2,17 +2,18 @@
namespace Syroot.Worms.Worms2.GameServer namespace Syroot.Worms.Worms2.GameServer
{ {
/// <summary>
/// Represents the main class of the application containing the program entry point.
/// </summary>
internal class Program internal class Program
{ {
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Main() private static void Main()
{ {
Proxy.Start(); //Proxy.Run(); return;
return; Server server = new Server();
server.Run(new IPEndPoint(IPAddress.Any, 17000));
Server server = new Server(new IPEndPoint(IPAddress.Any, 17000));
server.Run();
} }
} }
} }

View File

@ -6,17 +6,20 @@ using Syroot.ColoredConsole;
namespace Syroot.Worms.Worms2.GameServer namespace Syroot.Worms.Worms2.GameServer
{ {
internal class Proxy /// <summary>
/// Represents a proxy dumping Worms 2 network traffic to console for debug purposes.
/// </summary>
internal static class Proxy
{ {
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal static void Start() internal static void Run()
{ {
// Start listening for clients to intercept. // Start listening for clients to intercept.
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 17000); IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 17000);
TcpListener listener = new TcpListener(localEndPoint); TcpListener listener = new TcpListener(localEndPoint);
listener.Start(); listener.Start();
ColorConsole.WriteLine($"Listening under {localEndPoint}..."); ColorConsole.WriteLine($"Proxy listening under {localEndPoint}...");
TcpClient? client; TcpClient? client;
while ((client = listener.AcceptTcpClient()) != null) while ((client = listener.AcceptTcpClient()) != null)
@ -27,12 +30,15 @@ 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.");
Task.Run(() => Forward(clientConnection, serverConnection, true)); Task.Run(() => Forward(clientConnection, serverConnection, true));
Task.Run(() => Forward(serverConnection, clientConnection, false)); Task.Run(() => Forward(serverConnection, clientConnection, false));
} }
} }
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Forward(PacketConnection from, PacketConnection to, bool fromClient) private static void Forward(PacketConnection from, PacketConnection to, bool fromClient)
{ {
while (true) while (true)

View File

@ -0,0 +1,40 @@
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a room in which users can meet, chat, and host games.
/// </summary>
internal class Room
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="Room"/> class with the given identificatoin.
/// </summary>
/// <param name="id">The unique numerical identifier of the room.</param>
/// <param name="name">The name of the room as given by the creator.</param>
/// <param name="nation">The flag displayed with the room.</param>
internal Room(int id, string name, Nation nation)
{
ID = id;
Name = name;
Session = new SessionInfo(nation, SessionType.Room);
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the unique numerical identifier of the room.
/// </summary>
internal int ID { get; }
/// <summary>
/// Gets the name of the room as given by the creator.
/// </summary>
internal string Name { get; set; }
/// <summary>
/// Gets the <see cref="SessionInfo"/> describing the room.
/// </summary>
internal SessionInfo Session { get; }
}
}

View File

@ -1,30 +1,36 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.IO; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Syroot.ColoredConsole; using Syroot.ColoredConsole;
namespace Syroot.Worms.Worms2.GameServer namespace Syroot.Worms.Worms2.GameServer
{ {
/// <summary>
/// Represents a simplistic game server managing users, rooms, and games.
/// </summary>
internal class Server internal class Server
{ {
// ---- FIELDS ------------------------------------------------------------------------------------------------- // ---- FIELDS -------------------------------------------------------------------------------------------------
private int _idCounter; private int _lastID;
private readonly TcpListener _listener; 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 Dictionary<PacketCode, Action<PacketConnection, Packet>> _packetHandlers;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal Server(IPEndPoint localEndPoint) /// <summary>
{ /// Initializes a new instance of the <see cref="Server"/> class.
_listener = new TcpListener(localEndPoint); /// </summary>
_packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>> internal Server() => _packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
{ {
[PacketCode.ListRooms] = OnListRooms, [PacketCode.ListRooms] = OnListRooms,
[PacketCode.ListUsers] = OnListUsers, [PacketCode.ListUsers] = OnListUsers,
@ -37,18 +43,19 @@ namespace Syroot.Worms.Worms2.GameServer
[PacketCode.ChatRoom] = OnChatRoom, [PacketCode.ChatRoom] = OnChatRoom,
[PacketCode.ConnectGame] = OnConnectGame [PacketCode.ConnectGame] = OnConnectGame
}; };
}
// ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- // ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal void Run() /// <summary>
/// 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)
{ {
_listener.Start(); // Begin handling any queued jobs.
Console.WriteLine($"Listening under {_listener.LocalEndpoint}..."); Task.Run(() => HandleJobs());
// Begin listening for new connections. Currently synchronous and blocking.
TcpClient? client; HandleConnections(localEndPoint);
while ((client = _listener.AcceptTcpClient()) != null)
Task.Run(() => HandleClient(client));
} }
// ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
@ -67,9 +74,40 @@ namespace Syroot.Worms.Worms2.GameServer
ColorConsole.WriteLine(Color.Magenta, $"{connection.RemoteEndPoint} << {packet}"); ColorConsole.WriteLine(Color.Magenta, $"{connection.RemoteEndPoint} << {packet}");
} }
private int GetNextID() => Interlocked.Increment(ref _lastID);
private User? GetUser(PacketConnection connection)
{
foreach (User user in _users)
if (user.Connection == connection)
return user;
return null;
}
private void HandleJobs()
{
foreach (Action job in _jobs.GetConsumingEnumerable())
job();
}
private void HandleConnections(IPEndPoint localEndPoint)
{
// Start a new listener for new incoming connections.
TcpListener listener = new TcpListener(localEndPoint);
listener.Start();
Console.WriteLine($"Server listening under {listener.LocalEndpoint}...");
// Dispatch each connection into its own thread.
TcpClient? client;
while ((client = listener.AcceptTcpClient()) != null)
Task.Run(() => HandleClient(client));
}
private void HandleClient(TcpClient client) private void HandleClient(TcpClient client)
{ {
PacketConnection connection = new PacketConnection(client); PacketConnection connection = new PacketConnection(client);
ColorConsole.WriteLine(Color.Green, $"{connection.RemoteEndPoint} connected.");
try try
{ {
while (true) while (true)
@ -78,99 +116,165 @@ namespace Syroot.Worms.Worms2.GameServer
Packet packet = connection.Receive(); Packet packet = connection.Receive();
LogPacket(connection, packet, true); LogPacket(connection, packet, true);
// Handle query. // Queue handling of known queries.
if (_packetHandlers.TryGetValue(packet.Code, out Action<PacketConnection, Packet>? handler)) if (_packetHandlers.TryGetValue(packet.Code, out Action<PacketConnection, Packet>? handler))
handler(connection, packet); _jobs.Add(() => handler(connection, packet));
else else
ColorConsole.WriteLine(Color.Red, $"Unhandled packet {packet.Code}."); ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}.");
} }
} }
catch (EndOfStreamException) { } catch (Exception ex)
{
ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}");
_jobs.Add(() => OnDisconnectUser(connection));
}
} }
// ---- Packet Handlers ---- // ---- Handlers ----
private void OnListRooms(PacketConnection connection, Packet packet) private void OnListRooms(PacketConnection connection, Packet packet)
{ {
SendPacket(connection, new Packet(PacketCode.ListItem, foreach (Room room in _rooms)
value1: 1234,
data: Encoding.ASCII.GetBytes("12.34.45.56"),
name: "SomeRoom",
session: new SessionInfo
{ {
Unknown0 = 0x17171717, SendPacket(connection, new Packet(PacketCode.ListItem,
Unknown4 = 0x02010101, value1: room.ID,
Nation = Nation.CY, data: String.Empty, // do not report creator IP
GameVersion = 49, name: room.Name,
GameRelease = 49, session: room.Session));
Action0 = 0x01, }
Action1 = 0x01,
Action2 = 0x01,
Action3 = 0x00
}));
SendPacket(connection, new Packet(PacketCode.ListEnd)); SendPacket(connection, new Packet(PacketCode.ListEnd));
} }
private void OnListUsers(PacketConnection connection, Packet packet) private void OnListUsers(PacketConnection connection, Packet packet)
{ {
SendPacket(connection, new Packet(PacketCode.ListItem, User? fromUser = GetUser(connection);
value1: 12, // user ID, if (fromUser == null)
data: Encoding.ASCII.GetBytes("12.34.45.67"), return;
name: "SomeUser",
session: new SessionInfo foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID)) // notably includes the user itself
{ {
Unknown0 = 0x17171717, SendPacket(connection, new Packet(PacketCode.ListItem,
Unknown4 = 0x02010101, value1: user.ID,
GameVersion = 49, name: user.Name,
GameRelease = 49, session: user.Session));
Nation = Nation.IT, }
Action0 = 01,
Action1 = 01,
Action2 = 01,
}));
SendPacket(connection, new Packet(PacketCode.ListEnd)); SendPacket(connection, new Packet(PacketCode.ListEnd));
} }
private void OnListGames(PacketConnection connection, Packet packet) private void OnListGames(PacketConnection connection, Packet packet)
{ {
SendPacket(connection, new Packet(PacketCode.ListItem, User? fromUser = GetUser(connection);
value1: 12, // user ID, if (fromUser == null)
data: Encoding.ASCII.GetBytes("12.34.45.67"), return;
name: "SomeUser",
session: new SessionInfo foreach (Game game in _games.Where(x => x.RoomID == fromUser.RoomID))
{ {
Unknown0 = 0x17171717, SendPacket(connection, new Packet(PacketCode.ListItem,
Unknown4 = 0x02010101, value1: game.ID,
GameVersion = 49, data: game.IPAddress.ToString(),
GameRelease = 49, name: game.Name,
Nation = Nation.IT, session: game.Session));
Action0 = 01, }
Action1 = 02,
Action2 = 01,
}));
SendPacket(connection, new Packet(PacketCode.ListEnd)); SendPacket(connection, new Packet(PacketCode.ListEnd));
} }
private void OnLogin(PacketConnection connection, Packet packet) private void OnLogin(PacketConnection connection, Packet packet)
{ {
// Check if user name is valid and not already taken.
if (!String.IsNullOrWhiteSpace(packet.Name)
&& packet.Session.HasValue
&& !_users.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
User newUser = new User(connection, GetNextID(), packet.Name, packet.Session.Value.Nation);
// 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));
}
// Register new user and send reply to him.
_users.Add(newUser);
SendPacket(connection, new Packet(PacketCode.LoginReply, SendPacket(connection, new Packet(PacketCode.LoginReply,
value1: Interlocked.Increment(ref _idCounter), // user ID value1: newUser.ID,
error: 0)); error: 0));
} }
else
{
SendPacket(connection, new Packet(PacketCode.LoginReply,
error: 1));
}
}
private void OnCreateRoom(PacketConnection connection, Packet packet) private void OnCreateRoom(PacketConnection connection, Packet packet)
{ {
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
// Check if room name is valid is not already taken.
if (!String.IsNullOrWhiteSpace(packet.Name)
&& packet.Session.HasValue
&& !_rooms.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
Room newRoom = new Room(GetNextID(), packet.Name, packet.Session.Value.Nation);
_rooms.Add(newRoom);
// Send reply to creator.
SendPacket(connection, new Packet(PacketCode.CreateRoomReply, SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
value1: Interlocked.Increment(ref _idCounter), // room ID value1: newRoom.ID,
error: 0)); error: 0));
// Notify other users about new room.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.CreateRoom,
value1: newRoom.ID,
data: String.Empty, // do not report creator IP
name: newRoom.Name,
session: newRoom.Session));
}
}
else
{
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
error: 1));
}
} }
private void OnJoinRoom(PacketConnection connection, Packet packet) private void OnJoinRoom(PacketConnection connection, Packet packet)
{ {
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
// Check if room ID is valid.
if (packet.Value2.HasValue && _rooms.Any(x => x.ID == packet.Value2))
{
fromUser.RoomID = packet.Value2.Value;
// Notify other users about the join.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.Join,
value1: fromUser.RoomID,
value10: fromUser.ID));
}
// Send reply to joiner.
SendPacket(connection, new Packet(PacketCode.JoinReply, SendPacket(connection, new Packet(PacketCode.JoinReply,
error: 0)); error: 0));
} }
else
{
SendPacket(connection, new Packet(PacketCode.JoinReply,
error: 1));
}
}
private void OnLeaveRoom(PacketConnection connection, Packet packet) private void OnLeaveRoom(PacketConnection connection, Packet packet)
{ {
@ -178,6 +282,22 @@ namespace Syroot.Worms.Worms2.GameServer
error: 0)); error: 0));
} }
private void OnDisconnectUser(PacketConnection connection)
{
User? disconnectedUser = GetUser(connection);
if (disconnectedUser == null)
return;
_users.Remove(disconnectedUser);
// Notify other users.
foreach (User user in _users)
{
SendPacket(user.Connection, new Packet(PacketCode.DisconnectUser,
value10: disconnectedUser.ID));
}
}
private void OnCreateGame(PacketConnection connection, Packet packet) private void OnCreateGame(PacketConnection connection, Packet packet)
{ {
SendPacket(connection, new Packet(PacketCode.CreateGameReply, SendPacket(connection, new Packet(PacketCode.CreateGameReply,
@ -194,7 +314,7 @@ namespace Syroot.Worms.Worms2.GameServer
private void OnConnectGame(PacketConnection connection, Packet packet) private void OnConnectGame(PacketConnection connection, Packet packet)
{ {
SendPacket(connection, new Packet(PacketCode.ConnectGameReply, SendPacket(connection, new Packet(PacketCode.ConnectGameReply,
data: Encoding.ASCII.GetBytes("12.34.45.56"))); data: "12.34.45.56"));
} }
} }
} }

View File

@ -12,10 +12,11 @@ namespace Syroot.Worms.Worms2.GameServer
internal Nation Nation; internal Nation Nation;
internal byte GameVersion; internal byte GameVersion;
internal byte GameRelease; internal byte GameRelease;
internal byte Action0; internal SessionType Type;
internal byte Action1; internal SessionAccess Access;
internal byte Action2; internal byte Unknown13;
internal byte Action3; internal byte Unknown14;
internal byte Unused15; internal byte Unused15;
internal ulong Unused16; internal ulong Unused16;
internal ulong Unused24; internal ulong Unused24;
@ -23,11 +24,46 @@ namespace Syroot.Worms.Worms2.GameServer
internal ulong Unused40; internal ulong Unused40;
internal ushort Unused48; internal ushort Unused48;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal SessionInfo(Nation nation, SessionType type, SessionAccess access = SessionAccess.Public)
{
Unknown0 = 0x17171717;
Unknown4 = 0x02010101;
Nation = nation;
GameVersion = 49;
GameRelease = 49;
Type = type;
Access = access;
Unknown13 = 1;
Unknown14 = 0;
Unused15 = default;
Unused16 = default;
Unused24 = default;
Unused32 = default;
Unused40 = default;
Unused48 = default;
}
// ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- // ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() => $"{Unknown0:X8}-{Unknown4:X8} {Nation} {GameVersion}/{GameRelease} " public override string ToString() => $"{Unknown0:X8}-{Unknown4:X8} {Nation} {GameVersion}/{GameRelease} "
+ $"{Action0:X2}-{Action1:X2}-{Action2:X2}-{Action3:X2} " + $"{Type}/{Access}/{Unknown13:X2}/{Unknown14:X2} "
+ $"{Unused15:X2}{Unused16:X16}{Unused24:X16}{Unused24:X16}{Unused40:X16}{Unused48:X4}"; + $"({Unused15}-{Unused16}-{Unused24}-{Unused24}-{Unused40}-{Unused48})";
}
internal enum SessionType : byte
{
Room = 1,
Game = 4,
User = 5
}
internal enum SessionAccess : byte
{
Public = 1,
Protected = 2
} }
} }

View File

@ -0,0 +1,52 @@
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents information on a client connected to the server.
/// </summary>
internal class User
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="User"/> class with the given identification.
/// </summary>
/// <param name="connection">The <see cref="PacketCode"/> to communicate through.</param>
/// <param name="id">The unique numerical identifier of the user.</param>
/// <param name="name">The textual login name displayed to others.</param>
/// <param name="nation">The flag displayed to others.</param>
internal User(PacketConnection connection, int id, string name, Nation nation)
{
Connection = connection;
ID = id;
Name = name;
Session = new SessionInfo(nation, SessionType.User);
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the <see cref="PacketConnection"/> to communicate through.
/// </summary>
internal PacketConnection Connection { get; }
/// <summary>
/// Gets the unique numerical identifier of the user.
/// </summary>
internal int ID { get; }
/// <summary>
/// Gets the textual login name displayed to others.
/// </summary>
internal string Name { get; }
/// <summary>
/// Gets the <see cref="SessionInfo"/> describing the user.
/// </summary>
internal SessionInfo Session { get; }
/// <summary>
/// Gets or sets the ID of the <see cref="Room"/> the user is in, or 0 for no room.
/// </summary>
internal int RoomID { get; set; }
}
}