<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>
+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;
}
}
}
/// <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" />.
--- /dev/null
+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);
+ }
+ }
+ }
+}
using System.Numerics;
using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
using Impostor.Api.Net.Inner;
using Impostor.Api.Unity;
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
{
using System.Numerics;
using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
using Impostor.Api.Net.Inner;
using Impostor.Api.Unity;
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);
{
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)
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);
}
}
}
{
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)
{
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();
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;
{
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;
public PlatformSpecificData PlatformSpecificData { get; }
- public int GameVersion { get; }
+ public GameVersion GameVersion { get; }
public IHazelConnection Connection { get; }
_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;
{
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);
}
}
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;
{
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()
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(),
};
&& _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;
- }
}
}
--- /dev/null
+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;
+ }
+}
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;
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;
_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);
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;
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)))
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);
}
}
.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)
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;
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;
GameFilterOptions filterOptions,
ClientManager clientManager,
IEventManager eventManager,
+ ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig)
{
FilterOptions = filterOptions;
_clientManager = clientManager;
_eventManager = eventManager;
+ _compatibilityManager = compatibilityManager;
_compatibilityConfig = compatibilityConfig.Value;
_timeoutConfig = timeoutConfig.Value;
Items = new ConcurrentDictionary<object, object>();
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>());
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;
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);
}
}
--- /dev/null
+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)));
+ }
+ }
+}
--- /dev/null
+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);
+ }
+}
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);
<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" />