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
# warning RS0037: PublicAPI.txt is missing '#nullable enable'
dotnet_diagnostic.RS0037.severity = none
+
+# ReSharper properties
+resharper_trailing_comma_in_multiline_lists = true
--- /dev/null
+using System;
+
+namespace Impostor.Api
+{
+ public interface IDateTimeProvider
+ {
+ DateTimeOffset UtcNow { get; }
+ }
+}
private readonly ClientManager _clientManager;
private readonly GameManager _gameManager;
- public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection, ISet<Mod> mods)
- : base(name, connection, mods)
+ public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, int gameVersion, IHazelConnection connection, ISet<Mod> mods)
+ : base(name, gameVersion, connection, mods)
{
_logger = logger;
_antiCheatConfig = antiCheatOptions.Value;
{
internal abstract class ClientBase : IClient
{
- protected ClientBase(string name, IHazelConnection connection, ISet<Mod> mods)
+ protected ClientBase(string name, int gameVersion, IHazelConnection connection, ISet<Mod> mods)
{
Name = name;
+ GameVersion = gameVersion;
Connection = connection;
Mods = mods;
Items = new ConcurrentDictionary<object, object>();
public string Name { get; }
+ public int GameVersion { get; }
+
public ISet<Mod> Mods { get; }
public Dictionary<int, string> ModIdMap { get; }
public ClientBase Create(IHazelConnection connection, string name, int clientVersion, ISet<Mod> mods)
{
- var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection, mods);
+ var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, clientVersion, connection, mods);
connection.Client = client;
return client;
}
}
-}
\ No newline at end of file
+}
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;
private readonly ILogger<InnerPlayerControl> _logger;
private readonly IEventManager _eventManager;
private readonly Game _game;
- private readonly ServerEnvironment _serverEnvironment;
+ private readonly IDateTimeProvider _dateTimeProvider;
- public InnerPlayerControl(ILogger<InnerPlayerControl> logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game, ServerEnvironment serverEnvironment)
+ public InnerPlayerControl(ILogger<InnerPlayerControl> logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game, IDateTimeProvider dateTimeProvider)
{
_logger = logger;
_eventManager = eventManager;
_game = game;
- _serverEnvironment = serverEnvironment;
+ _dateTimeProvider = dateTimeProvider;
Physics = ActivatorUtilities.CreateInstance<InnerPlayerPhysics>(serviceProvider, this, _eventManager, _game);
NetworkTransform = ActivatorUtilities.CreateInstance<InnerCustomNetworkTransform>(serviceProvider, this, _game);
private async ValueTask<bool> 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"))
{
}
}
- PlayerInfo.LastMurder = DateTimeOffset.UtcNow;
+ PlayerInfo.LastMurder = _dateTimeProvider.UtcNow;
if (!target.PlayerInfo.IsDead)
{
using System;
using System.Collections.Generic;
+using Impostor.Api;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
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)
public ClientRedirector(
string name,
+ int gameVersion,
HazelConnection connection,
ISet<Mod> mods,
ClientManager clientManager,
INodeProvider nodeProvider,
INodeLocator nodeLocator)
- : base(name, connection, mods)
+ : base(name, gameVersion, connection, mods)
{
_clientManager = clientManager;
_nodeProvider = nodeProvider;
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;
.Get<AnnouncementsServerConfig>() ?? new AnnouncementsServerConfig();
services.AddSingleton<ServerEnvironment>();
+ services.AddSingleton<IDateTimeProvider, RealDateTimeProvider>();
services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section));
services.Configure<AntiCheatConfig>(host.Configuration.GetSection(AntiCheatConfig.Section));
--- /dev/null
+using System;
+using Impostor.Api;
+
+namespace Impostor.Server
+{
+ public class RealDateTimeProvider : IDateTimeProvider
+ {
+ public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
+ }
+}
private bool _createdGame;
private bool _recordAfter;
- public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, ISet<Mod> mods, PacketRecorder recorder)
- : base(logger, antiCheatOptions, clientManager, gameManager, name, connection, mods)
+ public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, int gameVersion, HazelConnection connection, ISet<Mod> mods, PacketRecorder recorder)
+ : base(logger, antiCheatOptions, clientManager, gameManager, name, gameVersion, connection, mods)
{
_recorder = recorder;
_isFirst = true;
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;
private readonly ILogger<PacketRecorder> _logger;
private readonly ObjectPool<PacketSerializationContext> _pool;
private readonly Channel<byte[]> _channel;
+ private DateTimeOffset _startTime;
public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool)
{
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
{
try
{
- WriteHeader(context, RecordedPacketType.Connect);
+ WritePacketHeader(context, RecordedPacketType.Connect);
WriteClient(context, client, true);
WriteLength(context);
try
{
- WriteHeader(context, RecordedPacketType.Disconnect);
+ WritePacketHeader(context, RecordedPacketType.Disconnect);
WriteClient(context, client, false);
context.Writer.Write(reason);
WriteLength(context);
try
{
- WriteHeader(context, RecordedPacketType.Message);
+ WritePacketHeader(context, RecordedPacketType.Message);
WriteClient(context, client, false);
WritePacket(context, reader, messageType);
WriteLength(context);
try
{
- WriteHeader(context, RecordedPacketType.GameCreated);
+ WritePacketHeader(context, RecordedPacketType.GameCreated);
WriteClient(context, client, false);
WriteGameCode(context, gameCode);
WriteLength(context);
}
}
- 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)
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);
}
var length = context.Stream.Position;
context.Stream.Position = 0;
- context.Writer.Write((int) length);
+ context.Writer.Write((int)length);
context.Stream.Position = length;
}
--- /dev/null
+namespace Impostor.Server.Recorder
+{
+ /// <summary>
+ /// Version of the server replay data format.
+ /// </summary>
+ public enum ServerReplayVersion
+ {
+ /// <summary>
+ /// Initial version
+ /// </summary>
+ Initial = 1,
+
+ /// <summary>
+ /// Latest version
+ /// </summary>
+ Latest = Initial,
+ }
+}
--- /dev/null
+using System;
+using Impostor.Api;
+
+namespace Impostor.Tools.ServerReplay
+{
+ public class FakeDateTimeProvider : IDateTimeProvider
+ {
+ public DateTimeOffset UtcNow { get; set; }
+ }
+}
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;
private static MockGameCodeFactory _gameCodeFactory;
private static ClientManager _clientManager;
private static GameManager _gameManager;
+ private static FakeDateTimeProvider _fakeDateTimeProvider;
private static async Task Main(string[] args)
{
var stopwatch = Stopwatch.StartNew();
- foreach (var file in Directory.GetFiles(args[0]))
+ foreach (var file in Directory.GetFiles(args[0], "*.dat"))
{
// Clear.
Connections.Clear();
_gameCodeFactory = _serviceProvider.GetRequiredService<MockGameCodeFactory>();
_clientManager = _serviceProvider.GetRequiredService<ClientManager>();
_gameManager = _serviceProvider.GetRequiredService<GameManager>();
+ _fakeDateTimeProvider = _serviceProvider.GetRequiredService<FakeDateTimeProvider>();
await using (var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var reader = new BinaryReader(stream))
IsReplay = true
});
+ services.AddSingleton<FakeDateTimeProvider>();
+ services.AddSingleton<IDateTimeProvider>(p => p.GetRequiredService<FakeDateTimeProvider>());
+
services.AddLogging(builder =>
{
builder.ClearProviders();
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();
await using (var stream = new MemoryStream(data))
using (var readerInner = new BinaryReader(stream))
{
+ _fakeDateTimeProvider.UtcNow = startTime + TimeSpan.FromMilliseconds(readerInner.ReadUInt32());
await ParsePacket(readerInner);
}
}
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);