From 8ff4e7708b261e38a9bd252c5d195fd024558126 Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Thu, 22 Oct 2020 22:15:34 +0200 Subject: [PATCH] Update recorder and add replayer --- src/Impostor.Api/Games/IGameCodeFactory.cs | 7 + src/Impostor.Api/Impostor.Api.csproj | 6 +- src/Impostor.Api/Net/IClient.cs | 8 +- src/Impostor.Api/Net/IConnection.cs | 31 +--- src/Impostor.Api/Net/IHazelConnection.cs | 34 ++++ src/Impostor.Server/Net/Client.cs | 5 +- src/Impostor.Server/Net/ClientBase.Api.cs | 11 -- src/Impostor.Server/Net/ClientBase.cs | 10 +- .../Net/Factories/ClientFactory.cs | 3 +- .../Net/Factories/IClientFactory.cs | 4 +- src/Impostor.Server/Net/GameCodeFactory.cs | 12 ++ .../Net/Hazel/HazelConnection.Api.cs | 9 - .../Net/Hazel/HazelConnection.cs | 5 +- .../Net/Manager/ClientManager.Api.cs | 11 -- .../Net/Manager/ClientManager.cs | 7 +- .../Net/Manager/GameManager.Api.cs | 14 -- .../Net/Manager/GameManager.cs | 17 +- src/Impostor.Server/Net/Matchmaker.cs | 5 +- .../Net/State/Game.Outgoing.cs | 7 +- src/Impostor.Server/Net/State/Game.cs | 10 +- src/Impostor.Server/Program.cs | 2 + .../Properties/AssemblyInfo.cs | 3 +- .../Recorder/ClientRecorder.cs | 38 +++- .../Recorder/PacketRecorder.cs | 83 +++++++-- .../Recorder/RecordedPacketType.cs | 4 +- .../Impostor.Tools.ServerReplay.csproj | 16 ++ .../Mocks/MockGameCodeFactory.cs | 14 ++ .../Mocks/MockHazelConnection.cs | 26 +++ src/Impostor.Tools.ServerReplay/Program.cs | 167 ++++++++++++++++++ src/Impostor.sln | 11 ++ 30 files changed, 454 insertions(+), 126 deletions(-) create mode 100644 src/Impostor.Api/Games/IGameCodeFactory.cs create mode 100644 src/Impostor.Api/Net/IHazelConnection.cs delete mode 100644 src/Impostor.Server/Net/ClientBase.Api.cs create mode 100644 src/Impostor.Server/Net/GameCodeFactory.cs delete mode 100644 src/Impostor.Server/Net/Hazel/HazelConnection.Api.cs delete mode 100644 src/Impostor.Server/Net/Manager/ClientManager.Api.cs delete mode 100644 src/Impostor.Server/Net/Manager/GameManager.Api.cs create mode 100644 src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj create mode 100644 src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs create mode 100644 src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs create mode 100644 src/Impostor.Tools.ServerReplay/Program.cs diff --git a/src/Impostor.Api/Games/IGameCodeFactory.cs b/src/Impostor.Api/Games/IGameCodeFactory.cs new file mode 100644 index 0000000..f264fe0 --- /dev/null +++ b/src/Impostor.Api/Games/IGameCodeFactory.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Games +{ + public interface IGameCodeFactory + { + GameCode Create(); + } +} \ No newline at end of file diff --git a/src/Impostor.Api/Impostor.Api.csproj b/src/Impostor.Api/Impostor.Api.csproj index 3ff2516..973c5ac 100644 --- a/src/Impostor.Api/Impostor.Api.csproj +++ b/src/Impostor.Api/Impostor.Api.csproj @@ -9,9 +9,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Impostor.Api/Net/IClient.cs b/src/Impostor.Api/Net/IClient.cs index 137ace0..b882d2a 100644 --- a/src/Impostor.Api/Net/IClient.cs +++ b/src/Impostor.Api/Net/IClient.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; namespace Impostor.Api.Net { @@ -30,7 +32,7 @@ namespace Impostor.Api.Net /// /// Null when the client was not registered by the matchmaker. /// - IConnection? Connection { get; } + IHazelConnection? Connection { get; } /// /// Gets a value indicating whether the client is a bot. @@ -56,5 +58,9 @@ namespace Impostor.Api.Net /// Gets or sets the current game data of the . /// IClientPlayer? Player { get; } + + ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType); + + ValueTask HandleDisconnectAsync(string reason); } } \ No newline at end of file diff --git a/src/Impostor.Api/Net/IConnection.cs b/src/Impostor.Api/Net/IConnection.cs index 3c8e1f5..94f9b8b 100644 --- a/src/Impostor.Api/Net/IConnection.cs +++ b/src/Impostor.Api/Net/IConnection.cs @@ -1,34 +1,7 @@ -using System.Net; -using System.Threading.Tasks; -using Impostor.Api.Net.Messages; - -namespace Impostor.Api.Net +namespace Impostor.Api.Net { - /// - /// Represents the connection of the client. - /// public interface IConnection { - /// - /// Gets the IP endpoint of the client. - /// - IPEndPoint EndPoint { get; } - - /// - /// Gets a value indicating whether the client is connected to the server. - /// - bool IsConnected { get; } - - /// - /// Gets the client of the connection. - /// - IClient? Client { get; } - - /// - /// Sends a message writer to the connection. - /// - /// The message. - /// - ValueTask SendAsync(IMessageWriter writer); + } } \ No newline at end of file diff --git a/src/Impostor.Api/Net/IHazelConnection.cs b/src/Impostor.Api/Net/IHazelConnection.cs new file mode 100644 index 0000000..a39e0b5 --- /dev/null +++ b/src/Impostor.Api/Net/IHazelConnection.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Net +{ + /// + /// Represents the connection of the client. + /// + public interface IHazelConnection + { + /// + /// Gets the IP endpoint of the client. + /// + IPEndPoint EndPoint { get; } + + /// + /// Gets a value indicating whether the client is connected to the server. + /// + bool IsConnected { get; } + + /// + /// Gets the client of the connection. + /// + IClient? Client { get; set; } + + /// + /// Sends a message writer to the connection. + /// + /// The message. + /// + ValueTask SendAsync(IMessageWriter writer); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index 68068eb..361906b 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Impostor.Api.Games; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Data; +using Impostor.Api.Net; using Impostor.Api.Net.Messages; using Impostor.Api.Net.Messages.C2S; using Impostor.Api.Net.Messages.S2C; @@ -20,7 +21,7 @@ namespace Impostor.Server.Net private readonly ClientManager _clientManager; private readonly GameManager _gameManager; - public Client(ILogger logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection) + public Client(ILogger logger, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection) : base(name, connection) { _logger = logger; @@ -148,7 +149,7 @@ namespace Impostor.Server.Net var readerCopy = reader.Slice(reader.Position); // TODO: Return value, either a bool (to cancel) or a writer (to cancel (null) or modify/overwrite). - await Player.Game.HandleGameData(readerCopy, Player, toPlayer); + // await Player.Game.HandleGameData(readerCopy, Player, toPlayer); // Broadcast packet to all other players. using (var writer = MessageWriter.Get(messageType)) diff --git a/src/Impostor.Server/Net/ClientBase.Api.cs b/src/Impostor.Server/Net/ClientBase.Api.cs deleted file mode 100644 index 4b34987..0000000 --- a/src/Impostor.Server/Net/ClientBase.Api.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Impostor.Api.Net; - -namespace Impostor.Server.Net -{ - internal abstract partial class ClientBase : IClient - { - IConnection IClient.Connection => Connection; - - IClientPlayer IClient.Player => Player; - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/ClientBase.cs b/src/Impostor.Server/Net/ClientBase.cs index 9ca4d36..de23a39 100644 --- a/src/Impostor.Server/Net/ClientBase.cs +++ b/src/Impostor.Server/Net/ClientBase.cs @@ -1,15 +1,15 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; +using Impostor.Api.Net; using Impostor.Api.Net.Messages; -using Impostor.Server.Net.Hazel; using Impostor.Server.Net.State; namespace Impostor.Server.Net { - internal abstract partial class ClientBase + internal abstract class ClientBase : IClient { - protected ClientBase(string name, HazelConnection connection) + protected ClientBase(string name, IHazelConnection connection) { Name = name; Connection = connection; @@ -20,7 +20,7 @@ namespace Impostor.Server.Net public string Name { get; } - public HazelConnection Connection { get; } + public IHazelConnection Connection { get; } public bool IsBot => false; @@ -28,6 +28,8 @@ namespace Impostor.Server.Net public ClientPlayer Player { get; set; } + IClientPlayer IClient.Player => Player; + public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType); public abstract ValueTask HandleDisconnectAsync(string reason); diff --git a/src/Impostor.Server/Net/Factories/ClientFactory.cs b/src/Impostor.Server/Net/Factories/ClientFactory.cs index 632781b..c04044c 100644 --- a/src/Impostor.Server/Net/Factories/ClientFactory.cs +++ b/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -1,4 +1,5 @@ using System; +using Impostor.Api.Net; using Impostor.Server.Net.Hazel; using Microsoft.Extensions.DependencyInjection; @@ -14,7 +15,7 @@ namespace Impostor.Server.Net.Factories _serviceProvider = serviceProvider; } - public ClientBase Create(HazelConnection connection, string name, int clientVersion) + public ClientBase Create(IHazelConnection connection, string name, int clientVersion) { var client = ActivatorUtilities.CreateInstance(_serviceProvider, name, connection); connection.Client = client; diff --git a/src/Impostor.Server/Net/Factories/IClientFactory.cs b/src/Impostor.Server/Net/Factories/IClientFactory.cs index f090f74..6859ae3 100644 --- a/src/Impostor.Server/Net/Factories/IClientFactory.cs +++ b/src/Impostor.Server/Net/Factories/IClientFactory.cs @@ -1,9 +1,9 @@ -using Impostor.Server.Net.Hazel; +using Impostor.Api.Net; namespace Impostor.Server.Net.Factories { internal interface IClientFactory { - ClientBase Create(HazelConnection connection, string name, int clientVersion); + ClientBase Create(IHazelConnection connection, string name, int clientVersion); } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/GameCodeFactory.cs b/src/Impostor.Server/Net/GameCodeFactory.cs new file mode 100644 index 0000000..2a0553f --- /dev/null +++ b/src/Impostor.Server/Net/GameCodeFactory.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Games; + +namespace Impostor.Server.Net +{ + public class GameCodeFactory : IGameCodeFactory + { + public GameCode Create() + { + return GameCode.Create(); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/HazelConnection.Api.cs b/src/Impostor.Server/Net/Hazel/HazelConnection.Api.cs deleted file mode 100644 index c7056d2..0000000 --- a/src/Impostor.Server/Net/Hazel/HazelConnection.Api.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Impostor.Api.Net; - -namespace Impostor.Server.Net.Hazel -{ - internal partial class HazelConnection : IConnection - { - IClient IConnection.Client => Client; - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/HazelConnection.cs b/src/Impostor.Server/Net/Hazel/HazelConnection.cs index a1520b8..3e92594 100644 --- a/src/Impostor.Server/Net/Hazel/HazelConnection.cs +++ b/src/Impostor.Server/Net/Hazel/HazelConnection.cs @@ -1,12 +1,13 @@ using System.Net; using System.Threading.Tasks; +using Impostor.Api.Net; using Impostor.Api.Net.Messages; using Impostor.Hazel; using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Hazel { - internal partial class HazelConnection + internal class HazelConnection : IHazelConnection { private readonly ILogger _logger; @@ -24,7 +25,7 @@ namespace Impostor.Server.Net.Hazel public bool IsConnected => InnerConnection.State == ConnectionState.Connected; - public ClientBase Client { get; set; } + public IClient Client { get; set; } public ValueTask SendAsync(IMessageWriter writer) { diff --git a/src/Impostor.Server/Net/Manager/ClientManager.Api.cs b/src/Impostor.Server/Net/Manager/ClientManager.Api.cs deleted file mode 100644 index 6cbe5bf..0000000 --- a/src/Impostor.Server/Net/Manager/ClientManager.Api.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Impostor.Api.Net; -using Impostor.Api.Net.Manager; - -namespace Impostor.Server.Net.Manager -{ - internal partial class ClientManager : IClientManager - { - IEnumerable IClientManager.Clients => _clients.Values; - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Manager/ClientManager.cs b/src/Impostor.Server/Net/Manager/ClientManager.cs index 31f0a73..3c4e8e8 100644 --- a/src/Impostor.Server/Net/Manager/ClientManager.cs +++ b/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Data; using Impostor.Api.Net; +using Impostor.Api.Net.Manager; using Impostor.Api.Net.Messages; using Impostor.Api.Net.Messages.S2C; using Impostor.Hazel; @@ -15,7 +16,7 @@ using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Manager { - internal partial class ClientManager + internal class ClientManager : IClientManager { public static HashSet SupportedVersions { get; } = new HashSet { @@ -37,6 +38,8 @@ namespace Impostor.Server.Net.Manager public IEnumerable Clients => _clients.Values; + IEnumerable IClientManager.Clients => _clients.Values; + public int NextId() { var clientId = Interlocked.Increment(ref _idLast); @@ -53,7 +56,7 @@ namespace Impostor.Server.Net.Manager return clientId; } - public async ValueTask RegisterConnectionAsync(HazelConnection connection, string name, int clientVersion) + public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion) { if (name.Length > 10) { diff --git a/src/Impostor.Server/Net/Manager/GameManager.Api.cs b/src/Impostor.Server/Net/Manager/GameManager.Api.cs deleted file mode 100644 index 8ddb19b..0000000 --- a/src/Impostor.Server/Net/Manager/GameManager.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Impostor.Api.Games; -using Impostor.Api.Games.Managers; - -namespace Impostor.Server.Net.Manager -{ - internal partial class GameManager : IGameManager - { - IEnumerable IGameManager.Games => _games.Select(kv => kv.Value); - - IGame IGameManager.Find(GameCode code) => Find(code); - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index a47fc9d..bc65c25 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -8,6 +8,7 @@ using Impostor.Api; using Impostor.Api.Events; using Impostor.Api.Events.Managers; using Impostor.Api.Games; +using Impostor.Api.Games.Managers; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Data; using Impostor.Server.Data; @@ -20,7 +21,7 @@ using Microsoft.Extensions.Options; namespace Impostor.Server.Net.Manager { - internal partial class GameManager + internal class GameManager : IGameManager { private readonly ILogger _logger; private readonly INodeLocator _nodeLocator; @@ -28,18 +29,24 @@ namespace Impostor.Server.Net.Manager private readonly ConcurrentDictionary _games; private readonly IServiceProvider _serviceProvider; private readonly IEventManager _eventManager; + private readonly IGameCodeFactory _gameCodeFactory; - public GameManager(ILogger logger, IOptions config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager) + public GameManager(ILogger logger, IOptions config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager, IGameCodeFactory gameCodeFactory) { _logger = logger; _nodeLocator = nodeLocator; _serviceProvider = serviceProvider; _eventManager = eventManager; + _gameCodeFactory = gameCodeFactory; _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort); _games = new ConcurrentDictionary(); } - public async ValueTask CreateAsync(GameOptionsData options) + IEnumerable IGameManager.Games => _games.Select(kv => kv.Value); + + IGame IGameManager.Find(GameCode code) => Find(code); + + public async ValueTask CreateAsync(GameOptionsData options) { // TODO: Prevent duplicates when using server redirector using INodeProvider. var (success, game) = await TryCreateAsync(options); @@ -57,9 +64,9 @@ namespace Impostor.Server.Net.Manager return game; } - public async ValueTask<(bool success, Game game)> TryCreateAsync(GameOptionsData options) + private async ValueTask<(bool success, Game game)> TryCreateAsync(GameOptionsData options) { - var gameCode = GameCode.Create(); + var gameCode = _gameCodeFactory.Create(); var gameCodeStr = gameCode.Code; var game = ActivatorUtilities.CreateInstance(_serviceProvider, _publicIp, gameCode, options); diff --git a/src/Impostor.Server/Net/Matchmaker.cs b/src/Impostor.Server/Net/Matchmaker.cs index 54a0581..1c1bcf3 100644 --- a/src/Impostor.Server/Net/Matchmaker.cs +++ b/src/Impostor.Server/Net/Matchmaker.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Threading.Tasks; using Impostor.Hazel; using Impostor.Hazel.Udp; +using Impostor.Server.Net.Hazel; using Impostor.Server.Net.Manager; using Microsoft.Extensions.Logging; @@ -19,7 +20,7 @@ namespace Impostor.Server.Net public Matchmaker( ILogger logger, ClientManager clientManager, - ILogger connectionLogger) + ILogger connectionLogger) { _logger = logger; _clientManager = clientManager; @@ -52,7 +53,7 @@ namespace Impostor.Server.Net var clientVersion = e.HandshakeData.ReadInt32(); var name = e.HandshakeData.ReadString(); - var connection = new Net.Hazel.HazelConnection(e.Connection, _connectionLogger); + var connection = new HazelConnection(e.Connection, _connectionLogger); // Register client await _clientManager.RegisterConnectionAsync(connection, name, clientVersion); diff --git a/src/Impostor.Server/Net/State/Game.Outgoing.cs b/src/Impostor.Server/Net/State/Game.Outgoing.cs index 14c493b..57e3a62 100644 --- a/src/Impostor.Server/Net/State/Game.Outgoing.cs +++ b/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Impostor.Api.Innersloth.Data; using Impostor.Api.Net; @@ -30,9 +31,9 @@ namespace Impostor.Server.Net.State public async ValueTask SendToAsync(IMessageWriter writer, int id) { - if (TryGetPlayer(id, out var player) && player.Client.Connection is HazelConnection hazelConnection) + if (TryGetPlayer(id, out var player)) { - await hazelConnection.InnerConnection.SendAsync(writer); + await player.Client.Connection.SendAsync(writer); } } diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs index 5aa7925..2110c04 100644 --- a/src/Impostor.Server/Net/State/Game.cs +++ b/src/Impostor.Server/Net/State/Game.cs @@ -27,7 +27,6 @@ namespace Impostor.Server.Net.State private readonly IServiceProvider _serviceProvider; private readonly GameManager _gameManager; private readonly ClientManager _clientManager; - private readonly Matchmaker _matchmaker; private readonly ConcurrentDictionary _players; private readonly HashSet _bannedIps; private readonly IEventManager _eventManager; @@ -35,11 +34,9 @@ namespace Impostor.Server.Net.State public Game( IServiceProvider serviceProvider, GameManager gameManager, - INodeLocator nodeLocator, IPEndPoint publicIp, GameCode code, GameOptionsData options, - Matchmaker matchmaker, ClientManager clientManager, IEventManager eventManager) { @@ -53,7 +50,6 @@ namespace Impostor.Server.Net.State HostId = -1; GameState = GameStates.NotStarted; Options = options; - _matchmaker = matchmaker; _clientManager = clientManager; _eventManager = eventManager; Items = new ConcurrentDictionary(); @@ -103,13 +99,11 @@ namespace Impostor.Server.Net.State return SendToAllExceptAsync(message, player.Client.Id); } - private IEnumerable GetConnections(Func filter) + private IEnumerable GetConnections(Func filter) { return Players .Where(filter) - .Select(p => p.Client.Connection) - .OfType() - .Select(c => c.InnerConnection); + .Select(p => p.Client.Connection); } } } \ No newline at end of file diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index f85e23d..b3f8279 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -1,5 +1,6 @@ using System; using Impostor.Api.Events.Managers; +using Impostor.Api.Games; using Impostor.Api.Games.Managers; using Impostor.Api.Net.Manager; using Impostor.Server.Data; @@ -169,6 +170,7 @@ namespace Impostor.Server services.AddSingleton(p => p.GetRequiredService()); } + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); diff --git a/src/Impostor.Server/Properties/AssemblyInfo.cs b/src/Impostor.Server/Properties/AssemblyInfo.cs index eb56780..99d6f86 100644 --- a/src/Impostor.Server/Properties/AssemblyInfo.cs +++ b/src/Impostor.Server/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Impostor.Tests")] \ No newline at end of file +[assembly:InternalsVisibleTo("Impostor.Tests")] +[assembly:InternalsVisibleTo("Impostor.Tools.ServerReplay")] \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/ClientRecorder.cs b/src/Impostor.Server/Recorder/ClientRecorder.cs index a03be75..b41a687 100644 --- a/src/Impostor.Server/Recorder/ClientRecorder.cs +++ b/src/Impostor.Server/Recorder/ClientRecorder.cs @@ -10,17 +10,53 @@ namespace Impostor.Server.Recorder internal class ClientRecorder : Client { private readonly PacketRecorder _recorder; + private bool _isFirst; + private bool _createdGame; public ClientRecorder(ILogger logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder) : base(logger, clientManager, gameManager, name, connection) { _recorder = recorder; + _isFirst = true; + _createdGame = false; } public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) { - await _recorder.WriteMessageAsync(this, reader.Tag, reader.Buffer); + var messageCopy = reader.Slice(0); + + // Trigger connect event. + if (_isFirst) + { + _isFirst = false; + + await _recorder.WriteConnectAsync(this); + } + + // Check if we were in-game before handling the message. + var inGame = Player?.Game != null; + + // Handle the message. await base.HandleMessageAsync(reader, messageType); + + // Player created a game. + if (reader.Tag == MessageFlags.HostGame) + { + _createdGame = true; + } + else if (reader.Tag == MessageFlags.JoinGame && _createdGame) + { + _createdGame = false; + + // We created a game and are now in-game, stored that event. + if (!inGame && Player?.Game != null) + { + await _recorder.WriteGameCreatedAsync(this, Player.Game.Code); + } + } + + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); } public override async ValueTask HandleDisconnectAsync(string reason) diff --git a/src/Impostor.Server/Recorder/PacketRecorder.cs b/src/Impostor.Server/Recorder/PacketRecorder.cs index a949e58..23be21a 100644 --- a/src/Impostor.Server/Recorder/PacketRecorder.cs +++ b/src/Impostor.Server/Recorder/PacketRecorder.cs @@ -2,6 +2,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; using Impostor.Server.Data; using Impostor.Server.Net; using Microsoft.Extensions.Logging; @@ -32,17 +34,16 @@ namespace Impostor.Server.Recorder _writer = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); } - public async Task WriteMessageAsync(ClientRecorder client, byte tag, ReadOnlyMemory buffer) + public async Task WriteConnectAsync(ClientRecorder client) { - _logger.LogTrace("Writing Message."); + _logger.LogTrace("Writing Connect."); var context = _pool.Get(); try { - WriteHeader(context, RecordedPacketType.Message); - WriteClient(context, client); - WritePacket(context, tag, buffer.Span); + WriteHeader(context, RecordedPacketType.Connect); + WriteClient(context, client, true); WriteLength(context); await WriteAsync(context.Stream); @@ -62,7 +63,49 @@ namespace Impostor.Server.Recorder try { WriteHeader(context, RecordedPacketType.Disconnect); - WriteClient(context, client); + WriteClient(context, client, false); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteMessageAsync(ClientRecorder client, IMessageReader reader, MessageType messageType) + { + _logger.LogTrace("Writing Message."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Message); + WriteClient(context, client, false); + WritePacket(context, reader, messageType); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteGameCreatedAsync(ClientRecorder client, GameCode gameCode) + { + _logger.LogTrace("Writing GameCreated {0}.", gameCode); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.GameCreated); + WriteClient(context, client, false); + WriteGameCode(context, gameCode); WriteLength(context); await WriteAsync(context.Stream); @@ -80,20 +123,32 @@ namespace Impostor.Server.Recorder context.Writer.Write((byte) type); } - private static void WriteClient(PacketSerializationContext context, ClientBase client) + private static void WriteClient(PacketSerializationContext context, ClientBase client, bool full) { var addressBytes = client.Connection.EndPoint.Address.GetAddressBytes(); - context.Writer.Write((byte) addressBytes.Length); - context.Writer.Write(addressBytes); - context.Writer.Write((ushort) client.Connection.EndPoint.Port); + context.Writer.Write(client.Id); + + if (full) + { + context.Writer.Write((byte) addressBytes.Length); + context.Writer.Write(addressBytes); + context.Writer.Write((ushort) client.Connection.EndPoint.Port); + context.Writer.Write(client.Name); + } + } + + private static void WritePacket(PacketSerializationContext context, IMessageReader reader, MessageType messageType) + { + context.Writer.Write((byte) messageType); + context.Writer.Write((byte) reader.Tag); + context.Writer.Write((int) reader.Buffer.Length); + context.Writer.Write(reader.Buffer.Span); } - private static void WritePacket(PacketSerializationContext context, byte tag, ReadOnlySpan buffer) + private static void WriteGameCode(PacketSerializationContext context, in GameCode gameCode) { - context.Writer.Write((byte) tag); - context.Writer.Write((int) buffer.Length); - context.Writer.Write(buffer); + context.Writer.Write(gameCode.Code); } private static void WriteLength(PacketSerializationContext context) diff --git a/src/Impostor.Server/Recorder/RecordedPacketType.cs b/src/Impostor.Server/Recorder/RecordedPacketType.cs index 24e645e..a8a20bc 100644 --- a/src/Impostor.Server/Recorder/RecordedPacketType.cs +++ b/src/Impostor.Server/Recorder/RecordedPacketType.cs @@ -2,7 +2,9 @@ { internal enum RecordedPacketType : byte { - Message = 1, + Connect = 1, Disconnect = 2, + Message = 3, + GameCreated = 4 } } \ No newline at end of file diff --git a/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj b/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj new file mode 100644 index 0000000..53ce7bf --- /dev/null +++ b/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + + + + + + + + + + + diff --git a/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs b/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs new file mode 100644 index 0000000..1111b7e --- /dev/null +++ b/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs @@ -0,0 +1,14 @@ +using Impostor.Api.Games; + +namespace Impostor.Tools.ServerReplay.Mocks +{ + public class MockGameCodeFactory : IGameCodeFactory + { + public GameCode Result { get; set; } + + public GameCode Create() + { + return Result; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs b/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs new file mode 100644 index 0000000..941fdb1 --- /dev/null +++ b/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; + +namespace Impostor.Tools.ServerReplay.Mocks +{ + public class MockHazelConnection : IHazelConnection + { + public MockHazelConnection(IPEndPoint endPoint) + { + EndPoint = endPoint; + IsConnected = true; + Client = null; + } + + public IPEndPoint EndPoint { get; } + public bool IsConnected { get; } + public IClient? Client { get; set; } + + public ValueTask SendAsync(IMessageWriter writer) + { + return ValueTask.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Tools.ServerReplay/Program.cs b/src/Impostor.Tools.ServerReplay/Program.cs new file mode 100644 index 0000000..adb9b19 --- /dev/null +++ b/src/Impostor.Tools.ServerReplay/Program.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Hazel; +using Impostor.Server.Events; +using Impostor.Server.Net; +using Impostor.Server.Net.Factories; +using Impostor.Server.Net.Manager; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Recorder; +using Impostor.Tools.ServerReplay.Mocks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace Impostor.Tools.ServerReplay +{ + internal static class Program + { + private const string GameRecorderPath = "C:\\DevData\\ImpostorSessions"; + + private static readonly ILogger Logger = Log.ForContext(typeof(Program)); + private static readonly Dictionary Connections = new Dictionary(); + private static readonly Dictionary GameOptions = new Dictionary(); + + private static ServiceProvider _serviceProvider; + + private static MockGameCodeFactory _gameCodeFactory; + private static ClientManager _clientManager; + private static GameManager _gameManager; + + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console() + .CreateLogger(); + + // Create service provider. + _serviceProvider = BuildServices(); + + // Create required instances. + _gameCodeFactory = _serviceProvider.GetRequiredService(); + _clientManager = _serviceProvider.GetRequiredService(); + _gameManager = _serviceProvider.GetRequiredService(); + + foreach (var file in Directory.GetFiles(GameRecorderPath)) + { + await using (var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var reader = new BinaryReader(stream)) + { + await ParseSession(reader); + } + } + } + + private static ServiceProvider BuildServices() + { + var services = new ServiceCollection(); + + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddSerilog(); + }); + + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton>(); + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private static async Task ParseSession(BinaryReader reader) + { + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var dataLength = reader.ReadInt32(); + var data = reader.ReadBytes(dataLength - 4); + + await using (var stream = new MemoryStream(data)) + using (var readerInner = new BinaryReader(stream)) + { + await ParsePacket(readerInner); + } + } + } + + private static async Task ParsePacket(BinaryReader reader) + { + var dataType = (RecordedPacketType) reader.ReadByte(); + + // Read client id. + var clientId = reader.ReadInt32(); + + switch (dataType) + { + case RecordedPacketType.Connect: + // Read data. + var addressLength = reader.ReadByte(); + var addressBytes = reader.ReadBytes(addressLength); + var addressPort = reader.ReadUInt16(); + var address = new IPEndPoint(new IPAddress(addressBytes), addressPort); + var name = reader.ReadString(); + + // Create and register connection. + var connection = new MockHazelConnection(address); + + await _clientManager.RegisterConnectionAsync(connection, name, 50516550); + + // Store reference for ourselfs. + Connections.Add(clientId, connection); + break; + + case RecordedPacketType.Disconnect: + await Connections[clientId].Client!.HandleDisconnectAsync(null); + Connections.Remove(clientId); + break; + + case RecordedPacketType.Message: + var messageType = (MessageType) reader.ReadByte(); + var tag = reader.ReadByte(); + var length = reader.ReadInt32(); + var buffer = reader.ReadBytes(length); + var message = new MessageReader(tag, buffer); + + if (tag == MessageFlags.HostGame) + { + GameOptions.Add(clientId, Message00HostGameC2S.Deserialize(message)); + } + else + { + await Connections[clientId].Client!.HandleMessageAsync(message, messageType); + } + break; + + case RecordedPacketType.GameCreated: + _gameCodeFactory.Result = GameCode.From(reader.ReadString()); + + await _gameManager.CreateAsync(GameOptions[clientId]); + + GameOptions.Remove(clientId); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.sln b/src/Impostor.sln index 3d60abb..c04c378 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.ServerReplay", "Impostor.Tools.ServerReplay\Impostor.Tools.ServerReplay.csproj", "{4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -125,6 +127,14 @@ Global {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.Build.0 = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.Build.0 = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.ActiveCfg = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,5 +150,6 @@ Global {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F} {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F} + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99} = {56DD9707-D811-4056-9E2C-8A9CC2479B07} EndGlobalSection EndGlobal -- 2.39.5