2020-07-10 21:01:09 +02:00

508 lines
20 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
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 _lastID = 0x1000; // start at an offset to prevent bugs with chat
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 ------------------------------------------------------------------------------
/// <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,
[PacketCode.ListGames] = OnListGames,
[PacketCode.Login] = OnLogin,
[PacketCode.CreateRoom] = OnCreateRoom,
[PacketCode.Join] = OnJoin,
[PacketCode.Leave] = OnLeave,
[PacketCode.Close] = OnClose,
[PacketCode.CreateGame] = OnCreateGame,
[PacketCode.ChatRoom] = OnChatRoom,
[PacketCode.ConnectGame] = OnConnectGame,
};
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <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)
{
// Begin handling any queued jobs.
Task.Run(() => HandleJobs());
// Begin listening for new connections. Currently synchronous and blocking.
HandleConnections(localEndPoint);
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private static void SendPacket(PacketConnection connection, Packet packet)
{
LogPacket(connection, packet, false);
connection.Send(packet);
}
private static void LogPacket(PacketConnection connection, Packet packet, bool fromClient)
{
if (fromClient)
ColorConsole.WriteLine(Color.Cyan, $"{connection.RemoteEndPoint} >> {packet}");
else
ColorConsole.WriteLine(Color.Magenta, $"{connection.RemoteEndPoint} << {packet}");
}
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)
{
// Receive and log query.
Packet packet = connection.Receive();
LogPacket(connection, packet, true);
// Queue handling of known queries.
if (_packetHandlers.TryGetValue(packet.Code, out Action<PacketConnection, Packet>? handler))
_jobs.Add(() => handler(connection, packet));
else
ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} unhandled {packet.Code}.");
}
}
catch (Exception ex)
{
ColorConsole.WriteLine(Color.Red, $"{connection.RemoteEndPoint} disconnected. {ex.Message}");
_jobs.Add(() => OnDisconnectUser(connection));
}
}
private void LeaveGame(User fromUser)
{
// Close an abandoned game.
Game? game = _games.SingleOrDefault(x => x.Name == fromUser.Name);
if (game == null)
return;
_games.Remove(game);
// Notify other users.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.Leave,
value2: game.ID,
value10: fromUser.ID));
SendPacket(user.Connection, new Packet(PacketCode.Close,
value10: game.ID));
}
}
private void LeaveRoom(User fromUser)
{
// Close an abandoned room.
bool roomClosed = fromUser.RoomID != 0
&& !_users.Any(x => x != fromUser && x.RoomID == fromUser.RoomID)
&& !_games.Any(x => x.RoomID == fromUser.RoomID);
if (roomClosed)
_rooms.Remove(_rooms.Single(x => x.ID == fromUser.RoomID));
// Notify other users.
foreach (User user in _users.Where(x => x != fromUser))
{
// Notify room leave, if any.
if (fromUser.RoomID != 0)
{
SendPacket(user.Connection, new Packet(PacketCode.Leave,
value2: fromUser.RoomID,
value10: fromUser.ID));
}
// Notify room close, if any.
if (roomClosed)
{
SendPacket(user.Connection, new Packet(PacketCode.Close,
value10: fromUser.RoomID));
}
}
// Update user room.
fromUser.RoomID = 0;
}
// ---- Handlers ----
private void OnListRooms(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value4 != 0)
return;
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)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0)
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)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 != fromUser.RoomID || packet.Value4 != 0)
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)
{
if (packet.Value1 == null || packet.Value4 == null || packet.Name == null || packet.Session == null)
return;
// Check if user name is valid and not already taken.
if (_users.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
SendPacket(connection, new Packet(PacketCode.LoginReply, value1: 0, error: 1));
}
else
{
User newUser = new User(connection, ++_lastID, 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));
}
}
private void OnCreateRoom(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value1 != 0 || packet.Value4 != 0 || packet.Data == null
|| packet.Name == null || packet.Session == null)
return;
// Check if room name is valid is not already taken.
if (!_rooms.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
Room newRoom = new Room(++_lastID, packet.Name, packet.Session.Value.Nation,
connection.RemoteEndPoint.Address);
_rooms.Add(newRoom);
// 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,
value4: 0,
data: String.Empty, // do not report creator IP
name: newRoom.Name,
session: newRoom.Session));
}
// Send reply to creator.
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
value1: newRoom.ID,
error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
value1: 0,
error: 1));
}
}
private void OnJoin(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID)
return;
// Require valid room or game ID.
if (_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,
value2: fromUser.RoomID,
value10: fromUser.ID));
}
// Send reply to joiner.
SendPacket(connection, new Packet(PacketCode.JoinReply, error: 0));
}
else if (_games.Any(x => x.ID == packet.Value2 && x.RoomID == fromUser.RoomID))
{
// Notify other users about the join.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.Join,
value2: 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 OnLeave(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 == null || packet.Value10 != fromUser.ID)
return;
// Require valid room ID (never sent for games, users disconnect if leaving a game).
if (packet.Value2 == fromUser.RoomID)
{
LeaveRoom(fromUser);
// Reply to leaver.
SendPacket(connection, new Packet(PacketCode.LeaveReply,
error: 0));
}
else
{
// Reply to leaver.
SendPacket(connection, new Packet(PacketCode.LeaveReply,
error: 1));
}
}
private void OnDisconnectUser(PacketConnection connection)
{
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
_users.Remove(fromUser);
LeaveGame(fromUser);
LeaveRoom(fromUser);
// Notify user disconnect.
foreach (User user in _users)
{
SendPacket(user.Connection, new Packet(PacketCode.DisconnectUser,
value10: fromUser.ID));
}
}
private void OnClose(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value10 == null)
return;
// Never sent for games, users disconnect if leaving a game.
// Simply reply success to client, the server decides when to actually close rooms.
SendPacket(connection, new Packet(PacketCode.CloseReply, error: 0));
}
private void OnCreateGame(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value2 == null || packet.Data == null || packet.Session == null)
return;
// Require valid room ID.
if (packet.Value2 == fromUser.RoomID)
{
Game newGame = new Game(++_lastID, fromUser.Name, fromUser.Session.Nation, fromUser.RoomID,
connection.RemoteEndPoint.Address, // do not use bad NAT IP reported by users here
packet.Session.Value.Access);
_games.Add(newGame);
// Notify other users about new game, even those in other rooms.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.CreateGame,
value2: newGame.RoomID,
value4: newGame.ID,
data: newGame.IPAddress.ToString(),
name: newGame.Name,
session: newGame.Session));
}
// Send reply to host.
SendPacket(connection, new Packet(PacketCode.CreateGameReply,
value1: newGame.ID,
error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.CreateGameReply,
value1: 0,
error: 1));
}
}
private void OnChatRoom(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value0 != fromUser.ID || packet.Value3 == null || packet.Data == null)
return;
int targetID = packet.Value3.Value;
string prefix;
if (packet.Data.StartsWith(prefix = $"GRP:[ {fromUser.Name} ] ", StringComparison.InvariantCulture))
{
// Check if user can access the room.
if (fromUser.RoomID == targetID)
{
// Notify all users of the room.
string message = packet.Data.Substring(prefix.Length);
foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID && x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.ChatRoom,
value0: fromUser.ID,
value3: user.RoomID,
data: prefix + message));
}
// Notify sender.
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1));
}
}
else if (packet.Data.StartsWith(prefix = $"PRV:[ {fromUser.Name} ] ", StringComparison.InvariantCulture))
{
// Check if user can access the user.
User? user = _users.FirstOrDefault(x => x.RoomID == fromUser.RoomID && x.ID == targetID);
if (user == null)
{
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1));
}
else
{
// Notify receiver of the message.
string message = packet.Data.Substring(prefix.Length);
SendPacket(user.Connection, new Packet(PacketCode.ChatRoom,
value0: fromUser.ID,
value3: user.ID,
data: prefix + message));
// Notify sender.
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0));
}
}
}
private void OnConnectGame(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null || packet.Value0 == null)
return;
// Require valid game ID and user to be in appropriate room.
Game? game = _games.FirstOrDefault(x => x.ID == packet.Value0 && x.RoomID == fromUser.RoomID);
if (game == null)
{
SendPacket(connection, new Packet(PacketCode.ConnectGameReply,
data: String.Empty,
error: 1));
}
else
{
SendPacket(connection, new Packet(PacketCode.ConnectGameReply,
data: game.IPAddress.ToString(),
error: 0));
}
}
}
}