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.IO;
using System.Text;
using System.Threading.Tasks;
using Syroot.BinaryData;
using Syroot.Worms.IO;
@ -15,7 +14,7 @@ namespace Syroot.Worms.Worms2.GameServer
internal Packet(PacketCode code,
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)
{
Code = code;
@ -40,7 +39,7 @@ namespace Syroot.Worms.Worms2.GameServer
internal int? Value3 { get; set; }
internal int? Value4 { get; set; }
internal int? Value10 { get; set; }
internal byte[]? Data { get; set; }
internal string? Data { get; set; }
internal int? Error { get; set; }
internal string? Name { get; set; }
internal SessionInfo? Session { get; set; }
@ -53,24 +52,18 @@ namespace Syroot.Worms.Worms2.GameServer
StringBuilder sb = new StringBuilder();
sb.AppendLine($"{Code:D} {Code}");
if (Value0.HasValue) sb.AppendLine($"{nameof(Value0)} = {Value0:X8}");
if (Value1.HasValue) sb.AppendLine($"{nameof(Value1)} = {Value1:X8}");
if (Value2.HasValue) sb.AppendLine($"{nameof(Value2)} = {Value2:X8}");
if (Value3.HasValue) sb.AppendLine($"{nameof(Value3)} = {Value3:X8}");
if (Value4.HasValue) sb.AppendLine($"{nameof(Value4)} = {Value4:X8}");
if (Value10.HasValue) sb.AppendLine($"{nameof(Value10)} = {Value10:X8}");
if (Data != null)
{
sb.Append($"{nameof(Data)}[{Data.Length}] = ");
foreach (byte b in Data)
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}");
if (Value0.HasValue) sb.AppendLine($" {nameof(Value0),7}: {Value0:X8}");
if (Value1.HasValue) sb.AppendLine($" {nameof(Value1),7}: {Value1:X8}");
if (Value2.HasValue) sb.AppendLine($" {nameof(Value2),7}: {Value2:X8}");
if (Value3.HasValue) sb.AppendLine($" {nameof(Value3),7}: {Value3:X8}");
if (Value4.HasValue) sb.AppendLine($" {nameof(Value4),7}: {Value4:X8}");
if (Value10.HasValue) sb.AppendLine($" {nameof(Value10),7}: {Value10:X8}");
if (Data != null) sb.AppendLine($" {nameof(Data),7}: {Data}");
if (Error.HasValue) sb.AppendLine($" {nameof(Error),7}: {Error:X8}");
if (Name != null) sb.AppendLine($" {nameof(Name),7}: {Name}");
if (Session.HasValue) sb.AppendLine($" {nameof(Session),7}: {Session}");
return sb.ToString();
return sb.ToString().TrimEnd();
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
@ -87,30 +80,12 @@ namespace Syroot.Worms.Worms2.GameServer
if (flags.HasFlag(Flags.HasValue4)) Value4 = stream.ReadInt32();
if (flags.HasFlag(Flags.HasValue10)) Value10 = 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.HasUserName)) Name = stream.ReadFixedString(20);
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)
{
stream.WriteInt32((int)Code);
@ -124,33 +99,13 @@ namespace Syroot.Worms.Worms2.GameServer
if (Data != null)
{
stream.WriteInt32(Data.Length);
stream.WriteBytes(Data);
stream.WriteFixedString(Data, Data.Length);
}
if (Error.HasValue) stream.WriteInt32(Error.Value);
if (Name != null) stream.WriteFixedString(Name, 20);
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) --------------------------------------------------------------------------------------
private Flags GetFlags()

View File

@ -1,87 +1,64 @@
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
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
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly Stream _stream;
private readonly SemaphoreSlim _recvSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1);
private readonly object _recvLock = new object();
private readonly object _sendLock = new object();
// ---- 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();
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint;
_stream = client.GetStream();
RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the client's <see cref="IPEndPoint"/>.
/// </summary>
internal IPEndPoint RemoteEndPoint { get; }
// ---- 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()
{
_recvSemaphore.Wait();
try
lock (_recvLock)
{
Packet packet = new Packet();
packet.Receive(_stream);
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)
{
_sendSemaphore.Wait();
try
{
lock (_sendLock)
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
{
/// <summary>
/// Represents the main class of the application containing the program entry point.
/// </summary>
internal class Program
{
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Main()
{
Proxy.Start();
return;
Server server = new Server(new IPEndPoint(IPAddress.Any, 17000));
server.Run();
//Proxy.Run(); return;
Server server = new Server();
server.Run(new IPEndPoint(IPAddress.Any, 17000));
}
}
}

View File

@ -6,17 +6,20 @@ using Syroot.ColoredConsole;
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) -------------------------------------------------------------------------------------
internal static void Start()
internal static void Run()
{
// Start listening for clients to intercept.
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 17000);
TcpListener listener = new TcpListener(localEndPoint);
listener.Start();
ColorConsole.WriteLine($"Listening under {localEndPoint}...");
ColorConsole.WriteLine($"Proxy listening under {localEndPoint}...");
TcpClient? client;
while ((client = listener.AcceptTcpClient()) != null)
@ -27,12 +30,15 @@ namespace Syroot.Worms.Worms2.GameServer
PacketConnection clientConnection = new PacketConnection(client);
PacketConnection serverConnection = new PacketConnection(server);
ColorConsole.WriteLine(Color.Green, $"{clientConnection.RemoteEndPoint} connected.");
Task.Run(() => Forward(clientConnection, serverConnection, true));
Task.Run(() => Forward(serverConnection, clientConnection, false));
}
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void Forward(PacketConnection from, PacketConnection to, bool fromClient)
{
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.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Syroot.ColoredConsole;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a simplistic game server managing users, rooms, and games.
/// </summary>
internal class Server
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private int _idCounter;
private readonly TcpListener _listener;
private int _lastID;
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;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal Server(IPEndPoint localEndPoint)
{
_listener = new TcpListener(localEndPoint);
_packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
/// <summary>
/// Initializes a new instance of the <see cref="Server"/> class.
/// </summary>
internal Server() => _packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
{
[PacketCode.ListRooms] = OnListRooms,
[PacketCode.ListUsers] = OnListUsers,
@ -37,18 +43,19 @@ namespace Syroot.Worms.Worms2.GameServer
[PacketCode.ChatRoom] = OnChatRoom,
[PacketCode.ConnectGame] = OnConnectGame
};
}
// ---- 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();
Console.WriteLine($"Listening under {_listener.LocalEndpoint}...");
TcpClient? client;
while ((client = _listener.AcceptTcpClient()) != null)
Task.Run(() => HandleClient(client));
// Begin handling any queued jobs.
Task.Run(() => HandleJobs());
// Begin listening for new connections. Currently synchronous and blocking.
HandleConnections(localEndPoint);
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
@ -67,9 +74,40 @@ namespace Syroot.Worms.Worms2.GameServer
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)
{
PacketConnection connection = new PacketConnection(client);
ColorConsole.WriteLine(Color.Green, $"{connection.RemoteEndPoint} connected.");
try
{
while (true)
@ -78,99 +116,165 @@ namespace Syroot.Worms.Worms2.GameServer
Packet packet = connection.Receive();
LogPacket(connection, packet, true);
// Handle query.
// Queue handling of known queries.
if (_packetHandlers.TryGetValue(packet.Code, out Action<PacketConnection, Packet>? handler))
handler(connection, packet);
_jobs.Add(() => handler(connection, packet));
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)
{
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: 1234,
data: Encoding.ASCII.GetBytes("12.34.45.56"),
name: "SomeRoom",
session: new SessionInfo
foreach (Room room in _rooms)
{
Unknown0 = 0x17171717,
Unknown4 = 0x02010101,
Nation = Nation.CY,
GameVersion = 49,
GameRelease = 49,
Action0 = 0x01,
Action1 = 0x01,
Action2 = 0x01,
Action3 = 0x00
}));
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: room.ID,
data: String.Empty, // do not report creator IP
name: room.Name,
session: room.Session));
}
SendPacket(connection, new Packet(PacketCode.ListEnd));
}
private void OnListUsers(PacketConnection connection, Packet packet)
{
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: 12, // user ID,
data: Encoding.ASCII.GetBytes("12.34.45.67"),
name: "SomeUser",
session: new SessionInfo
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID)) // notably includes the user itself
{
Unknown0 = 0x17171717,
Unknown4 = 0x02010101,
GameVersion = 49,
GameRelease = 49,
Nation = Nation.IT,
Action0 = 01,
Action1 = 01,
Action2 = 01,
}));
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: user.ID,
name: user.Name,
session: user.Session));
}
SendPacket(connection, new Packet(PacketCode.ListEnd));
}
private void OnListGames(PacketConnection connection, Packet packet)
{
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: 12, // user ID,
data: Encoding.ASCII.GetBytes("12.34.45.67"),
name: "SomeUser",
session: new SessionInfo
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
foreach (Game game in _games.Where(x => x.RoomID == fromUser.RoomID))
{
Unknown0 = 0x17171717,
Unknown4 = 0x02010101,
GameVersion = 49,
GameRelease = 49,
Nation = Nation.IT,
Action0 = 01,
Action1 = 02,
Action2 = 01,
}));
SendPacket(connection, new Packet(PacketCode.ListItem,
value1: game.ID,
data: game.IPAddress.ToString(),
name: game.Name,
session: game.Session));
}
SendPacket(connection, new Packet(PacketCode.ListEnd));
}
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,
value1: Interlocked.Increment(ref _idCounter), // user ID
value1: newUser.ID,
error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.LoginReply,
error: 1));
}
}
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,
value1: Interlocked.Increment(ref _idCounter), // room ID
value1: newRoom.ID,
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)
{
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,
error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.JoinReply,
error: 1));
}
}
private void OnLeaveRoom(PacketConnection connection, Packet packet)
{
@ -178,6 +282,22 @@ namespace Syroot.Worms.Worms2.GameServer
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)
{
SendPacket(connection, new Packet(PacketCode.CreateGameReply,
@ -194,7 +314,7 @@ namespace Syroot.Worms.Worms2.GameServer
private void OnConnectGame(PacketConnection connection, Packet packet)
{
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 byte GameVersion;
internal byte GameRelease;
internal byte Action0;
internal byte Action1;
internal byte Action2;
internal byte Action3;
internal SessionType Type;
internal SessionAccess Access;
internal byte Unknown13;
internal byte Unknown14;
internal byte Unused15;
internal ulong Unused16;
internal ulong Unused24;
@ -23,11 +24,46 @@ namespace Syroot.Worms.Worms2.GameServer
internal ulong Unused40;
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) ---------------------------------------------------------------------------------------
/// <inheritdoc/>
public override string ToString() => $"{Unknown0:X8}-{Unknown4:X8} {Nation} {GameVersion}/{GameRelease} "
+ $"{Action0:X2}-{Action1:X2}-{Action2:X2}-{Action3:X2} "
+ $"{Unused15:X2}{Unused16:X16}{Unused24:X16}{Unused24:X16}{Unused40:X16}{Unused48:X4}";
+ $"{Type}/{Access}/{Unknown13:X2}/{Unknown14:X2} "
+ $"({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; }
}
}