mirror of
https://gitlab.com/Syroot/Worms.git
synced 2025-01-13 07:18:00 +03:00
Implement real packet handling.
This commit is contained in:
parent
ac2d12c1b6
commit
9aad0faf6b
57
src/tool/Syroot.Worms.Worms2.GameServer/Game.cs
Normal file
57
src/tool/Syroot.Worms.Worms2.GameServer/Game.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
40
src/tool/Syroot.Worms.Worms2.GameServer/Room.cs
Normal file
40
src/tool/Syroot.Worms.Worms2.GameServer/Room.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -1,54 +1,61 @@
|
||||
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)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Server"/> class.
|
||||
/// </summary>
|
||||
internal Server() => _packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
|
||||
{
|
||||
_listener = new TcpListener(localEndPoint);
|
||||
_packetHandlers = new Dictionary<PacketCode, Action<PacketConnection, Packet>>
|
||||
{
|
||||
[PacketCode.ListRooms] = OnListRooms,
|
||||
[PacketCode.ListUsers] = OnListUsers,
|
||||
[PacketCode.ListGames] = OnListGames,
|
||||
[PacketCode.Login] = OnLogin,
|
||||
[PacketCode.CreateRoom] = OnCreateRoom,
|
||||
[PacketCode.Join] = OnJoinRoom,
|
||||
[PacketCode.LeaveRoom] = OnLeaveRoom,
|
||||
[PacketCode.CreateGame] = OnCreateGame,
|
||||
[PacketCode.ChatRoom] = OnChatRoom,
|
||||
[PacketCode.ConnectGame] = OnConnectGame
|
||||
};
|
||||
}
|
||||
[PacketCode.ListRooms] = OnListRooms,
|
||||
[PacketCode.ListUsers] = OnListUsers,
|
||||
[PacketCode.ListGames] = OnListGames,
|
||||
[PacketCode.Login] = OnLogin,
|
||||
[PacketCode.CreateRoom] = OnCreateRoom,
|
||||
[PacketCode.Join] = OnJoinRoom,
|
||||
[PacketCode.LeaveRoom] = OnLeaveRoom,
|
||||
[PacketCode.CreateGame] = OnCreateGame,
|
||||
[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,98 +116,164 @@ 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
|
||||
{
|
||||
Unknown0 = 0x17171717,
|
||||
Unknown4 = 0x02010101,
|
||||
Nation = Nation.CY,
|
||||
GameVersion = 49,
|
||||
GameRelease = 49,
|
||||
Action0 = 0x01,
|
||||
Action1 = 0x01,
|
||||
Action2 = 0x01,
|
||||
Action3 = 0x00
|
||||
}));
|
||||
|
||||
foreach (Room room in _rooms)
|
||||
{
|
||||
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
|
||||
{
|
||||
Unknown0 = 0x17171717,
|
||||
Unknown4 = 0x02010101,
|
||||
GameVersion = 49,
|
||||
GameRelease = 49,
|
||||
Nation = Nation.IT,
|
||||
Action0 = 01,
|
||||
Action1 = 01,
|
||||
Action2 = 01,
|
||||
}));
|
||||
User? fromUser = GetUser(connection);
|
||||
if (fromUser == null)
|
||||
return;
|
||||
|
||||
foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID)) // notably includes the user itself
|
||||
{
|
||||
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
|
||||
{
|
||||
Unknown0 = 0x17171717,
|
||||
Unknown4 = 0x02010101,
|
||||
GameVersion = 49,
|
||||
GameRelease = 49,
|
||||
Nation = Nation.IT,
|
||||
Action0 = 01,
|
||||
Action1 = 02,
|
||||
Action2 = 01,
|
||||
}));
|
||||
User? fromUser = GetUser(connection);
|
||||
if (fromUser == null)
|
||||
return;
|
||||
|
||||
foreach (Game game in _games.Where(x => x.RoomID == fromUser.RoomID))
|
||||
{
|
||||
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)
|
||||
{
|
||||
SendPacket(connection, new Packet(PacketCode.LoginReply,
|
||||
value1: Interlocked.Increment(ref _idCounter), // user ID
|
||||
error: 0));
|
||||
// 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: newUser.ID,
|
||||
error: 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
SendPacket(connection, new Packet(PacketCode.LoginReply,
|
||||
error: 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreateRoom(PacketConnection connection, Packet packet)
|
||||
{
|
||||
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
|
||||
value1: Interlocked.Increment(ref _idCounter), // room ID
|
||||
error: 0));
|
||||
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: 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)
|
||||
{
|
||||
SendPacket(connection, new Packet(PacketCode.JoinReply,
|
||||
error: 0));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
52
src/tool/Syroot.Worms.Worms2.GameServer/User.cs
Normal file
52
src/tool/Syroot.Worms.Worms2.GameServer/User.cs
Normal 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; }
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user