--- /dev/null
+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
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.8" />
+ <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>
+ <ItemGroup>
+ <None Update="config.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
</Project>
Connection = connection;
Connection.DataReceived += OnDataReceived;
Connection.Disconnected += OnDisconnected;
- Player = new ClientPlayer(this);
+ Player = new ClientPlayer(this, _gameManager);
}
public int Id { get; }
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);
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);
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<GameManager>();
-
+ private readonly ILogger<GameManager> _logger;
+ private readonly IPEndPoint _publicIp;
private readonly ConcurrentDictionary<int, Game> _games;
- public GameManager()
+ public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config)
{
+ _logger = logger;
+ _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort);
_games = new ConcurrentDictionary<int, Game>();
}
- 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;
}
return game;
}
+ public IEnumerable<Game> 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 _);
}
}
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<Matchmaker>();
-
+ private readonly ILogger<Matchmaker> _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<Matchmaker> logger, IOptions<ServerConfig> 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)
{
--- /dev/null
+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<MatchmakerService> _logger;
+ private readonly Matchmaker _matchmaker;
+
+ public MatchmakerService(ILogger<MatchmakerService> 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
-using System.IO;
-using Hazel;
+using Hazel;
using Impostor.Shared.Innersloth;
-using Impostor.Shared.Innersloth.Data;
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
--- /dev/null
+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<Game> 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
--- /dev/null
+using Hazel;
+using Impostor.Server.Net.Messages;
+using Impostor.Shared.Innersloth;
+
+namespace Impostor.Server.Net.State
+{
+ public partial class ClientPlayer
+ {
+ /// <summary>
+ /// Triggered when the connected client requests the game listing.
+ /// </summary>
+ /// <param name="options">
+ /// All options given.
+ /// At this moment, the client can only specify the map, impostor count and chat language.
+ /// </param>
+ 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
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)
{
private readonly ConcurrentDictionary<int, ClientPlayer> _players;
private readonly HashSet<IPAddress> _bannedIps;
- public Game(GameManager gameManager, int code, GameOptionsData options)
+ public Game(GameManager gameManager, IPEndPoint publicIp, int code, GameOptionsData options)
{
_gameManager = gameManager;
_players = new ConcurrentDictionary<int, ClientPlayer>();
_bannedIps = new HashSet<IPAddress>();
-
+
+ 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];
/// <summary>
/// Send a message to all players except one.
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<ServerConfig>(host.Configuration.GetSection("Server"));
+
+ services.AddSingleton<GameManager>();
+ services.AddSingleton<Matchmaker>();
+
+ services.AddHostedService<MatchmakerService>();
+ })
+ .UseSerilog();
}
}
\ No newline at end of file
--- /dev/null
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22023,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22023
+ }
+}
\ No newline at end of file