From 443168ef7163ac15111dd6da2d08c9f0f9f8cb86 Mon Sep 17 00:00:00 2001 From: js6pak Date: Sun, 14 Mar 2021 20:50:55 +0000 Subject: [PATCH] Add DateTime to server replays (#349) --- src/.editorconfig | 5 +- src/Impostor.Api/IDateTimeProvider.cs | 9 +++ src/Impostor.Server/Net/Client.cs | 4 +- src/Impostor.Server/Net/ClientBase.cs | 5 +- .../Net/Factories/ClientFactory.cs | 4 +- .../Net/Inner/Objects/InnerPlayerControl.cs | 12 ++-- .../Net/Inner/Objects/InnerPlayerInfo.cs | 5 +- .../Net/Redirector/ClientRedirector.cs | 3 +- src/Impostor.Server/Program.cs | 2 + src/Impostor.Server/RealDateTimeProvider.cs | 10 ++++ .../Recorder/ClientRecorder.cs | 4 +- .../Recorder/PacketRecorder.cs | 54 +++++++++++++----- .../Recorder/ServerReplayVersion.cs | 18 ++++++ .../FakeDateTimeProvider.cs | 10 ++++ src/Impostor.Tools.ServerReplay/Program.cs | 23 +++++++- .../sessions/.gitkeep | 0 ...on_1604255331821_dead_player_exception.dat | Bin 47057 -> 0 bytes 17 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 src/Impostor.Api/IDateTimeProvider.cs create mode 100644 src/Impostor.Server/RealDateTimeProvider.cs create mode 100644 src/Impostor.Server/Recorder/ServerReplayVersion.cs create mode 100644 src/Impostor.Tools.ServerReplay/FakeDateTimeProvider.cs create mode 100644 src/Impostor.Tools.ServerReplay/sessions/.gitkeep delete mode 100644 src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat diff --git a/src/.editorconfig b/src/.editorconfig index af97d16..38c7085 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -163,7 +163,7 @@ csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true +# csharp_new_line_before_members_in_object_initializers = true TODO seems like Rider/ReSharper has the value inverted, uncomment when its fixed csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true @@ -228,3 +228,6 @@ csharp_preserve_single_line_statements = true # warning RS0037: PublicAPI.txt is missing '#nullable enable' dotnet_diagnostic.RS0037.severity = none + +# ReSharper properties +resharper_trailing_comma_in_multiline_lists = true diff --git a/src/Impostor.Api/IDateTimeProvider.cs b/src/Impostor.Api/IDateTimeProvider.cs new file mode 100644 index 0000000..95b3dee --- /dev/null +++ b/src/Impostor.Api/IDateTimeProvider.cs @@ -0,0 +1,9 @@ +using System; + +namespace Impostor.Api +{ + public interface IDateTimeProvider + { + DateTimeOffset UtcNow { get; } + } +} diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index 713ef88..f26de40 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -24,8 +24,8 @@ namespace Impostor.Server.Net private readonly ClientManager _clientManager; private readonly GameManager _gameManager; - public Client(ILogger logger, IOptions antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection, ISet mods) - : base(name, connection, mods) + public Client(ILogger logger, IOptions antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, int gameVersion, IHazelConnection connection, ISet mods) + : base(name, gameVersion, connection, mods) { _logger = logger; _antiCheatConfig = antiCheatOptions.Value; diff --git a/src/Impostor.Server/Net/ClientBase.cs b/src/Impostor.Server/Net/ClientBase.cs index bae1fb3..7bff06e 100644 --- a/src/Impostor.Server/Net/ClientBase.cs +++ b/src/Impostor.Server/Net/ClientBase.cs @@ -16,9 +16,10 @@ namespace Impostor.Server.Net { internal abstract class ClientBase : IClient { - protected ClientBase(string name, IHazelConnection connection, ISet mods) + protected ClientBase(string name, int gameVersion, IHazelConnection connection, ISet mods) { Name = name; + GameVersion = gameVersion; Connection = connection; Mods = mods; Items = new ConcurrentDictionary(); @@ -40,6 +41,8 @@ namespace Impostor.Server.Net public string Name { get; } + public int GameVersion { get; } + public ISet Mods { get; } public Dictionary ModIdMap { get; } diff --git a/src/Impostor.Server/Net/Factories/ClientFactory.cs b/src/Impostor.Server/Net/Factories/ClientFactory.cs index ab6eedf..985ab4e 100644 --- a/src/Impostor.Server/Net/Factories/ClientFactory.cs +++ b/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -18,9 +18,9 @@ namespace Impostor.Server.Net.Factories public ClientBase Create(IHazelConnection connection, string name, int clientVersion, ISet mods) { - var client = ActivatorUtilities.CreateInstance(_serviceProvider, name, connection, mods); + var client = ActivatorUtilities.CreateInstance(_serviceProvider, name, clientVersion, connection, mods); connection.Client = client; return client; } } -} \ No newline at end of file +} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index c275913..8391ed0 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -13,7 +13,6 @@ using Impostor.Api.Net.Messages.Rpcs; using Impostor.Server.Events.Player; using Impostor.Server.Net.Inner.Objects.Components; using Impostor.Server.Net.State; -using Impostor.Server.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,14 +23,14 @@ namespace Impostor.Server.Net.Inner.Objects private readonly ILogger _logger; private readonly IEventManager _eventManager; private readonly Game _game; - private readonly ServerEnvironment _serverEnvironment; + private readonly IDateTimeProvider _dateTimeProvider; - public InnerPlayerControl(ILogger logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game, ServerEnvironment serverEnvironment) + public InnerPlayerControl(ILogger logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game, IDateTimeProvider dateTimeProvider) { _logger = logger; _eventManager = eventManager; _game = game; - _serverEnvironment = serverEnvironment; + _dateTimeProvider = dateTimeProvider; Physics = ActivatorUtilities.CreateInstance(serviceProvider, this, _eventManager, _game); NetworkTransform = ActivatorUtilities.CreateInstance(serviceProvider, this, _game); @@ -520,8 +519,7 @@ namespace Impostor.Server.Net.Inner.Objects private async ValueTask HandleMurderPlayer(ClientPlayer sender, IInnerPlayerControl? target) { - // TODO record replay with timestamps - if (!_serverEnvironment.IsReplay && !PlayerInfo.CanMurder(_game)) + if (!PlayerInfo.CanMurder(_game, _dateTimeProvider)) { if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder too fast")) { @@ -537,7 +535,7 @@ namespace Impostor.Server.Net.Inner.Objects } } - PlayerInfo.LastMurder = DateTimeOffset.UtcNow; + PlayerInfo.LastMurder = _dateTimeProvider.UtcNow; if (!target.PlayerInfo.IsDead) { diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs index 410b4af..301fc08 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Impostor.Api; using Impostor.Api.Games; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Customization; @@ -40,14 +41,14 @@ namespace Impostor.Server.Net.Inner.Objects public DateTimeOffset LastMurder { get; set; } - public bool CanMurder(IGame game) + public bool CanMurder(IGame game, IDateTimeProvider dateTimeProvider) { if (!IsImpostor) { return false; } - return DateTimeOffset.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown; + return dateTimeProvider.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown; } public void Serialize(IMessageWriter writer) diff --git a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs index 878a052..afae57e 100644 --- a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs +++ b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -24,12 +24,13 @@ namespace Impostor.Server.Net.Redirector public ClientRedirector( string name, + int gameVersion, HazelConnection connection, ISet mods, ClientManager clientManager, INodeProvider nodeProvider, INodeLocator nodeLocator) - : base(name, connection, mods) + : base(name, gameVersion, connection, mods) { _clientManager = clientManager; _nodeProvider = nodeProvider; diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 2af8d55..109c6be 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Impostor.Api; using Impostor.Api.Events.Managers; using Impostor.Api.Games; using Impostor.Api.Games.Managers; @@ -117,6 +118,7 @@ namespace Impostor.Server .Get() ?? new AnnouncementsServerConfig(); services.AddSingleton(); + services.AddSingleton(); services.Configure(host.Configuration.GetSection(DebugConfig.Section)); services.Configure(host.Configuration.GetSection(AntiCheatConfig.Section)); diff --git a/src/Impostor.Server/RealDateTimeProvider.cs b/src/Impostor.Server/RealDateTimeProvider.cs new file mode 100644 index 0000000..506e0c0 --- /dev/null +++ b/src/Impostor.Server/RealDateTimeProvider.cs @@ -0,0 +1,10 @@ +using System; +using Impostor.Api; + +namespace Impostor.Server +{ + public class RealDateTimeProvider : IDateTimeProvider + { + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + } +} diff --git a/src/Impostor.Server/Recorder/ClientRecorder.cs b/src/Impostor.Server/Recorder/ClientRecorder.cs index 868aa0a..fe9d94c 100644 --- a/src/Impostor.Server/Recorder/ClientRecorder.cs +++ b/src/Impostor.Server/Recorder/ClientRecorder.cs @@ -18,8 +18,8 @@ namespace Impostor.Server.Recorder private bool _createdGame; private bool _recordAfter; - public ClientRecorder(ILogger logger, IOptions antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, ISet mods, PacketRecorder recorder) - : base(logger, antiCheatOptions, clientManager, gameManager, name, connection, mods) + public ClientRecorder(ILogger logger, IOptions antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, int gameVersion, HazelConnection connection, ISet mods, PacketRecorder recorder) + : base(logger, antiCheatOptions, clientManager, gameManager, name, gameVersion, connection, mods) { _recorder = recorder; _isFirst = true; diff --git a/src/Impostor.Server/Recorder/PacketRecorder.cs b/src/Impostor.Server/Recorder/PacketRecorder.cs index af2ea4f..c8efdb3 100644 --- a/src/Impostor.Server/Recorder/PacketRecorder.cs +++ b/src/Impostor.Server/Recorder/PacketRecorder.cs @@ -8,6 +8,7 @@ using Impostor.Api.Games; using Impostor.Api.Net.Messages; using Impostor.Server.Config; using Impostor.Server.Net; +using Impostor.Server.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; @@ -24,6 +25,7 @@ namespace Impostor.Server.Recorder private readonly ILogger _logger; private readonly ObjectPool _pool; private readonly Channel _channel; + private DateTimeOffset _startTime; public PacketRecorder(ILogger logger, IOptions options, ObjectPool pool) { @@ -42,10 +44,13 @@ namespace Impostor.Server.Recorder protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + _startTime = DateTimeOffset.UtcNow; _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", _path); var writer = File.Open(_path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + await WriteFileHeaderAsync(); + // Handle messages. try { @@ -73,7 +78,7 @@ namespace Impostor.Server.Recorder try { - WriteHeader(context, RecordedPacketType.Connect); + WritePacketHeader(context, RecordedPacketType.Connect); WriteClient(context, client, true); WriteLength(context); @@ -93,7 +98,7 @@ namespace Impostor.Server.Recorder try { - WriteHeader(context, RecordedPacketType.Disconnect); + WritePacketHeader(context, RecordedPacketType.Disconnect); WriteClient(context, client, false); context.Writer.Write(reason); WriteLength(context); @@ -114,7 +119,7 @@ namespace Impostor.Server.Recorder try { - WriteHeader(context, RecordedPacketType.Message); + WritePacketHeader(context, RecordedPacketType.Message); WriteClient(context, client, false); WritePacket(context, reader, messageType); WriteLength(context); @@ -135,7 +140,7 @@ namespace Impostor.Server.Recorder try { - WriteHeader(context, RecordedPacketType.GameCreated); + WritePacketHeader(context, RecordedPacketType.GameCreated); WriteClient(context, client, false); WriteGameCode(context, gameCode); WriteLength(context); @@ -148,11 +153,33 @@ namespace Impostor.Server.Recorder } } - private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type) + private async Task WriteFileHeaderAsync() + { + var context = _pool.Get(); + + try + { + context.Writer.Write((uint)ServerReplayVersion.Initial); + context.Writer.Write(_startTime.ToUnixTimeMilliseconds()); + context.Writer.Write(DotnetUtils.GetVersion()); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + private void WritePacketHeader(PacketSerializationContext context, RecordedPacketType type) { // Length placeholder. - context.Writer.Write((int) 0); - context.Writer.Write((byte) type); + context.Writer.Write(0); + + // Timestamp relative to recording start time. + context.Writer.Write((uint)(DateTimeOffset.UtcNow - _startTime).TotalMilliseconds); + + context.Writer.Write((byte)type); } private static void WriteClient(PacketSerializationContext context, ClientBase client, bool full) @@ -164,18 +191,19 @@ namespace Impostor.Server.Recorder if (full) { - context.Writer.Write((byte) addressBytes.Length); + context.Writer.Write((byte)addressBytes.Length); context.Writer.Write(addressBytes); - context.Writer.Write((ushort) address.Port); + context.Writer.Write((ushort)address.Port); context.Writer.Write(client.Name); + context.Writer.Write(client.GameVersion); } } 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.Length); + context.Writer.Write((byte)messageType); + context.Writer.Write((byte)reader.Tag); + context.Writer.Write((int)reader.Length); context.Writer.Write(reader.Buffer, reader.Offset, reader.Length); } @@ -189,7 +217,7 @@ namespace Impostor.Server.Recorder var length = context.Stream.Position; context.Stream.Position = 0; - context.Writer.Write((int) length); + context.Writer.Write((int)length); context.Stream.Position = length; } diff --git a/src/Impostor.Server/Recorder/ServerReplayVersion.cs b/src/Impostor.Server/Recorder/ServerReplayVersion.cs new file mode 100644 index 0000000..648205b --- /dev/null +++ b/src/Impostor.Server/Recorder/ServerReplayVersion.cs @@ -0,0 +1,18 @@ +namespace Impostor.Server.Recorder +{ + /// + /// Version of the server replay data format. + /// + public enum ServerReplayVersion + { + /// + /// Initial version + /// + Initial = 1, + + /// + /// Latest version + /// + Latest = Initial, + } +} diff --git a/src/Impostor.Tools.ServerReplay/FakeDateTimeProvider.cs b/src/Impostor.Tools.ServerReplay/FakeDateTimeProvider.cs new file mode 100644 index 0000000..7bd4beb --- /dev/null +++ b/src/Impostor.Tools.ServerReplay/FakeDateTimeProvider.cs @@ -0,0 +1,10 @@ +using System; +using Impostor.Api; + +namespace Impostor.Tools.ServerReplay +{ + public class FakeDateTimeProvider : IDateTimeProvider + { + public DateTimeOffset UtcNow { get; set; } + } +} diff --git a/src/Impostor.Tools.ServerReplay/Program.cs b/src/Impostor.Tools.ServerReplay/Program.cs index c36ed24..261d679 100644 --- a/src/Impostor.Tools.ServerReplay/Program.cs +++ b/src/Impostor.Tools.ServerReplay/Program.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Net; using System.Threading.Tasks; +using Impostor.Api; using Impostor.Api.Events.Managers; using Impostor.Api.Games; using Impostor.Api.Games.Managers; @@ -42,6 +43,7 @@ namespace Impostor.Tools.ServerReplay private static MockGameCodeFactory _gameCodeFactory; private static ClientManager _clientManager; private static GameManager _gameManager; + private static FakeDateTimeProvider _fakeDateTimeProvider; private static async Task Main(string[] args) { @@ -53,7 +55,7 @@ namespace Impostor.Tools.ServerReplay var stopwatch = Stopwatch.StartNew(); - foreach (var file in Directory.GetFiles(args[0])) + foreach (var file in Directory.GetFiles(args[0], "*.dat")) { // Clear. Connections.Clear(); @@ -67,6 +69,7 @@ namespace Impostor.Tools.ServerReplay _gameCodeFactory = _serviceProvider.GetRequiredService(); _clientManager = _serviceProvider.GetRequiredService(); _gameManager = _serviceProvider.GetRequiredService(); + _fakeDateTimeProvider = _serviceProvider.GetRequiredService(); await using (var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var reader = new BinaryReader(stream)) @@ -89,6 +92,9 @@ namespace Impostor.Tools.ServerReplay IsReplay = true }); + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); + services.AddLogging(builder => { builder.ClearProviders(); @@ -114,6 +120,17 @@ namespace Impostor.Tools.ServerReplay private static async Task ParseSession(BinaryReader reader) { + var protocolVersion = (ServerReplayVersion)reader.ReadUInt32(); + if (protocolVersion < ServerReplayVersion.Initial || protocolVersion > ServerReplayVersion.Latest) + { + throw new NotSupportedException("Session's protocol version is unsupported"); + } + + var startTime = _fakeDateTimeProvider.UtcNow = DateTimeOffset.FromUnixTimeMilliseconds(reader.ReadInt64()); + var serverVersion = reader.ReadString(); + + Logger.Information("Loaded session (server: {ServerVersion}, recorded at {StartTime})", serverVersion, startTime); + while (reader.BaseStream.Position < reader.BaseStream.Length) { var dataLength = reader.ReadInt32(); @@ -122,6 +139,7 @@ namespace Impostor.Tools.ServerReplay await using (var stream = new MemoryStream(data)) using (var readerInner = new BinaryReader(stream)) { + _fakeDateTimeProvider.UtcNow = startTime + TimeSpan.FromMilliseconds(readerInner.ReadUInt32()); await ParsePacket(readerInner); } } @@ -143,11 +161,12 @@ namespace Impostor.Tools.ServerReplay var addressPort = reader.ReadUInt16(); var address = new IPEndPoint(new IPAddress(addressBytes), addressPort); var name = reader.ReadString(); + var gameVersion = reader.ReadInt32(); // Create and register connection. var connection = new MockHazelConnection(address); - await _clientManager.RegisterConnectionAsync(connection, name, 50516550, null); + await _clientManager.RegisterConnectionAsync(connection, name, gameVersion, null); // Store reference for ourselfs. Connections.Add(clientId, connection); diff --git a/src/Impostor.Tools.ServerReplay/sessions/.gitkeep b/src/Impostor.Tools.ServerReplay/sessions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat b/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat deleted file mode 100644 index e5f441bb7c3890f6485d04b86bc1a77419906a43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47057 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 -- 2.39.5