diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Game.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Game.cs new file mode 100644 index 0000000..7c221e3 --- /dev/null +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Game.cs @@ -0,0 +1,57 @@ +using System.Net; + +namespace Syroot.Worms.Worms2.GameServer +{ + /// + /// Represents a game hosted in a room which users can join. + /// + internal class Game + { + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + + /// + /// Initializes a new instance of the class with the given identification. + /// + /// The unique numerical identifier of the game. + /// The name of the room being the name of the creator. + /// The flag displayed with the game. + /// The ID of the room the game is hosted in. + /// The IP address of the host of the game. + /// The access modifier of the game. + 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 --------------------------------------------------------------------------------------------- + + /// + /// Gets the unique numerical identifier of the game. + /// + internal int ID { get; } + + /// + /// Gets the name of the room being the name of the creator. + /// + internal string Name { get; set; } + + /// + /// Gets the ID of the room the game is hosted in. + /// + internal int RoomID { get; } + + /// + /// Gets the IP address of the host of the game. + /// + internal IPAddress IPAddress { get; set; } + + /// + /// Gets the describing the game. + /// + internal SessionInfo Session { get; } + } +} diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs index 4b601ca..8bc06f6 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Packet.cs @@ -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(); } - 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(); - } - 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() diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs b/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs index 08ad9de..1c2dd5a 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/PacketConnection.cs @@ -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 { + /// + /// Represents a duplex connection to a client, allowing to receive and send instances. + /// 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) + /// + /// Initializes a new instance of the class communicating with the given + /// . + /// + /// The to communicate with. + internal PacketConnection(TcpClient client) { - _stream = tcpClient.GetStream(); - RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint; + _stream = client.GetStream(); + RemoteEndPoint = (IPEndPoint)client.Client.RemoteEndPoint; } // ---- PROPERTIES --------------------------------------------------------------------------------------------- + /// + /// Gets the client's . + /// internal IPEndPoint RemoteEndPoint { get; } // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- + /// + /// Blocks until a was received, and returns it. + /// + /// The received . internal Packet Receive() { - _recvSemaphore.Wait(); - try + lock (_recvLock) { Packet packet = new Packet(); packet.Receive(_stream); return packet; } - finally - { - _recvSemaphore.Release(); - } - } - - internal async Task ReceiveAsync() - { - await _recvSemaphore.WaitAsync(); - try - { - Packet packet = new Packet(); - await packet.ReceiveAsync(_stream); - return packet; - } - finally - { - _recvSemaphore.Release(); - } } + /// + /// Blocks until the given was sent. + /// + /// The to send. 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(); - } } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs index a589fa1..0e067f0 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Program.cs @@ -2,17 +2,18 @@ namespace Syroot.Worms.Worms2.GameServer { + /// + /// Represents the main class of the application containing the program entry point. + /// 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)); } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs index 4d500d0..5804e0a 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Proxy.cs @@ -6,17 +6,20 @@ using Syroot.ColoredConsole; namespace Syroot.Worms.Worms2.GameServer { - internal class Proxy + /// + /// Represents a proxy dumping Worms 2 network traffic to console for debug purposes. + /// + 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) diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Room.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Room.cs new file mode 100644 index 0000000..ec43f60 --- /dev/null +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Room.cs @@ -0,0 +1,40 @@ +namespace Syroot.Worms.Worms2.GameServer +{ + /// + /// Represents a room in which users can meet, chat, and host games. + /// + internal class Room + { + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + + /// + /// Initializes a new instance of the class with the given identificatoin. + /// + /// The unique numerical identifier of the room. + /// The name of the room as given by the creator. + /// The flag displayed with the room. + internal Room(int id, string name, Nation nation) + { + ID = id; + Name = name; + Session = new SessionInfo(nation, SessionType.Room); + } + + // ---- PROPERTIES --------------------------------------------------------------------------------------------- + + /// + /// Gets the unique numerical identifier of the room. + /// + internal int ID { get; } + + /// + /// Gets the name of the room as given by the creator. + /// + internal string Name { get; set; } + + /// + /// Gets the describing the room. + /// + internal SessionInfo Session { get; } + } +} diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs b/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs index ed766c2..a4cc977 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/Server.cs @@ -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 { + /// + /// Represents a simplistic game server managing users, rooms, and games. + /// internal class Server { // ---- FIELDS ------------------------------------------------------------------------------------------------- - private int _idCounter; - private readonly TcpListener _listener; + private int _lastID; + private readonly List _users = new List(); + private readonly List _rooms = new List(); + private readonly List _games = new List(); + private readonly BlockingCollection _jobs = new BlockingCollection(); private readonly Dictionary> _packetHandlers; // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ - internal Server(IPEndPoint localEndPoint) + /// + /// Initializes a new instance of the class. + /// + internal Server() => _packetHandlers = new Dictionary> { - _listener = new TcpListener(localEndPoint); - _packetHandlers = new Dictionary> - { - [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() + /// + /// Begins listening for new clients connecting to the given and dispatches + /// them into their own threads. + /// + 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? 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")); } } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/SessionInfo.cs b/src/tool/Syroot.Worms.Worms2.GameServer/SessionInfo.cs index 818bb22..729fb8a 100644 --- a/src/tool/Syroot.Worms.Worms2.GameServer/SessionInfo.cs +++ b/src/tool/Syroot.Worms.Worms2.GameServer/SessionInfo.cs @@ -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) --------------------------------------------------------------------------------------- /// 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 } } diff --git a/src/tool/Syroot.Worms.Worms2.GameServer/User.cs b/src/tool/Syroot.Worms.Worms2.GameServer/User.cs new file mode 100644 index 0000000..d1f7484 --- /dev/null +++ b/src/tool/Syroot.Worms.Worms2.GameServer/User.cs @@ -0,0 +1,52 @@ +namespace Syroot.Worms.Worms2.GameServer +{ + /// + /// Represents information on a client connected to the server. + /// + internal class User + { + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + + /// + /// Initializes a new instance of the class with the given identification. + /// + /// The to communicate through. + /// The unique numerical identifier of the user. + /// The textual login name displayed to others. + /// The flag displayed to others. + internal User(PacketConnection connection, int id, string name, Nation nation) + { + Connection = connection; + ID = id; + Name = name; + Session = new SessionInfo(nation, SessionType.User); + } + + // ---- PROPERTIES --------------------------------------------------------------------------------------------- + + /// + /// Gets the to communicate through. + /// + internal PacketConnection Connection { get; } + + /// + /// Gets the unique numerical identifier of the user. + /// + internal int ID { get; } + + /// + /// Gets the textual login name displayed to others. + /// + internal string Name { get; } + + /// + /// Gets the describing the user. + /// + internal SessionInfo Session { get; } + + /// + /// Gets or sets the ID of the the user is in, or 0 for no room. + /// + internal int RoomID { get; set; } + } +}