From: js6pak Date: Sun, 14 Mar 2021 20:50:55 +0000 (+0000) Subject: Add DateTime to server replays (#349) X-Git-Tag: v1.3.0~10^2 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=443168ef7163ac15111dd6da2d08c9f0f9f8cb86;p=rhonda%2Fimpostor.git Add DateTime to server replays (#349) --- 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 e5f441b..0000000 Binary files a/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat and /dev/null differ