]> git.deb.at Git - rhonda/impostor.git/commitdiff
Improve cross-compatibility (#534)
authorminiduikboot <mini@duikbo.at>
Fri, 6 Oct 2023 12:52:16 +0000 (14:52 +0200)
committerGitHub <noreply@github.com>
Fri, 6 Oct 2023 12:52:16 +0000 (14:52 +0200)
Co-authored-by: js6pak <me@6pak.dev>
25 files changed:
src/Directory.Build.props
src/Impostor.Api/Innersloth/GameVersion.cs
src/Impostor.Api/Net/IClient.cs
src/Impostor.Api/Net/Manager/ICompatibilityManager.cs [new file with mode: 0644]
src/Impostor.Api/Net/MessageReaderExtensions.cs
src/Impostor.Api/Net/MessageWriterExtensions.cs
src/Impostor.Api/Net/Messages/C2S/HandshakeC2S.cs
src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs
src/Impostor.Server/Net/Client.cs
src/Impostor.Server/Net/ClientBase.cs
src/Impostor.Server/Net/Factories/ClientFactory.cs
src/Impostor.Server/Net/Factories/IClientFactory.cs
src/Impostor.Server/Net/Manager/ClientManager.cs
src/Impostor.Server/Net/Manager/CompatibilityManager.cs [new file with mode: 0644]
src/Impostor.Server/Net/Manager/GameManager.cs
src/Impostor.Server/Net/State/Game.Incoming.cs
src/Impostor.Server/Net/State/Game.Outgoing.cs
src/Impostor.Server/Net/State/Game.cs
src/Impostor.Server/Program.cs
src/Impostor.Server/Recorder/ClientRecorder.cs
src/Impostor.Server/Recorder/PacketRecorder.cs
src/Impostor.Tests/CompatibilityManagerTests.cs [new file with mode: 0644]
src/Impostor.Tests/GameVersionTests.cs [new file with mode: 0644]
src/Impostor.Tools.ServerReplay/Program.cs
src/ProjectRules.ruleset

index 8f454b3ba7680be827e6bac6ca116e658a052868..e348a77fac7656fb02a2024505ca2a4307db6d99 100644 (file)
@@ -2,7 +2,7 @@
   <PropertyGroup>
     <TargetFramework>net7.0</TargetFramework>
     <LangVersion>11</LangVersion>
-    <VersionPrefix>1.8.4</VersionPrefix>
+    <VersionPrefix>1.9.0</VersionPrefix>
     <VersionSuffix>dev</VersionSuffix>
     <EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
     <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
index 5df8deaf0117998c16cf020ed51e263b2a1cf721..3c10804e53847ad60c112be00d86b86355a878b9 100644 (file)
+using System;
+using System.Numerics;
+
 namespace Impostor.Api.Innersloth
 {
-    public static class GameVersion
+    public readonly struct GameVersion : IEquatable<GameVersion>, IComparable<GameVersion>, IComparisonOperators<GameVersion, GameVersion, bool>
     {
-        public static int GetVersion(int year, int month, int day, int revision = 0)
+        private const int YearMask = 25000;
+        private const int MonthMask = 1800;
+        private const int DayMask = 50;
+
+        private const int DisableServerAuthorityFlag = 25;
+
+        public GameVersion(int value)
+        {
+            Value = value;
+        }
+
+        public GameVersion(int year, int month, int day, int revision = 0)
+        {
+            Value = (year * YearMask) + (month * MonthMask) + (day * DayMask) + revision;
+        }
+
+        public int Value { get; }
+
+        public int Year => Value / YearMask;
+
+        public int Month => (Value % YearMask) / MonthMask;
+
+        public int Day => ((Value % YearMask) % MonthMask) / DayMask;
+
+        public int Revision => Value % DayMask;
+
+        /// <summary>
+        /// Gets a value indicating whether the DisableServerAuthority flag is present.
+        /// </summary>
+        public bool HasDisableServerAuthorityFlag
+        {
+            get
+            {
+                return Revision >= DisableServerAuthorityFlag;
+            }
+        }
+
+        public static bool operator ==(GameVersion left, GameVersion right) => left.Value == right.Value;
+
+        public static bool operator !=(GameVersion left, GameVersion right) => left.Value != right.Value;
+
+        public static bool operator >(GameVersion left, GameVersion right) => left.Value > right.Value;
+
+        public static bool operator >=(GameVersion left, GameVersion right) => left.Value >= right.Value;
+
+        public static bool operator <(GameVersion left, GameVersion right) => left.Value < right.Value;
+
+        public static bool operator <=(GameVersion left, GameVersion right) => left.Value <= right.Value;
+
+        public void GetComponents(out int year, out int month, out int day, out int revision)
+        {
+            var value = Value;
+            year = value / YearMask;
+            value %= YearMask;
+            month = value / MonthMask;
+            value %= MonthMask;
+            day = value / DayMask;
+            revision = value % DayMask;
+        }
+
+        /// <summary>
+        /// Normalizes this game version by removing all the special flags.
+        /// </summary>
+        /// <returns>This GameVersion but stripped of special flags.</returns>
+        public GameVersion Normalize()
+        {
+            return HasDisableServerAuthorityFlag ? new GameVersion(Value - DisableServerAuthorityFlag) : this;
+        }
+
+        public override string ToString()
+        {
+            GetComponents(out var year, out var month, out var day, out var revision);
+            return $"{year}.{month}.{day}{(revision == 0 ? string.Empty : "." + revision)}";
+        }
+
+        public int CompareTo(GameVersion other)
+        {
+            return Value.CompareTo(other.Value);
+        }
+
+        public bool Equals(GameVersion other)
+        {
+            return Value == other.Value;
+        }
+
+        public override bool Equals(object? obj)
         {
-            return (year * 25000) + (month * 1800) + (day * 50) + revision;
+            return obj is GameVersion other && Equals(other);
         }
 
-        public static void ParseVersion(int version, out int year, out int month, out int day, out int revision)
+        public override int GetHashCode()
         {
-            year = version / 25000;
-            version %= 25000;
-            month = version / 1800;
-            version %= 1800;
-            day = version / 50;
-            revision = version % 50;
+            return Value;
         }
     }
 }
index 09b20bea26e085069f55cdcb7c6a703c250cdabb..74309b2f13ef7bcb53ced3866515ed7ee833dd78 100644 (file)
@@ -67,7 +67,7 @@ namespace Impostor.Api.Net
         /// <summary>
         /// Gets the version of the game the client is using.
         /// </summary>
-        int GameVersion { get; }
+        GameVersion GameVersion { get; }
 
         /// <summary>
         /// Gets platform specific data of the <see cref="IClient" />.
diff --git a/src/Impostor.Api/Net/Manager/ICompatibilityManager.cs b/src/Impostor.Api/Net/Manager/ICompatibilityManager.cs
new file mode 100644 (file)
index 0000000..27941ee
--- /dev/null
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.Linq;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Manager
+{
+    /// <summary>
+    /// Maintains an internal compatibility list of versions that are allowed to connect to the server, and which game
+    /// versions they are allowed to play with.
+    /// </summary>
+    public interface ICompatibilityManager
+    {
+        public enum VersionCompareResult
+        {
+            Compatible,
+            ClientTooOld,
+            ServerTooOld,
+            Unknown,
+        }
+
+        /// <summary>
+        /// Gets the compatibility groups.
+        /// </summary>
+        public IEnumerable<CompatibilityGroup> CompatibilityGroups { get; }
+
+        /// <summary>
+        /// Check if a client can join the server according to the currently accepted game versions.
+        /// </summary>
+        /// <param name="clientVersion">The client version to check for.</param>
+        /// <returns>
+        /// Whether this version is supported by the server at the moment and if not, whether it is too old or too new.
+        /// </returns>
+        public VersionCompareResult CanConnectToServer(GameVersion clientVersion);
+
+        /// <summary>Check if a player can join an existing game.</summary>
+        /// <param name="hostVersion">The client version of the host.</param>
+        /// <param name="clientVersion">The client version of the player that is joining.</param>
+        /// <returns>
+        /// <list type="bullet">
+        ///   <item><see cref="GameJoinError.None"/> if everything is OK.</item>
+        ///   <item><see cref="GameJoinError.ClientOutdated"/> if the player runs a too old game version.</item>
+        ///   <item><see cref="GameJoinError.ClientTooNew"/> if the player runs a too new game version.</item>
+        /// </list>
+        /// </returns>
+        public GameJoinError CanJoinGame(GameVersion hostVersion, GameVersion clientVersion);
+
+        /// <summary>
+        /// Add a new compatibility group.
+        ///
+        /// WARNING: this method does not magically make changes to Impostor to properly support these versions. If
+        /// Impostor cannot support the game versions you're trying to add or that the game versions you're making
+        /// compatible do not crossplay correctly, weird behaviour may occur. Here be dragons.
+        /// </summary>
+        /// <param name="compatibilityGroup">The compatibility group to add.</param>
+        public void AddCompatibilityGroup(CompatibilityGroup compatibilityGroup);
+
+        /// <summary>
+        /// Add a supported game version to the specified compatibility group.
+        ///
+        /// WARNING: this method does not magically make changes to Impostor to properly support these versions. If
+        /// Impostor cannot support the game versions you're trying to add or that the game versions you're making
+        /// compatible do not crossplay correctly, weird behaviour may occur. Here be dragons.
+        /// </summary>
+        /// <param name="compatibilityGroup">The compatibility group to add this version to.</param>
+        /// <param name="gameVersion">The game version to add.</param>
+        public void AddSupportedVersion(CompatibilityGroup compatibilityGroup, GameVersion gameVersion);
+
+        /// <summary>
+        /// Remove a version from the internal version compatibility list.
+        /// </summary>
+        /// Note that this will not stop players currently connected to the server from playing, it will only stop new
+        /// connections.
+        /// <param name="removedVersion">The version to remove from the list.</param>
+        /// <returns>True iff this version was on the current compatibility list.</returns>
+        public bool RemoveSupportedVersion(GameVersion removedVersion);
+
+        public sealed class CompatibilityGroup
+        {
+            private readonly List<GameVersion> _gameVersions;
+
+            public CompatibilityGroup(IEnumerable<GameVersion> gameVersions)
+            {
+                _gameVersions = gameVersions.ToList();
+            }
+
+            public IReadOnlyList<GameVersion> GameVersions => _gameVersions;
+
+            public static implicit operator CompatibilityGroup(GameVersion[] gameVersions) => new(gameVersions);
+
+            internal void Add(GameVersion gameVersion)
+            {
+                _gameVersions.Add(gameVersion);
+            }
+
+            internal bool Remove(GameVersion gameVersion)
+            {
+                return _gameVersions.Remove(gameVersion);
+            }
+        }
+    }
+}
index 1f77947d9a5e017de5baf72b61f2d7182835d7ef..023455515ec2b7ccab6de60bd21ceeb0dfd8e687 100644 (file)
@@ -1,5 +1,6 @@
 using System.Numerics;
 using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
 using Impostor.Api.Net.Inner;
 using Impostor.Api.Unity;
 
@@ -7,6 +8,11 @@ namespace Impostor.Api.Net;
 
 public static class MessageReaderExtensions
 {
+    public static GameVersion ReadGameVersion(this IMessageReader reader)
+    {
+        return new GameVersion(reader.ReadInt32());
+    }
+
     public static T? ReadNetObject<T>(this IMessageReader reader, IGame game)
         where T : IInnerNetObject
     {
index ddf3ba7ee44baa4f2faa194adf54f723be41a4b6..f860ce5154ebe78d62e93e78cf079a5907f4208c 100644 (file)
@@ -1,5 +1,6 @@
 using System.Numerics;
 using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
 using Impostor.Api.Net.Inner;
 using Impostor.Api.Unity;
 
@@ -7,6 +8,11 @@ namespace Impostor.Api.Net;
 
 public static class MessageWriterExtensions
 {
+    public static void Write(this IMessageWriter writer, GameVersion value)
+    {
+        writer.Write(value.Value);
+    }
+
     public static void Serialize(this GameCode gameCode, IMessageWriter writer)
     {
         writer.Write(gameCode.Value);
index fb7283c3da9cbd0e69511fe52439c2ffb5d4fdf3..454fce91bf53bcedd9c54c496f91345018034a05 100644 (file)
@@ -4,9 +4,9 @@ namespace Impostor.Api.Net.Messages.C2S
 {
     public static class HandshakeC2S
     {
-        public static void Deserialize(IMessageReader reader, out int clientVersion, out string name, out Language language, out QuickChatModes chatMode, out PlatformSpecificData? platformSpecificData)
+        public static void Deserialize(IMessageReader reader, out GameVersion clientVersion, out string name, out Language language, out QuickChatModes chatMode, out PlatformSpecificData? platformSpecificData)
         {
-            clientVersion = reader.ReadInt32();
+            clientVersion = reader.ReadGameVersion();
             name = reader.ReadString();
 
             if (clientVersion >= Version.V1)
@@ -44,13 +44,13 @@ namespace Impostor.Api.Net.Messages.C2S
 
         private static class Version
         {
-            public static readonly int V1 = GameVersion.GetVersion(2021, 4, 25);
+            public static readonly GameVersion V1 = new(2021, 4, 25);
 
-            public static readonly int V2 = GameVersion.GetVersion(2021, 6, 30);
+            public static readonly GameVersion V2 = new(2021, 6, 30);
 
-            public static readonly int V3 = GameVersion.GetVersion(2021, 11, 9);
+            public static readonly GameVersion V3 = new(2021, 11, 9);
 
-            public static readonly int V4 = GameVersion.GetVersion(2021, 12, 14);
+            public static readonly GameVersion V4 = new(2021, 12, 14);
         }
     }
 }
index bba0e71837fbac19770d1d405f5dc391895c00e5..d5d3718314c407378473f83be5e545feb5646994 100644 (file)
@@ -4,7 +4,7 @@ namespace Impostor.Api.Net.Messages.S2C
 {
     public static class Message07JoinedGameS2C
     {
-        public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, IClientPlayer[] otherPlayers, bool post20220202 = true)
+        public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, IClientPlayer[] otherPlayers)
         {
             if (clear)
             {
@@ -24,12 +24,9 @@ namespace Impostor.Api.Net.Messages.S2C
                 ply.Client.PlatformSpecificData.Serialize(writer);
                 writer.WritePacked(ply.Character?.PlayerInfo.PlayerLevel ?? 1);
 
-                if (post20220202)
-                {
-                    // ProductUserId and FriendCode are not yet known, so set them to an empty string
-                    writer.Write(string.Empty);
-                    writer.Write(string.Empty);
-                }
+                // ProductUserId and FriendCode are not yet known, so set them to an empty string
+                writer.Write(string.Empty);
+                writer.Write(string.Empty);
             }
 
             writer.EndMessage();
index 44d609a2dccd447c947871dd4f43e1aab96d2d50..48d8ea35e038612ec84d89e80758cb2ee63f070c 100644 (file)
@@ -26,7 +26,7 @@ namespace Impostor.Server.Net
         private readonly GameManager _gameManager;
         private readonly ICustomMessageManager<ICustomRootMessage> _customMessageManager;
 
-        public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, ICustomMessageManager<ICustomRootMessage> customMessageManager, string name, int gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, IHazelConnection connection)
+        public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, ICustomMessageManager<ICustomRootMessage> customMessageManager, string name, GameVersion gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, IHazelConnection connection)
             : base(name, gameVersion, language, chatMode, platformSpecificData, connection)
         {
             _logger = logger;
index eb5b14d78b4f26a36cc2ea6efd62def28afdaa6f..d967bda5d3979356cabce3cca52f18f98a6ba5d7 100644 (file)
@@ -10,7 +10,7 @@ namespace Impostor.Server.Net
 {
     internal abstract class ClientBase : IClient
     {
-        protected ClientBase(string name, int gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, IHazelConnection connection)
+        protected ClientBase(string name, GameVersion gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, IHazelConnection connection)
         {
             Name = name;
             GameVersion = gameVersion;
@@ -31,7 +31,7 @@ namespace Impostor.Server.Net
 
         public PlatformSpecificData PlatformSpecificData { get; }
 
-        public int GameVersion { get; }
+        public GameVersion GameVersion { get; }
 
         public IHazelConnection Connection { get; }
 
index 5a7e66604712bc47c7811023d95c1003519d3b71..98e5e4a8a2cc68f62c72399fc15148cb470501b6 100644 (file)
@@ -15,7 +15,7 @@ namespace Impostor.Server.Net.Factories
             _serviceProvider = serviceProvider;
         }
 
-        public ClientBase Create(IHazelConnection connection, string name, int clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData)
+        public ClientBase Create(IHazelConnection connection, string name, GameVersion clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData)
         {
             var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, clientVersion, language, chatMode, platformSpecificData, connection);
             connection.Client = client;
index ccccb33fbaf460c2ec6549a7928f7640608ca5e5..adee6dcc122cc2bd56416f7677574cb30201f5f5 100644 (file)
@@ -5,6 +5,6 @@ namespace Impostor.Server.Net.Factories
 {
     internal interface IClientFactory
     {
-        ClientBase Create(IHazelConnection connection, string name, int clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData);
+        ClientBase Create(IHazelConnection connection, string name, GameVersion clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData);
     }
 }
index d3610f7da0748d5ed173168ef6febc9239f84d1e..195d5363c701320e8e3f1410e03001ecdd2e72e3 100644 (file)
@@ -1,13 +1,13 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Impostor.Api.Config;
 using Impostor.Api.Events.Managers;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Net;
+using Impostor.Api.Net.Manager;
 using Impostor.Hazel;
 using Impostor.Server.Events.Client;
 using Impostor.Server.Net.Factories;
@@ -18,42 +18,24 @@ namespace Impostor.Server.Net.Manager
 {
     internal partial class ClientManager
     {
-        // NOTE: when updating this array, keep the versions ordered from old to new, otherwise the version compare logic doesn't work properly
-        private static readonly int[] SupportedVersions =
-        {
-            GameVersion.GetVersion(2022, 11, 1), // 2022.12.8
-            GameVersion.GetVersion(2022, 11, 9), // 2022.12.14
-            GameVersion.GetVersion(2022, 12, 2), // 2023.2.28
-            GameVersion.GetVersion(2023, 1, 11), // 2023.3.28s
-            GameVersion.GetVersion(2023, 3, 13), // 2023.3.28a
-            GameVersion.GetVersion(2023, 4, 21), // 2023.6.13
-            GameVersion.GetVersion(2023, 5, 20), // 2023.7.11
-        };
-
         private readonly ILogger<ClientManager> _logger;
         private readonly IEventManager _eventManager;
         private readonly ConcurrentDictionary<int, ClientBase> _clients;
+        private readonly ICompatibilityManager _compatibilityManager;
         private readonly CompatibilityConfig _compatibilityConfig;
         private readonly IClientFactory _clientFactory;
         private int _idLast;
 
-        public ClientManager(ILogger<ClientManager> logger, IEventManager eventManager, IClientFactory clientFactory, IOptions<CompatibilityConfig> compatibilityConfig)
+        public ClientManager(ILogger<ClientManager> logger, IEventManager eventManager, IClientFactory clientFactory, ICompatibilityManager compatibilityManager, IOptions<CompatibilityConfig> compatibilityConfig)
         {
             _logger = logger;
             _eventManager = eventManager;
             _clientFactory = clientFactory;
             _clients = new ConcurrentDictionary<int, ClientBase>();
+            _compatibilityManager = compatibilityManager;
             _compatibilityConfig = compatibilityConfig.Value;
         }
 
-        private enum VersionCompareResult
-        {
-            Compatible,
-            ClientTooOld,
-            ServerTooOld,
-            Unknown,
-        }
-
         public IEnumerable<ClientBase> Clients => _clients.Values;
 
         public int NextId()
@@ -72,26 +54,24 @@ namespace Impostor.Server.Net.Manager
             return clientId;
         }
 
-        public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData? platformSpecificData)
+        public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, GameVersion clientVersion, Language language, QuickChatModes chatMode, PlatformSpecificData? platformSpecificData)
         {
-            var versionCompare = CompareVersion(clientVersion);
-            if (versionCompare == VersionCompareResult.ServerTooOld && _compatibilityConfig.AllowFutureGameVersions && platformSpecificData != null)
+            var versionCompare = _compatibilityManager.CanConnectToServer(clientVersion);
+            if (versionCompare == ICompatibilityManager.VersionCompareResult.ServerTooOld && _compatibilityConfig.AllowFutureGameVersions && platformSpecificData != null)
             {
-                GameVersion.ParseVersion(clientVersion, out var year, out var month, out var day, out var revision);
-                _logger.LogWarning("Client connected using future version: {clientVersion} ({version}). Unsupported, continue at your own risk.", clientVersion, $"{year}.{month}.{day}{(revision == 0 ? string.Empty : "." + revision)}");
+                _logger.LogWarning("Client connected using future version: {clientVersion} ({version}). Unsupported, continue at your own risk.", clientVersion.Value, clientVersion.ToString());
             }
-            else if (versionCompare != VersionCompareResult.Compatible || platformSpecificData == null)
+            else if (versionCompare != ICompatibilityManager.VersionCompareResult.Compatible || platformSpecificData == null)
             {
-                GameVersion.ParseVersion(clientVersion, out var year, out var month, out var day, out var revision);
-                _logger.LogTrace("Client connected using unsupported version: {clientVersion} ({version})", clientVersion, $"{year}.{month}.{day}{(revision == 0 ? string.Empty : "." + revision)}");
+                _logger.LogTrace("Client connected using unsupported version: {clientVersion} ({version})", clientVersion.Value, clientVersion.ToString());
 
                 using var packet = MessageWriter.Get(MessageType.Reliable);
 
                 var message = versionCompare switch
                 {
-                    VersionCompareResult.ClientTooOld => DisconnectMessages.VersionClientTooOld,
-                    VersionCompareResult.ServerTooOld => DisconnectMessages.VersionServerTooOld,
-                    VersionCompareResult.Unknown => DisconnectMessages.VersionUnsupported,
+                    ICompatibilityManager.VersionCompareResult.ClientTooOld => DisconnectMessages.VersionClientTooOld,
+                    ICompatibilityManager.VersionCompareResult.ServerTooOld => DisconnectMessages.VersionServerTooOld,
+                    ICompatibilityManager.VersionCompareResult.Unknown => DisconnectMessages.VersionUnsupported,
                     _ => throw new ArgumentOutOfRangeException(),
                 };
 
@@ -133,29 +113,5 @@ namespace Impostor.Server.Net.Manager
                    && _clients.TryGetValue(client.Id, out var registeredClient)
                    && ReferenceEquals(client, registeredClient);
         }
-
-        private VersionCompareResult CompareVersion(int clientVersion)
-        {
-            foreach (var serverVersion in SupportedVersions)
-            {
-                if (clientVersion == serverVersion)
-                {
-                    return VersionCompareResult.Compatible;
-                }
-            }
-
-            if (clientVersion < SupportedVersions[0])
-            {
-                return VersionCompareResult.ClientTooOld;
-            }
-
-            if (clientVersion > SupportedVersions.Last())
-            {
-                return VersionCompareResult.ServerTooOld;
-            }
-
-            // This may happen in the very rare case that version X is supported, X+2 is as well, but X+1 is not.
-            return VersionCompareResult.Unknown;
-        }
     }
 }
diff --git a/src/Impostor.Server/Net/Manager/CompatibilityManager.cs b/src/Impostor.Server/Net/Manager/CompatibilityManager.cs
new file mode 100644 (file)
index 0000000..30395d8
--- /dev/null
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Manager;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Manager;
+
+using CompatibilityGroup = ICompatibilityManager.CompatibilityGroup;
+using VersionCompareResult = ICompatibilityManager.VersionCompareResult;
+
+internal class CompatibilityManager : ICompatibilityManager
+{
+    private static readonly CompatibilityGroup[] DefaultSupportedVersions =
+    {
+        new[]
+        {
+            new GameVersion(2022, 11, 1), // 2022.12.8
+        },
+
+        new[]
+        {
+            new GameVersion(2022, 11, 9), // 2022.12.14
+        },
+
+        new[]
+        {
+            new GameVersion(2022, 12, 2), // 2023.2.28
+        },
+
+        new[]
+        {
+            new GameVersion(2023, 1, 11), // 2023.3.28s
+            new GameVersion(2023, 3, 13), // 2023.3.28a
+            new GameVersion(2023, 4, 21), // 2023.6.13
+        },
+
+        new[]
+        {
+            new GameVersion(2023, 5, 20), // 2023.7.11
+            new GameVersion(2222, 0, 0), // 2023.7.11 for host-only mods
+        },
+    };
+
+    private readonly List<CompatibilityGroup> _compatibilityGroups = new();
+    private readonly Dictionary<GameVersion, CompatibilityGroup> _supportMap = new();
+    private readonly ILogger<CompatibilityManager> _logger;
+    private GameVersion _lowestVersionSupported = new(int.MaxValue);
+    private GameVersion _highestVersionSupported = new(0);
+
+    public CompatibilityManager(ILogger<CompatibilityManager> logger) : this(logger, DefaultSupportedVersions)
+    {
+    }
+
+    internal CompatibilityManager(ILogger<CompatibilityManager> logger, IEnumerable<CompatibilityGroup> defaultSupportedVersions)
+    {
+        _logger = logger;
+
+        foreach (var compatibilityGroup in defaultSupportedVersions)
+        {
+            AddCompatibilityGroup(compatibilityGroup);
+        }
+    }
+
+    public IEnumerable<CompatibilityGroup> CompatibilityGroups => _compatibilityGroups;
+
+    private CompatibilityGroup? TryGetCompatibilityGroup(GameVersion clientVersion)
+    {
+        // Innersloth servers allow disabling server authority by incrementing the version revision by 25.
+        // We should allow crossplay between client versions with this flag set and those without.
+        clientVersion = clientVersion.Normalize();
+
+        if (_supportMap.TryGetValue(clientVersion, out var compatibilityGroup))
+        {
+            return compatibilityGroup;
+        }
+
+        return null;
+    }
+
+    public VersionCompareResult CanConnectToServer(GameVersion clientVersion)
+    {
+        if (this.TryGetCompatibilityGroup(clientVersion) != null)
+        {
+            return VersionCompareResult.Compatible;
+        }
+
+        if (clientVersion < _lowestVersionSupported)
+        {
+            return VersionCompareResult.ClientTooOld;
+        }
+
+        if (clientVersion > _highestVersionSupported)
+        {
+            return VersionCompareResult.ServerTooOld;
+        }
+
+        return VersionCompareResult.Unknown;
+    }
+
+    public GameJoinError CanJoinGame(GameVersion hostVersion, GameVersion clientVersion)
+    {
+        if (hostVersion == clientVersion)
+        {
+            // Optimize a common case: a player on version X should always be able to join version X
+            return GameJoinError.None;
+        }
+
+        var hostCompatGroup = this.TryGetCompatibilityGroup(hostVersion);
+        var playerCompatGroup = this.TryGetCompatibilityGroup(clientVersion);
+
+        if (hostCompatGroup == null || playerCompatGroup == null)
+        {
+            return GameJoinError.InvalidClient;
+        }
+
+        if (hostCompatGroup != playerCompatGroup)
+        {
+            return clientVersion < hostVersion
+                ? GameJoinError.ClientOutdated
+                : GameJoinError.ClientTooNew;
+        }
+
+        return GameJoinError.None;
+    }
+
+    void ICompatibilityManager.AddCompatibilityGroup(CompatibilityGroup compatibilityGroup)
+    {
+        _logger.LogWarning($"{nameof(AddCompatibilityGroup)} was called by a plugin, this can create unexpected issues. Please proceed carefully");
+
+        AddCompatibilityGroup(compatibilityGroup);
+    }
+
+    void ICompatibilityManager.AddSupportedVersion(CompatibilityGroup compatibilityGroup, GameVersion gameVersion)
+    {
+        _logger.LogWarning($"{nameof(AddSupportedVersion)} was called by a plugin, this can create unexpected issues. Please proceed carefully");
+
+        if (compatibilityGroup.GameVersions.Contains(gameVersion))
+        {
+            return;
+        }
+
+        AddSupportedVersion(compatibilityGroup, gameVersion, true);
+    }
+
+    private void AddCompatibilityGroup(CompatibilityGroup compatibilityGroup)
+    {
+        foreach (var gameVersion in compatibilityGroup.GameVersions)
+        {
+            if (_supportMap.ContainsKey(gameVersion))
+            {
+                throw new InvalidOperationException($"Can't add this compatibility group because one if its versions ({gameVersion}) is already added");
+            }
+        }
+
+        _compatibilityGroups.Add(compatibilityGroup);
+
+        foreach (var gameVersion in compatibilityGroup.GameVersions)
+        {
+            AddSupportedVersion(compatibilityGroup, gameVersion, false);
+        }
+    }
+
+    private void AddSupportedVersion(CompatibilityGroup compatibilityGroup, GameVersion gameVersion, bool addToGroup)
+    {
+        if (!_compatibilityGroups.Contains(compatibilityGroup))
+        {
+            throw new InvalidOperationException("You have to add the compatibility group first");
+        }
+
+        if (_supportMap.ContainsKey(gameVersion))
+        {
+            throw new InvalidOperationException("Can't add this game version because it's already in another compatibility group");
+        }
+
+        if (addToGroup)
+        {
+            compatibilityGroup.Add(gameVersion);
+        }
+
+        _supportMap.Add(gameVersion, compatibilityGroup);
+
+        // We special case the host-only 2023.7.11 here
+        // Ideally it should never have existed so remove once 2023.7.11 is unsupported TODO
+        var includeInSupportRange = gameVersion != new GameVersion(2222, 0, 0);
+
+        if (includeInSupportRange)
+        {
+            if (gameVersion < _lowestVersionSupported)
+            {
+                _lowestVersionSupported = gameVersion;
+            }
+
+            if (gameVersion > _highestVersionSupported)
+            {
+                _highestVersionSupported = gameVersion;
+            }
+        }
+    }
+
+    public bool RemoveSupportedVersion(GameVersion removedVersion)
+    {
+        if (_supportMap.Remove(removedVersion, out var compatibilityGroup))
+        {
+            if (!compatibilityGroup.Remove(removedVersion))
+            {
+                throw new InvalidOperationException("Removed the version from the support map but it was missing from it's compatibility group");
+            }
+
+            if (!compatibilityGroup.GameVersions.Any())
+            {
+                _compatibilityGroups.Remove(compatibilityGroup);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+}
index 1a9505834691aa4d0237fb39eed2140ad3536d07..40c8610ccf26d739021f9470804dab5dbad2da71 100644 (file)
@@ -12,6 +12,7 @@ using Impostor.Api.Games.Managers;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
+using Impostor.Api.Net.Manager;
 using Impostor.Server.Events;
 using Impostor.Server.Net.State;
 using Microsoft.Extensions.DependencyInjection;
@@ -29,8 +30,16 @@ namespace Impostor.Server.Net.Manager
         private readonly IServiceProvider _serviceProvider;
         private readonly IEventManager _eventManager;
         private readonly IGameCodeFactory _gameCodeFactory;
-
-        public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, IServiceProvider serviceProvider, IEventManager eventManager, IGameCodeFactory gameCodeFactory, IOptions<CompatibilityConfig> compatibilityConfig)
+        private readonly ICompatibilityManager _compatibilityManager;
+
+        public GameManager(
+            ILogger<GameManager> logger,
+            IOptions<ServerConfig> config,
+            IServiceProvider serviceProvider,
+            IEventManager eventManager,
+            IGameCodeFactory gameCodeFactory,
+            IOptions<CompatibilityConfig> compatibilityConfig,
+            ICompatibilityManager compatibilityManager)
         {
             _logger = logger;
             _serviceProvider = serviceProvider;
@@ -39,6 +48,7 @@ namespace Impostor.Server.Net.Manager
             _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.ResolvePublicIp()), config.Value.PublicPort);
             _games = new ConcurrentDictionary<int, Game>();
             _compatibilityConfig = compatibilityConfig.Value;
+            _compatibilityManager = compatibilityManager;
         }
 
         IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value);
@@ -51,7 +61,13 @@ namespace Impostor.Server.Net.Manager
             return game;
         }
 
-        public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int gameVersion, HashSet<string> filterTags, int count = 10)
+        public IEnumerable<Game> FindListings(
+            MapFlags map,
+            int impostorCount,
+            GameKeywords language,
+            GameVersion gameVersion,
+            HashSet<string> filterTags,
+            int count = 10)
         {
             var results = 0;
 
@@ -60,7 +76,8 @@ namespace Impostor.Server.Net.Manager
                 x.Value.IsPublic &&
                 x.Value.GameState == GameStates.NotStarted &&
                 x.Value.PlayerCount < x.Value.Options.MaxPlayers &&
-                (_compatibilityConfig.AllowVersionMixing || x.Value.Host == null || x.Value.Host.Client.GameVersion == gameVersion)))
+                (_compatibilityConfig.AllowVersionMixing || x.Value.Host == null ||
+                 this._compatibilityManager.CanJoinGame(x.Value.Host.Client.GameVersion, gameVersion) == GameJoinError.None)))
             {
                 // Check for options.
                 if (!map.HasFlag((MapFlags)(1 << (byte)game.Options.Map)))
index 95281cf987971006629c63e966321a1e0f257a89..3d637e4de337cea8d94f8eb07d6df48a055277a6 100644 (file)
@@ -156,13 +156,10 @@ namespace Impostor.Server.Net.State
             if (_compatibilityConfig.AllowVersionMixing == false &&
                 this.Host != null && client.GameVersion != this.Host.Client.GameVersion)
             {
-                if (client.GameVersion < this.Host.Client.GameVersion)
+                var versionCheckResult = _compatibilityManager.CanJoinGame(Host.Client.GameVersion, client.GameVersion);
+                if (versionCheckResult != GameJoinError.None)
                 {
-                    return GameJoinResult.FromError(GameJoinError.ClientOutdated);
-                }
-                else
-                {
-                    return GameJoinResult.FromError(GameJoinError.ClientTooNew);
+                    return GameJoinResult.FromError(versionCheckResult);
                 }
             }
 
index ddb794bae76a67cf2a462c31a4b0d62895b8e396..d2bbe06e5330556eb60a17a4a9a6eea7afc6d387 100644 (file)
@@ -83,8 +83,7 @@ namespace Impostor.Server.Net.State
                 .Select(x => x.Value)
                 .ToArray();
 
-            // TODO: clean up post20220202 when versions before it are no longer supported.
-            Message07JoinedGameS2C.Serialize(message, clear, Code, player.Client.Id, HostId, players, player.Client.GameVersion >= GameVersion.GetVersion(2022, 2, 2));
+            Message07JoinedGameS2C.Serialize(message, clear, Code, player.Client.Id, HostId, players);
         }
 
         private void WriteAlterGameMessage(IMessageWriter message, bool clear, bool isPublic)
index b342b42c2d05a2be1e5657b7f8eb2098e54d962e..a83920cd4e0593e700a95823891c11c4abf46d35 100644 (file)
@@ -12,6 +12,7 @@ using Impostor.Api.Games;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
+using Impostor.Api.Net.Manager;
 using Impostor.Api.Net.Messages.S2C;
 using Impostor.Server.Events;
 using Impostor.Server.Net.Manager;
@@ -29,6 +30,7 @@ namespace Impostor.Server.Net.State
         private readonly ConcurrentDictionary<int, ClientPlayer> _players;
         private readonly HashSet<IPAddress> _bannedIps;
         private readonly IEventManager _eventManager;
+        private readonly ICompatibilityManager _compatibilityManager;
         private readonly CompatibilityConfig _compatibilityConfig;
         private readonly TimeoutConfig _timeoutConfig;
 
@@ -42,6 +44,7 @@ namespace Impostor.Server.Net.State
             GameFilterOptions filterOptions,
             ClientManager clientManager,
             IEventManager eventManager,
+            ICompatibilityManager compatibilityManager,
             IOptions<CompatibilityConfig> compatibilityConfig,
             IOptions<TimeoutConfig> timeoutConfig)
         {
@@ -60,6 +63,7 @@ namespace Impostor.Server.Net.State
             FilterOptions = filterOptions;
             _clientManager = clientManager;
             _eventManager = eventManager;
+            _compatibilityManager = compatibilityManager;
             _compatibilityConfig = compatibilityConfig.Value;
             _timeoutConfig = timeoutConfig.Value;
             Items = new ConcurrentDictionary<object, object>();
index abd4425cd327d433f312a31d556c5025536581d1..584629f27b4ba1be3dc37b0443eedcc2067186ab 100644 (file)
@@ -101,6 +101,7 @@ namespace Impostor.Server
                     services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
                     services.Configure<TimeoutConfig>(host.Configuration.GetSection(TimeoutConfig.Section));
 
+                    services.AddSingleton<ICompatibilityManager, CompatibilityManager>();
                     services.AddSingleton<ClientManager>();
                     services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>());
 
index 3029652e0b6a6308393440b357611a7eedd90a79..4671dd3f7b0f7230cdfe1b3cef54596a556fc09b 100644 (file)
@@ -18,7 +18,7 @@ namespace Impostor.Server.Recorder
         private bool _createdGame;
         private bool _recordAfter;
 
-        public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, ICustomMessageManager<ICustomRootMessage> customMessageManager, GameManager gameManager, string name, int gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, HazelConnection connection, PacketRecorder recorder)
+        public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, ICustomMessageManager<ICustomRootMessage> customMessageManager, GameManager gameManager, string name, GameVersion gameVersion, Language language, QuickChatModes chatMode, PlatformSpecificData platformSpecificData, HazelConnection connection, PacketRecorder recorder)
             : base(logger, antiCheatOptions, clientManager, gameManager, customMessageManager, name, gameVersion, language, chatMode, platformSpecificData, connection)
         {
             _recorder = recorder;
index f49e4b089c2748bb64277a64821a63e3bc68a853..2f8bb1ac15c5a4df593d579c6b4101c37d433d2b 100644 (file)
@@ -194,7 +194,7 @@ namespace Impostor.Server.Recorder
                 context.Writer.Write(addressBytes);
                 context.Writer.Write((ushort)address.Port);
                 context.Writer.Write(client.Name);
-                context.Writer.Write(client.GameVersion);
+                context.Writer.Write(client.GameVersion.Value);
             }
         }
 
diff --git a/src/Impostor.Tests/CompatibilityManagerTests.cs b/src/Impostor.Tests/CompatibilityManagerTests.cs
new file mode 100644 (file)
index 0000000..20cb776
--- /dev/null
@@ -0,0 +1,117 @@
+using System.Collections.Generic;
+using System.Linq;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Manager;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Impostor.Tests;
+
+using CompatibilityGroup = ICompatibilityManager.CompatibilityGroup;
+using VersionCompareResult = ICompatibilityManager.VersionCompareResult;
+
+public sealed class CompatibilityManagerTests
+{
+    private static readonly CompatibilityGroup[] DefaultSupportedVersions =
+    {
+        new[]
+        {
+            new GameVersion(1, 0, 0),
+        },
+
+        new[]
+        {
+            new GameVersion(2, 0, 0),
+            new GameVersion(2, 1, 0),
+        },
+    };
+
+    private readonly CompatibilityManager _compatibilityManager = new(NullLogger<CompatibilityManager>.Instance, DefaultSupportedVersions);
+
+    public static IEnumerable<object[]> CanConnectToServerData =>
+        new List<object[]>
+        {
+            new object[] { VersionCompareResult.ClientTooOld, new GameVersion(0, 0, 0) },
+            new object[] { VersionCompareResult.ServerTooOld, new GameVersion(100, 0, 0) },
+            new object[] { VersionCompareResult.Unknown, new GameVersion(2, 0, 1) },
+        };
+
+    [Theory]
+    [MemberData(nameof(CanConnectToServerData))]
+    public void CanConnectToServer(VersionCompareResult versionCompareResult, GameVersion gameVersion)
+    {
+        Assert.Equal(versionCompareResult, _compatibilityManager.CanConnectToServer(gameVersion));
+    }
+
+    public static IEnumerable<object[]> CanJoinGameData =>
+        new List<object[]>
+        {
+            new object[] { GameJoinError.None, new GameVersion(1, 0, 0), new GameVersion(1, 0, 0) },
+            new object[] { GameJoinError.InvalidClient, new GameVersion(1, 0, 0), new GameVersion(100, 0, 0) },
+            new object[] { GameJoinError.ClientOutdated, new GameVersion(2, 0, 0), new GameVersion(1, 0, 0) },
+            new object[] { GameJoinError.ClientTooNew, new GameVersion(1, 0, 0), new GameVersion(2, 0, 0) },
+            new object[] { GameJoinError.None, new GameVersion(2, 0, 0), new GameVersion(2, 1, 0) },
+        };
+
+    [Theory]
+    [MemberData(nameof(CanJoinGameData))]
+    public void CanJoinGame(GameJoinError gameJoinError, GameVersion hostVersion, GameVersion clientVersion)
+    {
+        Assert.Equal(gameJoinError, _compatibilityManager.CanJoinGame(hostVersion, clientVersion));
+    }
+
+    public static IEnumerable<object[]> CanConnectAndJoinData =>
+        new List<object[]>
+        {
+            new object[] { new GameVersion(1, 0, 0), new GameVersion(1, 0, 0) },
+            new object[] { new GameVersion(2, 0, 0), new GameVersion(2, 0, 0) },
+            new object[] { new GameVersion(2, 1, 0), new GameVersion(2, 0, 0) },
+            new object[] { new GameVersion(2, 0, 0), new GameVersion(2, 1, 0) },
+            new object[] { new GameVersion(2, 0, 0), new GameVersion(2, 0, 0, 25) }, // server authority flag
+        };
+
+    [Theory]
+    [MemberData(nameof(CanConnectAndJoinData))]
+    public void CanConnectAndJoin(GameVersion hostVersion, GameVersion clientVersion)
+    {
+        Assert.Equal(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(hostVersion));
+        Assert.Equal(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(clientVersion));
+
+        Assert.Equal(GameJoinError.None, _compatibilityManager.CanJoinGame(hostVersion, clientVersion));
+    }
+
+    [Fact]
+    public void PublicApi()
+    {
+        ICompatibilityManager compatibilityManager = _compatibilityManager;
+
+        {
+            Assert.NotEqual(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(2, 2, 0)));
+            Assert.NotEqual(GameJoinError.None, _compatibilityManager.CanJoinGame(new GameVersion(2, 2, 0), new GameVersion(2, 1, 0)));
+
+            var groupV2 = compatibilityManager.CompatibilityGroups.Single(g => g.GameVersions.Contains(new GameVersion(2, 0, 0)));
+            compatibilityManager.AddSupportedVersion(groupV2, new GameVersion(2, 2, 0));
+
+            Assert.Equal(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(2, 2, 0)));
+            Assert.Equal(GameJoinError.None, _compatibilityManager.CanJoinGame(new GameVersion(2, 2, 0), new GameVersion(2, 1, 0)));
+        }
+
+        {
+            Assert.NotEqual(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(3, 0, 0)));
+
+            compatibilityManager.AddCompatibilityGroup(new[] { new GameVersion(3, 0, 0), });
+
+            Assert.Equal(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(3, 0, 0)));
+        }
+
+        {
+            Assert.Equal(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(1, 0, 0)));
+
+            compatibilityManager.RemoveSupportedVersion(new GameVersion(1, 0, 0));
+
+            Assert.NotEqual(VersionCompareResult.Compatible, _compatibilityManager.CanConnectToServer(new GameVersion(1, 0, 0)));
+        }
+    }
+}
diff --git a/src/Impostor.Tests/GameVersionTests.cs b/src/Impostor.Tests/GameVersionTests.cs
new file mode 100644 (file)
index 0000000..8c62a8a
--- /dev/null
@@ -0,0 +1,28 @@
+using Impostor.Api.Innersloth;
+using Xunit;
+
+namespace Impostor.Tests;
+
+public sealed class GameVersionTests
+{
+    [Theory]
+    [InlineData(50588150, 2023, 7, 11)]
+    [InlineData(50588175, 2023, 7, 11, 25)]
+    public void Test(int value, int year, int month, int day, int revision = 0)
+    {
+        var gameVersion = new GameVersion(year, month, day, revision);
+
+        Assert.Equal(value, gameVersion.Value);
+
+        gameVersion.GetComponents(out var parsedYear, out var parsedMonth, out var parsedDay, out var parsedRevision);
+        Assert.Equal(year, parsedYear);
+        Assert.Equal(month, parsedMonth);
+        Assert.Equal(day, parsedDay);
+        Assert.Equal(revision, parsedRevision);
+
+        Assert.Equal(year, gameVersion.Year);
+        Assert.Equal(month, gameVersion.Month);
+        Assert.Equal(day, gameVersion.Day);
+        Assert.Equal(revision, gameVersion.Revision);
+    }
+}
index 7adeb145bbc8112c07c592f5ed009378c29d146f..ebfdd0810eb80903c0026d2cbb048194733b301c 100644 (file)
@@ -165,7 +165,7 @@ namespace Impostor.Tools.ServerReplay
                     var addressPort = reader.ReadUInt16();
                     var address = new IPEndPoint(new IPAddress(addressBytes), addressPort);
                     var name = reader.ReadString();
-                    var gameVersion = reader.ReadInt32();
+                    var gameVersion = new GameVersion(reader.ReadInt32());
 
                     // Create and register connection.
                     var connection = new MockHazelConnection(address);
index a50faddd756043e2e7cd95466600a50efc481a81..445d19b4cc89d066ffe7114bac63b7f4fe5b3485 100644 (file)
@@ -12,6 +12,7 @@
     <Rule Id="SA1101" Action="None" />
     <Rule Id="SA1111" Action="None" />
     <Rule Id="SA1128" Action="None" />
+    <Rule Id="SA1135" Action="None" />
   </Rules>
   <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
     <Rule Id="SA1309" Action="None" />