From 7974e333d263fdbc6739b0307cc20a5a656adce2 Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Sat, 26 Sep 2020 01:10:30 +0200 Subject: [PATCH] Added .NET generic host, configuration and gamelist packets --- src/Impostor.Server/Data/ServerConfig.cs | 10 +++ src/Impostor.Server/Impostor.Server.csproj | 8 +++ src/Impostor.Server/Net/Client.cs | 11 +++- .../Net/Manager/GameManager.cs | 55 +++++++++++++--- src/Impostor.Server/Net/Matchmaker.cs | 21 +++--- src/Impostor.Server/Net/MatchmakerService.cs | 37 +++++++++++ .../Net/Messages/Message00HostGame.cs | 44 +------------ .../Net/Messages/Message16GetGameListV2.cs | 48 ++++++++++++++ .../Net/State/ClientPlayer.Events.cs | 28 ++++++++ src/Impostor.Server/Net/State/ClientPlayer.cs | 10 ++- src/Impostor.Server/Net/State/Game.cs | 11 +++- src/Impostor.Server/Program.cs | 64 +++++++++++++------ src/Impostor.Server/config.json | 8 +++ 13 files changed, 268 insertions(+), 87 deletions(-) create mode 100644 src/Impostor.Server/Data/ServerConfig.cs create mode 100644 src/Impostor.Server/Net/MatchmakerService.cs create mode 100644 src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs create mode 100644 src/Impostor.Server/Net/State/ClientPlayer.Events.cs create mode 100644 src/Impostor.Server/config.json diff --git a/src/Impostor.Server/Data/ServerConfig.cs b/src/Impostor.Server/Data/ServerConfig.cs new file mode 100644 index 0000000..ff7de58 --- /dev/null +++ b/src/Impostor.Server/Data/ServerConfig.cs @@ -0,0 +1,10 @@ +namespace Impostor.Server.Data +{ + public class ServerConfig + { + public string PublicIp { get; set; } + public ushort PublicPort { get; set; } + public string ListenIp { get; set; } + public ushort ListenPort { get; set; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index bed5d07..003e636 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -18,7 +18,15 @@ + + + + + PreserveNewest + + + diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index 780234e..f171a74 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -26,7 +26,7 @@ namespace Impostor.Server.Net Connection = connection; Connection.DataReceived += OnDataReceived; Connection.Disconnected += OnDisconnected; - Player = new ClientPlayer(this); + Player = new ClientPlayer(this, _gameManager); } public int Id { get; } @@ -103,7 +103,7 @@ namespace Impostor.Server.Net var gameInfo = Message00HostGame.Deserialize(message); // Create game. - var game = _gameManager.Create(this, gameInfo); + var game = _gameManager.Create(gameInfo); if (game == null) { Player.SendDisconnectReason(DisconnectReason.ServerFull); @@ -238,6 +238,13 @@ namespace Impostor.Server.Net Player.Game.HandleKickPlayer(playerId, isBan); break; } + + case MessageFlags.GetGameListV2: + { + Message16GetGameListV2.Deserialize(message, out var options); + Player.OnRequestGameList(options); + break; + } default: Logger.Warning("Server received unknown flag {0}.", flag); diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index 0b2b251..b9cc9d5 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -1,33 +1,41 @@ using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Impostor.Server.Data; using Impostor.Server.Net.State; using Impostor.Shared.Innersloth; -using Serilog; +using Impostor.Shared.Innersloth.Data; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Impostor.Server.Net.Manager { public class GameManager { - private static readonly ILogger Logger = Log.ForContext(); - + private readonly ILogger _logger; + private readonly IPEndPoint _publicIp; private readonly ConcurrentDictionary _games; - public GameManager() + public GameManager(ILogger logger, IOptions config) { + _logger = logger; + _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort); _games = new ConcurrentDictionary(); } - public Game Create(Client owner, GameOptionsData options) + public Game Create(GameOptionsData options) { var gameCode = GameCode.GenerateCode(6); - var game = new Game(this, gameCode, options); + var game = new Game(this, _publicIp, gameCode, options); if (_games.TryAdd(gameCode, game)) { - Logger.Debug("Created game with code {0} ({1}).", game.CodeStr, gameCode); + _logger.LogDebug("Created game with code {0} ({1}).", game.CodeStr, gameCode); return game; } - Logger.Warning("Failed to create game."); + _logger.LogWarning("Failed to create game."); return null; } @@ -37,9 +45,38 @@ namespace Impostor.Server.Net.Manager return game; } + public IEnumerable FindListings(byte mapId, int impostorCount, GameKeywords language, int count = 10) + { + var results = 0; + + // Find games that have not started yet. + foreach (var (code, game) in _games.Where(x => + x.Value.GameState == GameStates.NotStarted && + x.Value.PlayerCount < 10)) + { + // Check for options. + // TODO: Re-enable map filter when GameData packets are done. + if (/* game.Options.MapId != mapId || */ + game.Options.Keywords != language || + (impostorCount != 0 && game.Options.NumImpostors != impostorCount)) + { + continue; + } + + // Add to result. + yield return game; + + // Break out if we have enough. + if (++results == count) + { + yield break; + } + } + } + public void Remove(int gameCode) { - Logger.Debug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode); + _logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode); _games.TryRemove(gameCode, out _); } } diff --git a/src/Impostor.Server/Net/Matchmaker.cs b/src/Impostor.Server/Net/Matchmaker.cs index 5a1bec7..b7bdb32 100644 --- a/src/Impostor.Server/Net/Matchmaker.cs +++ b/src/Impostor.Server/Net/Matchmaker.cs @@ -1,33 +1,38 @@ using System.Net; using Hazel; using Hazel.Udp; +using Impostor.Server.Data; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth.Data; -using Serilog; -using ILogger = Serilog.ILogger; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Impostor.Server.Net { public class Matchmaker { - private static readonly ILogger Logger = Log.ForContext(); - + private readonly ILogger _logger; + private readonly ServerConfig _config; private readonly GameManager _gameManager; private readonly ClientManager _clientManager; private readonly UdpConnectionListener _connection; - public Matchmaker(IPAddress ip, int port) + public Matchmaker(ILogger logger, IOptions configOptions, GameManager gameManager) { - _gameManager = new GameManager(); + _logger = logger; + _config = configOptions.Value; + _gameManager = gameManager; _clientManager = new ClientManager(); - _connection = new UdpConnectionListener(new IPEndPoint(ip, port), IPMode.IPv4, s => + _connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_config.ListenIp), _config.ListenPort), IPMode.IPv4, s => { - Logger.Warning("Log from Hazel: {0}", s); + _logger.LogWarning("Log from Hazel: {0}", s); }); _connection.NewConnection += OnNewConnection; } + + public IPEndPoint EndPoint => _connection.EndPoint; private void OnNewConnection(NewConnectionEventArgs e) { diff --git a/src/Impostor.Server/Net/MatchmakerService.cs b/src/Impostor.Server/Net/MatchmakerService.cs new file mode 100644 index 0000000..43fe21e --- /dev/null +++ b/src/Impostor.Server/Net/MatchmakerService.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net +{ + public class MatchmakerService : IHostedService + { + private readonly ILogger _logger; + private readonly Matchmaker _matchmaker; + + public MatchmakerService(ILogger logger, Matchmaker matchmaker) + { + _logger = logger; + _matchmaker = matchmaker; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _matchmaker.Start(); + _logger.LogInformation("Matchmaker is running on {0}:{1}.", + _matchmaker.EndPoint.Address, + _matchmaker.EndPoint.Port); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogWarning("Matchmaker is shutting down!"); + _matchmaker.Stop(); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Messages/Message00HostGame.cs b/src/Impostor.Server/Net/Messages/Message00HostGame.cs index 392bc38..60a5c64 100644 --- a/src/Impostor.Server/Net/Messages/Message00HostGame.cs +++ b/src/Impostor.Server/Net/Messages/Message00HostGame.cs @@ -1,7 +1,5 @@ -using System.IO; -using Hazel; +using Hazel; using Impostor.Shared.Innersloth; -using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.Messages { @@ -16,45 +14,7 @@ namespace Impostor.Server.Net.Messages public static GameOptionsData Deserialize(MessageReader reader) { - var bytes = reader.ReadBytesAndSize(); - - using (var stream = new MemoryStream(bytes)) - using (var binary = new BinaryReader(stream)) - { - var result = new GameOptionsData - { - Version = binary.ReadByte(), - MaxPlayers = binary.ReadByte(), - Keywords = (GameKeywords) binary.ReadUInt32(), - MapId = binary.ReadByte(), - PlayerSpeedMod = binary.ReadSingle(), - CrewLightMod = binary.ReadSingle(), - ImpostorLightMod = binary.ReadSingle(), - KillCooldown = binary.ReadSingle(), - NumCommonTasks = binary.ReadByte(), - NumLongTasks = binary.ReadByte(), - NumShortTasks = binary.ReadByte(), - NumEmergencyMeetings = binary.ReadInt32(), - NumImpostors = binary.ReadByte(), - KillDistance = binary.ReadByte(), - DiscussionTime = binary.ReadInt32(), - VotingTime = binary.ReadInt32(), - IsDefaults = binary.ReadBoolean() - }; - - if (result.Version > 1) - { - result.EmergencyCooldown = binary.ReadByte(); - } - - if (result.Version > 2) - { - result.ConfirmImpostor = binary.ReadBoolean(); - result.VisualTasks = binary.ReadBoolean(); - } - - return result; - } + return GameOptionsData.Deserialize(reader.ReadBytesAndSize()); } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs new file mode 100644 index 0000000..b8786ec --- /dev/null +++ b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Hazel; +using Impostor.Server.Net.State; +using Impostor.Shared.Innersloth; + +namespace Impostor.Server.Net.Messages +{ + internal static class Message16GetGameListV2 + { + public static void Deserialize(MessageReader reader, out GameOptionsData options) + { + reader.ReadPackedInt32(); // Hardcoded 0. + options = GameOptionsData.Deserialize(reader.ReadBytesAndSize()); + } + + public static void Serialize(MessageWriter writer, IEnumerable games) + { + writer.StartMessage(MessageFlags.GetGameListV2); + + // Count + writer.StartMessage(1); + writer.Write(123); // The Skeld + writer.Write(456); // Mira HQ + writer.Write(789); // Polus + writer.EndMessage(); + + // Listing + writer.StartMessage(0); + foreach (var game in games) + { + writer.StartMessage(0); + writer.Write(game.PublicIp.Address.GetAddressBytes()); + writer.Write((ushort) game.PublicIp.Port); + writer.Write(game.Code); + writer.Write(game.Host.Client.Name); + writer.Write(game.PlayerCount); + writer.WritePacked(1); // TODO: What does Age do? + writer.Write((byte) game.Options.MapId); + writer.Write((byte) game.Options.NumImpostors); + writer.Write((byte) game.Options.MaxPlayers); + writer.EndMessage(); + } + writer.EndMessage(); + + writer.EndMessage(); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/ClientPlayer.Events.cs b/src/Impostor.Server/Net/State/ClientPlayer.Events.cs new file mode 100644 index 0000000..87a8028 --- /dev/null +++ b/src/Impostor.Server/Net/State/ClientPlayer.Events.cs @@ -0,0 +1,28 @@ +using Hazel; +using Impostor.Server.Net.Messages; +using Impostor.Shared.Innersloth; + +namespace Impostor.Server.Net.State +{ + public partial class ClientPlayer + { + /// + /// Triggered when the connected client requests the game listing. + /// + /// + /// All options given. + /// At this moment, the client can only specify the map, impostor count and chat language. + /// + public void OnRequestGameList(GameOptionsData options) + { + using (var message = MessageWriter.Get(SendOption.Reliable)) + { + var games = _gameManager.FindListings(options.MapId, options.NumImpostors, options.Keywords); + + Message16GetGameListV2.Serialize(message, games); + + Client.Send(message); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/ClientPlayer.cs b/src/Impostor.Server/Net/State/ClientPlayer.cs index 242d6b9..d9c5c3d 100644 --- a/src/Impostor.Server/Net/State/ClientPlayer.cs +++ b/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -1,19 +1,23 @@ using Hazel; +using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.State { - public class ClientPlayer + public partial class ClientPlayer { - public ClientPlayer(Client client) + private readonly GameManager _gameManager; + + public ClientPlayer(Client client, GameManager gameManager) { + _gameManager = gameManager; + Client = client; } public Client Client { get; } public Game Game { get; set; } - public LimboStates LimboState { get; set; } public void SendDisconnectReason(DisconnectReason reason, string message = null) { diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs index fe043a1..8a60c2f 100644 --- a/src/Impostor.Server/Net/State/Game.cs +++ b/src/Impostor.Server/Net/State/Game.cs @@ -20,25 +20,30 @@ namespace Impostor.Server.Net.State private readonly ConcurrentDictionary _players; private readonly HashSet _bannedIps; - public Game(GameManager gameManager, int code, GameOptionsData options) + public Game(GameManager gameManager, IPEndPoint publicIp, int code, GameOptionsData options) { _gameManager = gameManager; _players = new ConcurrentDictionary(); _bannedIps = new HashSet(); - + + PublicIp = publicIp; Code = code; CodeStr = GameCode.IntToGameName(code); HostId = -1; GameState = GameStates.NotStarted; Options = options; } - + + public IPEndPoint PublicIp { get; } public int Code { get; } public string CodeStr { get; } public bool IsPublic { get; private set; } public int HostId { get; private set; } public GameStates GameState { get; private set; } public GameOptionsData Options { get; } + + public int PlayerCount => _players.Count; + public ClientPlayer Host => _players[HostId]; /// /// Send a message to all players except one. diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 9dab491..8cf8826 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -1,41 +1,65 @@ using System; -using System.Net; -using System.Threading; +using Impostor.Server.Data; using Impostor.Server.Net; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Serilog; +using Serilog.Events; namespace Impostor.Server { internal static class Program { - private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false); - - private static void Main(string[] args) + private static int Main(string[] args) { - // Listen for CTRL+C. - Console.CancelKeyPress += (sender, e) => - { - e.Cancel = true; - QuitEvent.Set(); - }; - - // Configure logger. Log.Logger = new LoggerConfiguration() #if DEBUG .MinimumLevel.Verbose() #else .MinimumLevel.Information() + #endif + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() .WriteTo.Console() .CreateLogger(); - // Initialize matchmaker. - var matchMaker = new Matchmaker(IPAddress.Any, 22023); - matchMaker.Start(); - Log.Logger.Information("Matchmaker is running on *:22023."); - QuitEvent.WaitOne(); - Log.Logger.Warning("Matchmaker is shutting down!"); - matchMaker.Stop(); + try + { + Log.Information("Starting Impostor"); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Impostor terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(builder => + { + builder.AddJsonFile("config.json", false); + builder.AddEnvironmentVariables(prefix: "IMPOSTOR_"); + builder.AddCommandLine(args); + }) + .ConfigureServices((host, services) => + { + services.Configure(host.Configuration.GetSection("Server")); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(); + }) + .UseSerilog(); } } \ No newline at end of file diff --git a/src/Impostor.Server/config.json b/src/Impostor.Server/config.json new file mode 100644 index 0000000..f969cbf --- /dev/null +++ b/src/Impostor.Server/config.json @@ -0,0 +1,8 @@ +{ + "Server": { + "PublicIp": "127.0.0.1", + "PublicPort": 22023, + "ListenIp": "0.0.0.0", + "ListenPort": 22023 + } +} \ No newline at end of file -- 2.39.5