]> git.deb.at Git - rhonda/impostor.git/commitdiff
Implement new GameOptions system
authorjs6pak <kubastaron@hotmail.com>
Sun, 11 Dec 2022 17:02:26 +0000 (18:02 +0100)
committerAeonLucid <aeonlucid@gmail.com>
Sun, 18 Dec 2022 21:45:56 +0000 (22:45 +0100)
Co-authored-by: miniduikboot <mini@duikbo.at>
36 files changed:
src/Impostor.Api/Games/IGame.cs
src/Impostor.Api/Games/Managers/IGameManager.cs
src/Impostor.Api/Innersloth/CrossplayFlags.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilterOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameModes.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/GameOptionsFactory.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/HideNSeekGameOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/NormalGameOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/EngineerRoleOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/GuardianAngelRoleOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/IRoleOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/LegacyRoleOptionsData.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleOptionsCollection.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleRate.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ScientistRoleOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ShapeshifterRoleOptions.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptionsData.cs [deleted file]
src/Impostor.Api/Innersloth/RoleOptionsData.cs [deleted file]
src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs
src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs
src/Impostor.Api/Net/Messages/Rpcs/Rpc02SyncSettings.cs
src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs
src/Impostor.Api/packages.lock.json
src/Impostor.Client.App/Program.cs
src/Impostor.Plugins.Example/ExamplePlugin.cs
src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs
src/Impostor.Server/Net/Client.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs
src/Impostor.Server/Net/Manager/GameManager.cs
src/Impostor.Server/Net/State/Game.Api.cs
src/Impostor.Server/Net/State/Game.cs
src/Impostor.Tools.ServerReplay/Program.cs

index f76382b12624d48e3a0ded3e8ce0c417e8912609..a10da63e161d372715a2b00b2533f36ded211b1a 100644 (file)
@@ -2,6 +2,7 @@
 using System.Net;
 using System.Threading.Tasks;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Inner;
 
@@ -9,7 +10,7 @@ namespace Impostor.Api.Games
 {
     public interface IGame
     {
-        GameOptionsData Options { get; }
+        IGameOptions Options { get; }
 
         GameCode Code { get; }
 
index f417d310892feefd86810cbd2d848fb16fef6db0..c418b729d1fbb32a68f9b8957e426505c09d9525 100644 (file)
@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
-using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 
 namespace Impostor.Api.Games.Managers
 {
@@ -16,6 +16,6 @@ namespace Impostor.Api.Games.Managers
         /// <param name="options">Game options.</param>
         /// <returns>Created game or null if creation was cancelled by a plugin.</returns>
         /// <exception cref="ImpostorException">Thrown when game creation failed.</exception>
-        ValueTask<IGame?> CreateAsync(GameOptionsData options);
+        ValueTask<IGame?> CreateAsync(IGameOptions options);
     }
 }
diff --git a/src/Impostor.Api/Innersloth/CrossplayFlags.cs b/src/Impostor.Api/Innersloth/CrossplayFlags.cs
new file mode 100644 (file)
index 0000000..1b251fe
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Innersloth;
+
+public enum CrossplayFlags
+{
+    Default = 4,
+    All = int.MaxValue,
+}
diff --git a/src/Impostor.Api/Innersloth/GameFilterOptions.cs b/src/Impostor.Api/Innersloth/GameFilterOptions.cs
new file mode 100644 (file)
index 0000000..e84cb52
--- /dev/null
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace Impostor.Api.Innersloth;
+
+public class GameFilterOptions
+{
+    public HashSet<string> FilterTags { get; } = new HashSet<string>();
+
+    public static GameFilterOptions Deserialize(IMessageReader reader)
+    {
+        var options = new GameFilterOptions();
+
+        var count = reader.ReadPackedInt32();
+        for (var i = 0; i < count; i++)
+        {
+            options.FilterTags.Add(reader.ReadString());
+        }
+
+        return options;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.WritePacked(FilterTags.Count);
+        foreach (var filterTag in FilterTags)
+        {
+            writer.Write(filterTag);
+        }
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameModes.cs b/src/Impostor.Api/Innersloth/GameModes.cs
new file mode 100644 (file)
index 0000000..0c55518
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Innersloth
+{
+    public enum GameModes : byte
+    {
+        None,
+        Normal,
+        HideNSeek,
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/GameOptionsFactory.cs b/src/Impostor.Api/Innersloth/GameOptions/GameOptionsFactory.cs
new file mode 100644 (file)
index 0000000..a7625aa
--- /dev/null
@@ -0,0 +1,74 @@
+using System;
+
+namespace Impostor.Api.Innersloth.GameOptions;
+
+public static class GameOptionsFactory
+{
+    public const byte ModularOptionsDataVersion = 7;
+
+    public static void Serialize(IMessageWriter writer, IGameOptions gameOptions)
+    {
+        if (gameOptions.Version < ModularOptionsDataVersion)
+        {
+            throw new NotSupportedException();
+        }
+
+        // In theory we should use WriteBytesAndSize here, but it's costly and requires computing the size
+        // Base game does similar hacks and completely ignores this value for modular game options so we can just write 0 here
+        writer.WritePacked(0);
+
+        writer.Write(gameOptions.Version);
+        writer.StartMessage(0);
+        writer.Write((byte)gameOptions.GameMode);
+        gameOptions.Serialize(writer);
+        writer.EndMessage();
+    }
+
+    public static IGameOptions Deserialize(IMessageReader reader)
+    {
+        reader.ReadPackedInt32();
+        var version = reader.ReadByte();
+
+        if (version < ModularOptionsDataVersion)
+        {
+            return LegacyGameOptionsData.Deserialize(reader, version);
+        }
+
+        var optionsReader = reader.ReadMessage();
+        var gameMode = (GameModes)optionsReader.ReadByte();
+
+        return gameMode switch
+        {
+            GameModes.Normal => NormalGameOptions.Deserialize(optionsReader, version),
+            GameModes.HideNSeek => HideNSeekGameOptions.Deserialize(optionsReader, version),
+            _ => throw new ArgumentOutOfRangeException(),
+        };
+    }
+
+    public static void DeserializeInto(IMessageReader reader, IGameOptions gameOptions)
+    {
+        reader.ReadPackedInt32();
+        var version = reader.ReadByte();
+
+        if (version < ModularOptionsDataVersion)
+        {
+            ((LegacyGameOptionsData)gameOptions).Deserialize(reader);
+            return;
+        }
+
+        var optionsReader = reader.ReadMessage();
+        var gameMode = (GameModes)optionsReader.ReadByte();
+
+        switch (gameMode)
+        {
+            case GameModes.Normal:
+                ((NormalGameOptions)gameOptions).Deserialize(optionsReader);
+                break;
+            case GameModes.HideNSeek:
+                ((HideNSeekGameOptions)gameOptions).Deserialize(optionsReader);
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(gameMode), gameMode, null);
+        }
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/HideNSeekGameOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/HideNSeekGameOptions.cs
new file mode 100644 (file)
index 0000000..6d061a9
--- /dev/null
@@ -0,0 +1,162 @@
+namespace Impostor.Api.Innersloth.GameOptions;
+
+public class HideNSeekGameOptions : IGameOptions
+{
+    public const int LatestVersion = 7;
+
+    public HideNSeekGameOptions(byte version = LatestVersion)
+    {
+        Version = version;
+        IGameOptions.EnsureVersionIsModular<HideNSeekGameOptions>(version);
+    }
+
+    /// <inheritdoc />
+    public byte Version { get; }
+
+    /// <inheritdoc />
+    public GameModes GameMode => GameModes.HideNSeek;
+
+    /// <inheritdoc />
+    public byte MaxPlayers { get; set; } = 15;
+
+    /// <inheritdoc />
+    public GameKeywords Keywords { get; set; } = GameKeywords.English;
+
+    /// <inheritdoc />
+    public MapTypes Map { get; set; } = MapTypes.Skeld;
+
+    /// <inheritdoc />
+    public int NumImpostors { get; set; } = 1;
+
+    /// <inheritdoc />
+    public bool IsDefaults { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets the Player speed modifier.
+    /// </summary>
+    public float PlayerSpeedMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are members of the crew as a multiplier value.
+    /// </summary>
+    public float CrewLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are Impostors as a multiplier value.
+    /// </summary>
+    public float ImpostorLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the number of common tasks.
+    /// </summary>
+    public int NumCommonTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of long tasks.
+    /// </summary>
+    public int NumLongTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of short tasks.
+    /// </summary>
+    public int NumShortTasks { get; set; } = 2;
+
+    public int CrewmateVentUses { get; set; } = 1;
+
+    public float CrewmateTimeInVent { get; set; } = 3f;
+
+    public float HidingTime { get; set; } = 200f;
+
+    public float CrewmateFlashlightSize { get; set; } = 0.35f;
+
+    public float ImpostorFlashlightSize { get; set; } = 0.25f;
+
+    public bool UseFlashlight { get; set; } = true;
+
+    public bool FinalHideSeekMap { get; set; } = true;
+
+    public float FinalHideTime { get; set; } = 50f;
+
+    public float FinalSeekerSpeed { get; set; } = 1.2f;
+
+    public bool FinalHidePings { get; set; } = true;
+
+    public bool ShowNames { get; set; } = true;
+
+    public uint SeekerPlayerId { get; set; } = 0xFFFFFFFF;
+
+    public float MaxPingTime { get; set; } = 6f;
+
+    public static HideNSeekGameOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new HideNSeekGameOptions(version);
+        options.Deserialize(reader);
+        return options;
+    }
+
+    public void Deserialize(IMessageReader reader)
+    {
+        MaxPlayers = reader.ReadByte();
+        Keywords = (GameKeywords)reader.ReadInt32();
+        Map = (MapTypes)reader.ReadByte();
+        PlayerSpeedMod = reader.ReadSingle();
+        CrewLightMod = reader.ReadSingle();
+        ImpostorLightMod = reader.ReadSingle();
+        NumCommonTasks = reader.ReadByte();
+        NumLongTasks = reader.ReadByte();
+        NumShortTasks = reader.ReadByte();
+        IsDefaults = reader.ReadBoolean();
+
+        CrewmateVentUses = reader.ReadInt32();
+        HidingTime = reader.ReadSingle();
+        CrewmateFlashlightSize = reader.ReadSingle();
+        ImpostorFlashlightSize = reader.ReadSingle();
+        UseFlashlight = reader.ReadBoolean();
+        FinalHideSeekMap = reader.ReadBoolean();
+        FinalHideTime = reader.ReadSingle();
+        FinalSeekerSpeed = reader.ReadSingle();
+        FinalHidePings = reader.ReadBoolean();
+        ShowNames = reader.ReadBoolean();
+        SeekerPlayerId = reader.ReadUInt32();
+        MaxPingTime = reader.ReadSingle();
+        CrewmateTimeInVent = reader.ReadSingle();
+
+        if (Version > 7)
+        {
+            IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
+        }
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(MaxPlayers);
+        writer.Write((uint)Keywords);
+        writer.Write((byte)Map);
+        writer.Write(PlayerSpeedMod);
+        writer.Write(CrewLightMod);
+        writer.Write(ImpostorLightMod);
+        writer.Write((byte)NumCommonTasks);
+        writer.Write((byte)NumLongTasks);
+        writer.Write((byte)NumShortTasks);
+        writer.Write(IsDefaults);
+
+        writer.Write(CrewmateVentUses);
+        writer.Write(HidingTime);
+        writer.Write(CrewmateFlashlightSize);
+        writer.Write(ImpostorFlashlightSize);
+        writer.Write(UseFlashlight);
+        writer.Write(FinalHideSeekMap);
+        writer.Write(FinalHideTime);
+        writer.Write(FinalSeekerSpeed);
+        writer.Write(FinalHidePings);
+        writer.Write(ShowNames);
+        writer.Write(SeekerPlayerId);
+        writer.Write(MaxPingTime);
+        writer.Write(CrewmateTimeInVent);
+
+        if (Version > 7)
+        {
+            IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
+        }
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs
new file mode 100644 (file)
index 0000000..10200c1
--- /dev/null
@@ -0,0 +1,57 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Impostor.Api.Innersloth.GameOptions;
+
+public interface IGameOptions
+{
+    /// <summary>
+    ///     Gets the version.
+    /// </summary>
+    public byte Version { get; }
+
+    /// <summary>
+    ///     Gets the currently active gamemode.
+    /// </summary>
+    public GameModes GameMode { get; }
+
+    /// <summary>
+    ///     Gets or sets the maximum amount of players for this lobby.
+    /// </summary>
+    public byte MaxPlayers { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the language of the lobby as per <see cref="GameKeywords" /> enum.
+    /// </summary>
+    public GameKeywords Keywords { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the Map selected for this lobby.
+    /// </summary>
+    public MapTypes Map { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of impostors for this lobby.
+    /// </summary>
+    public int NumImpostors { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the GameOptions are the default ones.
+    /// </summary>
+    public bool IsDefaults { get; set; }
+
+    public void Serialize(IMessageWriter writer);
+
+    public static void EnsureVersionIsModular<TCaller>(byte version)
+    {
+        if (version < GameOptionsFactory.ModularOptionsDataVersion)
+        {
+            throw new ImpostorException($"{typeof(TCaller).Name} didn't exist before version 7, did you mean {nameof(LegacyGameOptionsData)}?");
+        }
+    }
+
+    [DoesNotReturn]
+    public static void ThrowUnknownVersion<TCaller>(byte version)
+    {
+        throw new ImpostorException($"Unknown {typeof(TCaller).Name} version {version}");
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs b/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs
new file mode 100644 (file)
index 0000000..1ed7ecc
--- /dev/null
@@ -0,0 +1,263 @@
+using Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+namespace Impostor.Api.Innersloth.GameOptions;
+
+public class LegacyGameOptionsData : IGameOptions
+{
+    /// <summary>
+    ///     The latest major version of the game client.
+    /// </summary>
+    public const int LatestVersion = 5;
+
+    public LegacyGameOptionsData(byte version = LatestVersion)
+    {
+        Version = version;
+    }
+
+    /// <summary>
+    ///     Gets or sets host's version of the game.
+    /// </summary>
+    public byte Version { get; set; }
+
+    public GameModes GameMode => GameModes.Normal;
+
+    /// <summary>
+    ///     Gets or sets the maximum amount of players for this lobby.
+    /// </summary>
+    public byte MaxPlayers { get; set; } = 10;
+
+    /// <summary>
+    ///     Gets or sets the language of the lobby as per <see cref="GameKeywords" /> enum.
+    /// </summary>
+    public GameKeywords Keywords { get; set; } = GameKeywords.English;
+
+    /// <summary>
+    ///     Gets or sets the Map selected for this lobby.
+    /// </summary>
+    public MapTypes Map { get; set; } = MapTypes.Skeld;
+
+    /// <summary>
+    ///     Gets or sets the Player speed modifier.
+    /// </summary>
+    public float PlayerSpeedMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are members of the crew as a multiplier value.
+    /// </summary>
+    public float CrewLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are Impostors as a multiplier value.
+    /// </summary>
+    public float ImpostorLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Impostor cooldown to kill in seconds.
+    /// </summary>
+    public float KillCooldown { get; set; } = 15f;
+
+    /// <summary>
+    ///     Gets or sets the number of common tasks.
+    /// </summary>
+    public int NumCommonTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of long tasks.
+    /// </summary>
+    public int NumLongTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of short tasks.
+    /// </summary>
+    public int NumShortTasks { get; set; } = 2;
+
+    /// <summary>
+    ///     Gets or sets the maximum amount of emergency meetings each player can call during the game in seconds.
+    /// </summary>
+    public int NumEmergencyMeetings { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the cooldown between each time any player can call an emergency meeting in seconds.
+    /// </summary>
+    public int EmergencyCooldown { get; set; } = 15;
+
+    /// <summary>
+    ///     Gets or sets the number of impostors for this lobby.
+    /// </summary>
+    public int NumImpostors { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether ghosts (dead crew members) can do tasks.
+    /// </summary>
+    public bool GhostsDoTasks { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets the Kill as per values in <see cref="KillDistances" />.
+    /// </summary>
+    public KillDistances KillDistance { get; set; } = KillDistances.Normal;
+
+    /// <summary>
+    ///     Gets or sets the time for discussion before voting time in seconds.
+    /// </summary>
+    public int DiscussionTime { get; set; } = 15;
+
+    /// <summary>
+    ///     Gets or sets the time for voting in seconds.
+    /// </summary>
+    public int VotingTime { get; set; } = 120;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether an ejected player is an impostor or not.
+    /// </summary>
+    public bool ConfirmImpostor { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether players are able to see tasks being performed by other players.
+    /// </summary>
+    /// <remarks>
+    ///     By being set to true, tasks such as Empty Garbage, Submit Scan, Clear asteroids, Prime shields execution will be visible to other players.
+    /// </remarks>
+    public bool VisualTasks { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the vote is anonymous.
+    /// </summary>
+    public bool AnonymousVotes { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the task bar update mode as per values in <see cref="Innersloth.TaskBarUpdate" />.
+    /// </summary>
+    public TaskBarUpdate TaskBarUpdate { get; set; } = TaskBarUpdate.Always;
+
+    /// <summary>
+    ///     Gets or sets role options.
+    /// </summary>
+    public LegacyRoleOptionsData RoleOptions { get; set; } = new LegacyRoleOptionsData();
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the GameOptions are the default ones.
+    /// </summary>
+    public bool IsDefaults { get; set; } = true;
+
+    public static LegacyGameOptionsData Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new LegacyGameOptionsData(version);
+        options.Deserialize(reader);
+        return options;
+    }
+
+    public void Deserialize(IMessageReader reader)
+    {
+        Version = reader.ReadByte();
+        MaxPlayers = reader.ReadByte();
+        Keywords = (GameKeywords)reader.ReadUInt32();
+        Map = (MapTypes)reader.ReadByte();
+        PlayerSpeedMod = reader.ReadSingle();
+
+        CrewLightMod = reader.ReadSingle();
+        ImpostorLightMod = reader.ReadSingle();
+        KillCooldown = reader.ReadSingle();
+
+        NumCommonTasks = reader.ReadByte();
+        NumLongTasks = reader.ReadByte();
+        NumShortTasks = reader.ReadByte();
+
+        NumEmergencyMeetings = reader.ReadInt32();
+
+        NumImpostors = reader.ReadByte();
+        KillDistance = (KillDistances)reader.ReadByte();
+        DiscussionTime = reader.ReadInt32();
+        VotingTime = reader.ReadInt32();
+
+        IsDefaults = reader.ReadBoolean();
+
+        if (Version >= 2)
+        {
+            EmergencyCooldown = reader.ReadByte();
+        }
+
+        if (Version >= 3)
+        {
+            ConfirmImpostor = reader.ReadBoolean();
+            VisualTasks = reader.ReadBoolean();
+        }
+
+        if (Version >= 4)
+        {
+            AnonymousVotes = reader.ReadBoolean();
+            TaskBarUpdate = (TaskBarUpdate)reader.ReadByte();
+        }
+
+        if (Version >= 5)
+        {
+            RoleOptions = LegacyRoleOptionsData.Deserialize(reader);
+        }
+
+        if (Version >= 6)
+        {
+            // Nothing was changed in V6
+        }
+
+        if (Version > 6)
+        {
+            IGameOptions.ThrowUnknownVersion<LegacyGameOptionsData>(Version);
+        }
+    }
+
+    /// <summary>
+    ///     Serializes this instance of GameOptionsData object to a specified BinaryWriter.
+    /// </summary>
+    /// <param name="writer">The stream to write the message to.</param>
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write((byte)Version);
+        writer.Write((byte)MaxPlayers);
+        writer.Write((uint)Keywords);
+        writer.Write((byte)Map);
+        writer.Write((float)PlayerSpeedMod);
+        writer.Write((float)CrewLightMod);
+        writer.Write((float)ImpostorLightMod);
+        writer.Write((float)KillCooldown);
+        writer.Write((byte)NumCommonTasks);
+        writer.Write((byte)NumLongTasks);
+        writer.Write((byte)NumShortTasks);
+        writer.Write((int)NumEmergencyMeetings);
+        writer.Write((byte)NumImpostors);
+        writer.Write((byte)KillDistance);
+        writer.Write((uint)DiscussionTime);
+        writer.Write((uint)VotingTime);
+        writer.Write((bool)IsDefaults);
+
+        if (Version >= 2)
+        {
+            writer.Write((byte)EmergencyCooldown);
+        }
+
+        if (Version >= 3)
+        {
+            writer.Write((bool)ConfirmImpostor);
+            writer.Write((bool)VisualTasks);
+        }
+
+        if (Version >= 4)
+        {
+            writer.Write((bool)AnonymousVotes);
+            writer.Write((byte)TaskBarUpdate);
+        }
+
+        if (Version >= 5)
+        {
+            RoleOptions.Serialize(writer);
+        }
+
+        if (Version >= 6)
+        {
+            // Nothing was changed in V6
+        }
+
+        if (Version > 6)
+        {
+            throw new ImpostorException($"Unknown {nameof(LegacyGameOptionsData)} version {Version}");
+        }
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/NormalGameOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/NormalGameOptions.cs
new file mode 100644 (file)
index 0000000..e91cb42
--- /dev/null
@@ -0,0 +1,209 @@
+using Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+namespace Impostor.Api.Innersloth.GameOptions;
+
+public class NormalGameOptions : IGameOptions
+{
+    public const int LatestVersion = 7;
+
+    public NormalGameOptions(byte version = LatestVersion)
+    {
+        Version = version;
+        IGameOptions.EnsureVersionIsModular<NormalGameOptions>(version);
+        RoleOptions = new RoleOptionsCollection(version);
+    }
+
+    /// <inheritdoc />
+    public byte Version { get; }
+
+    /// <inheritdoc />
+    public GameModes GameMode => GameModes.Normal;
+
+    /// <inheritdoc />
+    public byte MaxPlayers { get; set; } = 10;
+
+    /// <inheritdoc />
+    public GameKeywords Keywords { get; set; } = GameKeywords.English;
+
+    /// <inheritdoc />
+    public MapTypes Map { get; set; } = MapTypes.Skeld;
+
+    /// <inheritdoc />
+    public int NumImpostors { get; set; } = 1;
+
+    /// <inheritdoc />
+    public bool IsDefaults { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets the Player speed modifier.
+    /// </summary>
+    public float PlayerSpeedMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are members of the crew as a multiplier value.
+    /// </summary>
+    public float CrewLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Light modifier for the players that are Impostors as a multiplier value.
+    /// </summary>
+    public float ImpostorLightMod { get; set; } = 1f;
+
+    /// <summary>
+    ///     Gets or sets the Impostor cooldown to kill in seconds.
+    /// </summary>
+    public float KillCooldown { get; set; } = 15f;
+
+    /// <summary>
+    ///     Gets or sets the number of common tasks.
+    /// </summary>
+    public int NumCommonTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of long tasks.
+    /// </summary>
+    public int NumLongTasks { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the number of short tasks.
+    /// </summary>
+    public int NumShortTasks { get; set; } = 2;
+
+    /// <summary>
+    ///     Gets or sets the maximum amount of emergency meetings each player can call during the game in seconds.
+    /// </summary>
+    public int NumEmergencyMeetings { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the cooldown between each time any player can call an emergency meeting in seconds.
+    /// </summary>
+    public int EmergencyCooldown { get; set; } = 15;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether ghosts (dead crew members) can do tasks.
+    /// </summary>
+    public bool GhostsDoTasks { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets the Kill as per values in <see cref="KillDistances" />.
+    /// </summary>
+    public KillDistances KillDistance { get; set; } = KillDistances.Normal;
+
+    /// <summary>
+    ///     Gets or sets the time for discussion before voting time in seconds.
+    /// </summary>
+    public int DiscussionTime { get; set; } = 15;
+
+    /// <summary>
+    ///     Gets or sets the time for voting in seconds.
+    /// </summary>
+    public int VotingTime { get; set; } = 120;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether an ejected player is an impostor or not.
+    /// </summary>
+    public bool ConfirmImpostor { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether players are able to see tasks being performed by other players.
+    /// </summary>
+    /// <remarks>
+    ///     By being set to true, tasks such as Empty Garbage, Submit Scan, Clear asteroids, Prime shields execution will be visible to other players.
+    /// </remarks>
+    public bool VisualTasks { get; set; } = true;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the vote is anonymous.
+    /// </summary>
+    public bool AnonymousVotes { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the task bar update mode as per values in <see cref="Innersloth.TaskBarUpdate" />.
+    /// </summary>
+    public TaskBarUpdate TaskBarUpdate { get; set; } = TaskBarUpdate.Always;
+
+    public RoleOptionsCollection RoleOptions { get; set; }
+
+    public static NormalGameOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new NormalGameOptions(version);
+        options.Deserialize(reader);
+        return options;
+    }
+
+    public void Deserialize(IMessageReader reader)
+    {
+        MaxPlayers = reader.ReadByte();
+        Keywords = (GameKeywords)reader.ReadUInt32();
+        Map = (MapTypes)reader.ReadByte();
+        PlayerSpeedMod = reader.ReadSingle();
+
+        CrewLightMod = reader.ReadSingle();
+        ImpostorLightMod = reader.ReadSingle();
+        KillCooldown = reader.ReadSingle();
+
+        NumCommonTasks = reader.ReadByte();
+        NumLongTasks = reader.ReadByte();
+        NumShortTasks = reader.ReadByte();
+
+        NumEmergencyMeetings = reader.ReadInt32();
+
+        NumImpostors = reader.ReadByte();
+        KillDistance = (KillDistances)reader.ReadByte();
+        DiscussionTime = reader.ReadInt32();
+        VotingTime = reader.ReadInt32();
+
+        IsDefaults = reader.ReadBoolean();
+
+        EmergencyCooldown = reader.ReadByte();
+        ConfirmImpostor = reader.ReadBoolean();
+        VisualTasks = reader.ReadBoolean();
+        AnonymousVotes = reader.ReadBoolean();
+        TaskBarUpdate = (TaskBarUpdate)reader.ReadByte();
+
+        RoleOptions.Deserialize(reader);
+
+        if (Version > 7)
+        {
+            IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
+        }
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(MaxPlayers);
+        writer.Write((uint)Keywords);
+        writer.Write((byte)Map);
+        writer.Write(PlayerSpeedMod);
+
+        writer.Write(CrewLightMod);
+        writer.Write(ImpostorLightMod);
+        writer.Write(KillCooldown);
+
+        writer.Write(NumCommonTasks);
+        writer.Write(NumLongTasks);
+        writer.Write(NumShortTasks);
+
+        writer.Write(NumEmergencyMeetings);
+
+        writer.Write(NumImpostors);
+        writer.Write((byte)KillDistance);
+        writer.Write(DiscussionTime);
+        writer.Write(VotingTime);
+
+        writer.Write(IsDefaults);
+
+        writer.Write(EmergencyCooldown);
+        writer.Write(ConfirmImpostor);
+        writer.Write(VisualTasks);
+        writer.Write(AnonymousVotes);
+        writer.Write((byte)TaskBarUpdate);
+
+        RoleOptions.Serialize(writer);
+
+        if (Version > 7)
+        {
+            IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
+        }
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/EngineerRoleOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/EngineerRoleOptions.cs
new file mode 100644 (file)
index 0000000..45c7b74
--- /dev/null
@@ -0,0 +1,33 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class EngineerRoleOptions : IRoleOptions
+{
+    public EngineerRoleOptions(byte version)
+    {
+        Version = version;
+    }
+
+    public byte Version { get; }
+
+    public RoleTypes Type => RoleTypes.Engineer;
+
+    public byte Cooldown { get; set; } = 30;
+
+    public byte InVentMaxTime { get; set; } = 15;
+
+    public static EngineerRoleOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new EngineerRoleOptions(version);
+
+        options.Cooldown = reader.ReadByte();
+        options.InVentMaxTime = reader.ReadByte();
+
+        return options;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(Cooldown);
+        writer.Write(InVentMaxTime);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/GuardianAngelRoleOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/GuardianAngelRoleOptions.cs
new file mode 100644 (file)
index 0000000..d46c57d
--- /dev/null
@@ -0,0 +1,37 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class GuardianAngelRoleOptions : IRoleOptions
+{
+    public GuardianAngelRoleOptions(byte version)
+    {
+        Version = version;
+    }
+
+    public byte Version { get; }
+
+    public RoleTypes Type => RoleTypes.GuardianAngel;
+
+    public byte Cooldown { get; set; } = 60;
+
+    public byte ProtectionDurationSeconds { get; set; } = 10;
+
+    public bool ImpostorsCanSeeProtect { get; set; }
+
+    public static GuardianAngelRoleOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new GuardianAngelRoleOptions(version);
+
+        options.Cooldown = reader.ReadByte();
+        options.ProtectionDurationSeconds = reader.ReadByte();
+        options.ImpostorsCanSeeProtect = reader.ReadBoolean();
+
+        return options;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(Cooldown);
+        writer.Write(ProtectionDurationSeconds);
+        writer.Write(ImpostorsCanSeeProtect);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/IRoleOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/IRoleOptions.cs
new file mode 100644 (file)
index 0000000..0fe0bac
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public interface IRoleOptions
+{
+    RoleTypes Type { get; }
+
+    void Serialize(IMessageWriter writer);
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/LegacyRoleOptionsData.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/LegacyRoleOptionsData.cs
new file mode 100644 (file)
index 0000000..5a5381f
--- /dev/null
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class LegacyRoleOptionsData
+{
+    public bool ShapeshifterLeaveSkin { get; set; }
+
+    public byte ShapeshifterCooldown { get; set; } = 10;
+
+    public byte ShapeshifterDuration { get; set; } = 30;
+
+    public byte ScientistCooldown { get; set; } = 15;
+
+    public byte ScientistBatteryCharge { get; set; } = 5;
+
+    public byte GuardianAngelCooldown { get; set; } = 60;
+
+    public bool ImpostorsCanSeeProtect { get; set; }
+
+    public byte ProtectionDurationSeconds { get; set; } = 10;
+
+    public byte EngineerCooldown { get; set; } = 30;
+
+    public byte EngineerInVentMaxTime { get; set; } = 15;
+
+    public Dictionary<RoleTypes, RoleRate> RoleRates { get; } = new Dictionary<RoleTypes, RoleRate>();
+
+    public static LegacyRoleOptionsData Deserialize(IMessageReader reader)
+    {
+        var roleOptionsData = new LegacyRoleOptionsData();
+        var num = reader.ReadPackedInt32();
+        for (var i = 0; i < num; i++)
+        {
+            var key = (RoleTypes)reader.ReadInt16();
+            var roleRate = RoleRate.Deserialize(reader);
+            roleOptionsData.RoleRates[key] = roleRate;
+        }
+
+        roleOptionsData.ShapeshifterLeaveSkin = reader.ReadBoolean();
+        roleOptionsData.ShapeshifterCooldown = reader.ReadByte();
+        roleOptionsData.ShapeshifterDuration = reader.ReadByte();
+        roleOptionsData.ScientistCooldown = reader.ReadByte();
+        roleOptionsData.GuardianAngelCooldown = reader.ReadByte();
+        roleOptionsData.EngineerCooldown = reader.ReadByte();
+        roleOptionsData.EngineerInVentMaxTime = reader.ReadByte();
+        roleOptionsData.ScientistBatteryCharge = reader.ReadByte();
+        roleOptionsData.ProtectionDurationSeconds = reader.ReadByte();
+        roleOptionsData.ImpostorsCanSeeProtect = reader.ReadBoolean();
+        return roleOptionsData;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.WritePacked(RoleRates.Count);
+        foreach (var roleRate in RoleRates)
+        {
+            writer.Write((ushort)roleRate.Key);
+            roleRate.Value.Serialize(writer);
+        }
+
+        writer.Write(ShapeshifterLeaveSkin);
+        writer.Write(ShapeshifterCooldown);
+        writer.Write(ShapeshifterDuration);
+        writer.Write(ScientistCooldown);
+        writer.Write(GuardianAngelCooldown);
+        writer.Write(EngineerCooldown);
+        writer.Write(EngineerInVentMaxTime);
+        writer.Write(ScientistBatteryCharge);
+        writer.Write(ProtectionDurationSeconds);
+        writer.Write(ImpostorsCanSeeProtect);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleOptionsCollection.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleOptionsCollection.cs
new file mode 100644 (file)
index 0000000..d8b7076
--- /dev/null
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class RoleOptionsCollection
+{
+    public RoleOptionsCollection(byte version)
+    {
+        Version = version;
+    }
+
+    public byte Version { get; }
+
+    public Dictionary<RoleTypes, RoleData> Roles { get; } = new Dictionary<RoleTypes, RoleData>();
+
+    public void Deserialize(IMessageReader reader)
+    {
+        Roles.Clear();
+
+        var count = reader.ReadPackedInt32();
+        Roles.EnsureCapacity(count);
+        for (var i = 0; i < count; i++)
+        {
+            var roleType = (RoleTypes)reader.ReadInt16();
+            var roleRate = RoleRate.Deserialize(reader);
+            var roleOptionsReader = reader.ReadMessage();
+            IRoleOptions roleOptions = roleType switch
+            {
+                RoleTypes.Scientist => ScientistRoleOptions.Deserialize(roleOptionsReader, Version),
+                RoleTypes.Engineer => EngineerRoleOptions.Deserialize(roleOptionsReader, Version),
+                RoleTypes.GuardianAngel => GuardianAngelRoleOptions.Deserialize(roleOptionsReader, Version),
+                RoleTypes.Shapeshifter => ShapeshifterRoleOptions.Deserialize(roleOptionsReader, Version),
+                _ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
+            };
+
+            Roles.Add(roleType, new RoleData(roleType, roleOptions, roleRate));
+        }
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.WritePacked(Roles.Count);
+        foreach (var (key, roleData) in Roles)
+        {
+            writer.Write((ushort)key);
+            roleData.Rate.Serialize(writer);
+            writer.StartMessage(0);
+            roleData.RoleOptions.Serialize(writer);
+            writer.EndMessage();
+        }
+    }
+
+    public readonly record struct RoleData(RoleTypes Type, IRoleOptions RoleOptions, RoleRate Rate);
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleRate.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/RoleRate.cs
new file mode 100644 (file)
index 0000000..352db4f
--- /dev/null
@@ -0,0 +1,15 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public readonly record struct RoleRate(byte MaxCount, byte Chance)
+{
+    public static RoleRate Deserialize(IMessageReader reader)
+    {
+        return new RoleRate(reader.ReadByte(), reader.ReadByte());
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(MaxCount);
+        writer.Write(Chance);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ScientistRoleOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ScientistRoleOptions.cs
new file mode 100644 (file)
index 0000000..4a38953
--- /dev/null
@@ -0,0 +1,33 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class ScientistRoleOptions : IRoleOptions
+{
+    public ScientistRoleOptions(byte version)
+    {
+        Version = version;
+    }
+
+    public byte Version { get; }
+
+    public RoleTypes Type => RoleTypes.Scientist;
+
+    public byte Cooldown { get; set; } = 15;
+
+    public byte BatteryCharge { get; set; } = 5;
+
+    public static ScientistRoleOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new ScientistRoleOptions(version);
+
+        options.Cooldown = reader.ReadByte();
+        options.BatteryCharge = reader.ReadByte();
+
+        return options;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(Cooldown);
+        writer.Write(BatteryCharge);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ShapeshifterRoleOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/RoleOptions/ShapeshifterRoleOptions.cs
new file mode 100644 (file)
index 0000000..8202b7d
--- /dev/null
@@ -0,0 +1,37 @@
+namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
+
+public class ShapeshifterRoleOptions : IRoleOptions
+{
+    public ShapeshifterRoleOptions(byte version)
+    {
+        Version = version;
+    }
+
+    public byte Version { get; }
+
+    public RoleTypes Type => RoleTypes.Shapeshifter;
+
+    public bool LeaveSkin { get; set; }
+
+    public byte Cooldown { get; set; } = 10;
+
+    public byte Duration { get; set; } = 30;
+
+    public static ShapeshifterRoleOptions Deserialize(IMessageReader reader, byte version)
+    {
+        var options = new ShapeshifterRoleOptions(version);
+
+        options.LeaveSkin = reader.ReadBoolean();
+        options.Cooldown = reader.ReadByte();
+        options.Duration = reader.ReadByte();
+
+        return options;
+    }
+
+    public void Serialize(IMessageWriter writer)
+    {
+        writer.Write(LeaveSkin);
+        writer.Write(Cooldown);
+        writer.Write(Duration);
+    }
+}
diff --git a/src/Impostor.Api/Innersloth/GameOptionsData.cs b/src/Impostor.Api/Innersloth/GameOptionsData.cs
deleted file mode 100644 (file)
index 71a6e43..0000000
+++ /dev/null
@@ -1,278 +0,0 @@
-using System;
-using System.IO;
-
-namespace Impostor.Api.Innersloth
-{
-    public class GameOptionsData
-    {
-        /// <summary>
-        ///     The latest major version of the game client.
-        /// </summary>
-        public const int LatestVersion = 5;
-
-        /// <summary>
-        ///     Gets or sets host's version of the game.
-        /// </summary>
-        public byte Version { get; set; } = LatestVersion;
-
-        /// <summary>
-        ///     Gets or sets the maximum amount of players for this lobby.
-        /// </summary>
-        public byte MaxPlayers { get; set; } = 10;
-
-        /// <summary>
-        ///     Gets or sets the language of the lobby as per <see cref="GameKeywords" /> enum.
-        /// </summary>
-        public GameKeywords Keywords { get; set; } = GameKeywords.English;
-
-        /// <summary>
-        ///     Gets or sets the Map selected for this lobby.
-        /// </summary>
-        public MapTypes Map { get; set; } = MapTypes.Skeld;
-
-        /// <summary>
-        ///     Gets or sets the Player speed modifier.
-        /// </summary>
-        public float PlayerSpeedMod { get; set; } = 1f;
-
-        /// <summary>
-        ///     Gets or sets the Light modifier for the players that are members of the crew as a multiplier value.
-        /// </summary>
-        public float CrewLightMod { get; set; } = 1f;
-
-        /// <summary>
-        ///     Gets or sets the Light modifier for the players that are Impostors as a multiplier value.
-        /// </summary>
-        public float ImpostorLightMod { get; set; } = 1f;
-
-        /// <summary>
-        ///     Gets or sets the Impostor cooldown to kill in seconds.
-        /// </summary>
-        public float KillCooldown { get; set; } = 15f;
-
-        /// <summary>
-        ///     Gets or sets the number of common tasks.
-        /// </summary>
-        public int NumCommonTasks { get; set; } = 1;
-
-        /// <summary>
-        ///     Gets or sets the number of long tasks.
-        /// </summary>
-        public int NumLongTasks { get; set; } = 1;
-
-        /// <summary>
-        ///     Gets or sets the number of short tasks.
-        /// </summary>
-        public int NumShortTasks { get; set; } = 2;
-
-        /// <summary>
-        ///     Gets or sets the maximum amount of emergency meetings each player can call during the game in seconds.
-        /// </summary>
-        public int NumEmergencyMeetings { get; set; } = 1;
-
-        /// <summary>
-        ///     Gets or sets the cooldown between each time any player can call an emergency meeting in seconds.
-        /// </summary>
-        public int EmergencyCooldown { get; set; } = 15;
-
-        /// <summary>
-        ///     Gets or sets the number of impostors for this lobby.
-        /// </summary>
-        public int NumImpostors { get; set; } = 1;
-
-        /// <summary>
-        ///     Gets or sets a value indicating whether ghosts (dead crew members) can do tasks.
-        /// </summary>
-        public bool GhostsDoTasks { get; set; } = true;
-
-        /// <summary>
-        ///     Gets or sets the Kill as per values in <see cref="KillDistances" />.
-        /// </summary>
-        public KillDistances KillDistance { get; set; } = KillDistances.Normal;
-
-        /// <summary>
-        ///     Gets or sets the time for discussion before voting time in seconds.
-        /// </summary>
-        public int DiscussionTime { get; set; } = 15;
-
-        /// <summary>
-        ///     Gets or sets the time for voting in seconds.
-        /// </summary>
-        public int VotingTime { get; set; } = 120;
-
-        /// <summary>
-        ///     Gets or sets a value indicating whether an ejected player is an impostor or not.
-        /// </summary>
-        public bool ConfirmImpostor { get; set; } = true;
-
-        /// <summary>
-        ///     Gets or sets a value indicating whether players are able to see tasks being performed by other players.
-        /// </summary>
-        /// <remarks>
-        ///     By being set to true, tasks such as Empty Garbage, Submit Scan, Clear asteroids, Prime shields execution will be visible to other players.
-        /// </remarks>
-        public bool VisualTasks { get; set; } = true;
-
-        /// <summary>
-        ///     Gets or sets a value indicating whether the vote is anonymous.
-        /// </summary>
-        public bool AnonymousVotes { get; set; }
-
-        /// <summary>
-        ///     Gets or sets the task bar update mode as per values in <see cref="Innersloth.TaskBarUpdate" />.
-        /// </summary>
-        public TaskBarUpdate TaskBarUpdate { get; set; } = TaskBarUpdate.Always;
-
-        /// <summary>
-        ///     Gets or sets role options.
-        /// </summary>
-        public RoleOptionsData RoleOptions { get; set; } = new RoleOptionsData();
-
-        /// <summary>
-        ///     Gets or sets a value indicating whether the GameOptions are the default ones.
-        /// </summary>
-        public bool IsDefaults { get; set; } = true;
-
-        /// <summary>
-        ///     Deserialize a packet/message to a new GameOptionsData object.
-        /// </summary>
-        /// <param name="reader">Message reader object containing the raw message.</param>
-        /// <returns>GameOptionsData object.</returns>
-        public static GameOptionsData DeserializeCreate(IMessageReader reader)
-        {
-            var options = new GameOptionsData();
-            options.Deserialize(reader.ReadBytesAndSize());
-            return options;
-        }
-
-        /// <summary>
-        ///     Serializes this instance of GameOptionsData object to a specified BinaryWriter.
-        /// </summary>
-        /// <param name="writer">The stream to write the message to.</param>
-        /// <param name="version">The version of the game.</param>
-        public void Serialize(BinaryWriter writer, byte version = LatestVersion)
-        {
-            writer.Write((byte)version);
-            writer.Write((byte)MaxPlayers);
-            writer.Write((uint)Keywords);
-            writer.Write((byte)Map);
-            writer.Write((float)PlayerSpeedMod);
-            writer.Write((float)CrewLightMod);
-            writer.Write((float)ImpostorLightMod);
-            writer.Write((float)KillCooldown);
-            writer.Write((byte)NumCommonTasks);
-            writer.Write((byte)NumLongTasks);
-            writer.Write((byte)NumShortTasks);
-            writer.Write((int)NumEmergencyMeetings);
-            writer.Write((byte)NumImpostors);
-            writer.Write((byte)KillDistance);
-            writer.Write((uint)DiscussionTime);
-            writer.Write((uint)VotingTime);
-            writer.Write((bool)IsDefaults);
-
-            if (version >= 2)
-            {
-                writer.Write((byte)EmergencyCooldown);
-            }
-
-            if (version >= 3)
-            {
-                writer.Write((bool)ConfirmImpostor);
-                writer.Write((bool)VisualTasks);
-            }
-
-            if (version >= 4)
-            {
-                writer.Write((bool)AnonymousVotes);
-                writer.Write((byte)TaskBarUpdate);
-            }
-
-            if (version >= 5)
-            {
-                RoleOptions.Serialize(writer);
-            }
-
-            if (version >= 6)
-            {
-                // Nothing was changed in V6
-            }
-
-            if (version > 6)
-            {
-                throw new ImpostorException($"Unknown GameOptionsData version {Version}.");
-            }
-        }
-
-        public void Serialize(IMessageWriter writer)
-        {
-            using var memory = new MemoryStream();
-            using var writerBin = new BinaryWriter(memory);
-            Serialize(writerBin);
-            writer.WriteBytesAndSize(memory.ToArray());
-        }
-
-        /// <summary>
-        ///     Deserialize a ReadOnlyMemory object to this instance of the GameOptionsData object.
-        /// </summary>
-        /// <param name="memory">Memory containing the message/packet.</param>
-        public void Deserialize(ReadOnlyMemory<byte> memory)
-        {
-            var bytes = memory.Span;
-
-            Version = bytes.ReadByte();
-            MaxPlayers = bytes.ReadByte();
-            Keywords = (GameKeywords)bytes.ReadUInt32();
-            Map = (MapTypes)bytes.ReadByte();
-            PlayerSpeedMod = bytes.ReadSingle();
-
-            CrewLightMod = bytes.ReadSingle();
-            ImpostorLightMod = bytes.ReadSingle();
-            KillCooldown = bytes.ReadSingle();
-
-            NumCommonTasks = bytes.ReadByte();
-            NumLongTasks = bytes.ReadByte();
-            NumShortTasks = bytes.ReadByte();
-
-            NumEmergencyMeetings = bytes.ReadInt32();
-
-            NumImpostors = bytes.ReadByte();
-            KillDistance = (KillDistances)bytes.ReadByte();
-            DiscussionTime = bytes.ReadInt32();
-            VotingTime = bytes.ReadInt32();
-
-            IsDefaults = bytes.ReadBoolean();
-
-            if (Version >= 2)
-            {
-                EmergencyCooldown = bytes.ReadByte();
-            }
-
-            if (Version >= 3)
-            {
-                ConfirmImpostor = bytes.ReadBoolean();
-                VisualTasks = bytes.ReadBoolean();
-            }
-
-            if (Version >= 4)
-            {
-                AnonymousVotes = bytes.ReadBoolean();
-                TaskBarUpdate = (TaskBarUpdate)bytes.ReadByte();
-            }
-
-            if (Version >= 5)
-            {
-                RoleOptions = RoleOptionsData.Deserialize(bytes);
-            }
-
-            if (Version >= 6)
-            {
-                // Nothing was changed in V6
-            }
-
-            if (Version > 6)
-            {
-                throw new ImpostorException($"Unknown GameOptionsData version {Version}.");
-            }
-        }
-    }
-}
diff --git a/src/Impostor.Api/Innersloth/RoleOptionsData.cs b/src/Impostor.Api/Innersloth/RoleOptionsData.cs
deleted file mode 100644 (file)
index 85356cf..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-namespace Impostor.Api.Innersloth
-{
-    public class RoleOptionsData
-    {
-        public bool ShapeshifterLeaveSkin { get; set; }
-
-        public byte ShapeshifterCooldown { get; set; } = 10;
-
-        public byte ShapeshifterDuration { get; set; } = 30;
-
-        public byte ScientistCooldown { get; set; } = 15;
-
-        public byte ScientistBatteryCharge { get; set; } = 5;
-
-        public byte GuardianAngelCooldown { get; set; } = 60;
-
-        public bool ImpostorsCanSeeProtect { get; set; }
-
-        public byte ProtectionDurationSeconds { get; set; } = 10;
-
-        public byte EngineerCooldown { get; set; } = 30;
-
-        public byte EngineerInVentMaxTime { get; set; } = 15;
-
-        public Dictionary<RoleTypes, RoleRate> RoleRates { get; } = new Dictionary<RoleTypes, RoleRate>();
-
-        public static RoleOptionsData Deserialize(ReadOnlySpan<byte> span)
-        {
-            var roleOptionsData = new RoleOptionsData();
-            var num = span.ReadInt32();
-            for (var i = 0; i < num; i++)
-            {
-                var key = (RoleTypes)span.ReadInt16();
-                var roleRate = new RoleRate(span.ReadByte(), span.ReadByte());
-                roleOptionsData.RoleRates[key] = roleRate;
-            }
-
-            roleOptionsData.ShapeshifterLeaveSkin = span.ReadBoolean();
-            roleOptionsData.ShapeshifterCooldown = span.ReadByte();
-            roleOptionsData.ShapeshifterDuration = span.ReadByte();
-            roleOptionsData.ScientistCooldown = span.ReadByte();
-            roleOptionsData.GuardianAngelCooldown = span.ReadByte();
-            roleOptionsData.EngineerCooldown = span.ReadByte();
-            roleOptionsData.EngineerInVentMaxTime = span.ReadByte();
-            roleOptionsData.ScientistBatteryCharge = span.ReadByte();
-            roleOptionsData.ProtectionDurationSeconds = span.ReadByte();
-            roleOptionsData.ImpostorsCanSeeProtect = span.ReadBoolean();
-            return roleOptionsData;
-        }
-
-        public static RoleOptionsData Deserialize(IMessageReader reader)
-        {
-            var roleOptionsData = new RoleOptionsData();
-            var num = reader.ReadPackedInt32();
-            for (var i = 0; i < num; i++)
-            {
-                var key = (RoleTypes)reader.ReadInt16();
-                var roleRate = new RoleRate(reader.ReadByte(), reader.ReadByte());
-                roleOptionsData.RoleRates[key] = roleRate;
-            }
-
-            roleOptionsData.ShapeshifterLeaveSkin = reader.ReadBoolean();
-            roleOptionsData.ShapeshifterCooldown = reader.ReadByte();
-            roleOptionsData.ShapeshifterDuration = reader.ReadByte();
-            roleOptionsData.ScientistCooldown = reader.ReadByte();
-            roleOptionsData.GuardianAngelCooldown = reader.ReadByte();
-            roleOptionsData.EngineerCooldown = reader.ReadByte();
-            roleOptionsData.EngineerInVentMaxTime = reader.ReadByte();
-            roleOptionsData.ScientistBatteryCharge = reader.ReadByte();
-            roleOptionsData.ProtectionDurationSeconds = reader.ReadByte();
-            roleOptionsData.ImpostorsCanSeeProtect = reader.ReadBoolean();
-            return roleOptionsData;
-        }
-
-        public void Serialize(IMessageWriter writer)
-        {
-            writer.WritePacked(RoleRates.Count);
-            foreach (var roleRate in RoleRates)
-            {
-                writer.Write((ushort)roleRate.Key);
-                writer.Write((byte)roleRate.Value.MaxCount);
-                writer.Write((byte)roleRate.Value.Chance);
-            }
-
-            writer.Write(ShapeshifterLeaveSkin);
-            writer.Write(ShapeshifterCooldown);
-            writer.Write(ShapeshifterDuration);
-            writer.Write(ScientistCooldown);
-            writer.Write(GuardianAngelCooldown);
-            writer.Write(EngineerCooldown);
-            writer.Write(EngineerInVentMaxTime);
-            writer.Write(ScientistBatteryCharge);
-            writer.Write(ProtectionDurationSeconds);
-            writer.Write(ImpostorsCanSeeProtect);
-        }
-
-        public void Serialize(BinaryWriter writer)
-        {
-            writer.Write(RoleRates.Count);
-            foreach (var roleRate in RoleRates)
-            {
-                writer.Write((ushort)roleRate.Key);
-                writer.Write((byte)roleRate.Value.MaxCount);
-                writer.Write((byte)roleRate.Value.Chance);
-            }
-
-            writer.Write(ShapeshifterLeaveSkin);
-            writer.Write(ShapeshifterCooldown);
-            writer.Write(ShapeshifterDuration);
-            writer.Write(ScientistCooldown);
-            writer.Write(GuardianAngelCooldown);
-            writer.Write(EngineerCooldown);
-            writer.Write(EngineerInVentMaxTime);
-            writer.Write(ScientistBatteryCharge);
-            writer.Write(ProtectionDurationSeconds);
-            writer.Write(ImpostorsCanSeeProtect);
-        }
-
-        public readonly struct RoleRate
-        {
-            public readonly int MaxCount;
-            public readonly int Chance;
-
-            public RoleRate(int maxCount, int chance)
-            {
-                MaxCount = maxCount;
-                Chance = chance;
-            }
-        }
-    }
-}
index 87f32e990af06ac7ab317a47a1715e66507ce91d..8b802c2211b78fa99d734d49f180b769f6013fac 100644 (file)
@@ -1,28 +1,24 @@
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 
 namespace Impostor.Api.Net.Messages.C2S
 {
     public static class Message00HostGameC2S
     {
-        public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData)
+        public static void Serialize(IMessageWriter writer, IGameOptions gameOptions, CrossplayFlags crossplayFlags, GameFilterOptions gameFilterOptions)
         {
             writer.StartMessage(MessageFlags.HostGame);
-            gameOptionsData.Serialize(writer);
-            writer.Write(int.MaxValue); // crossplayFlags
+            GameOptionsFactory.Serialize(writer, gameOptions);
+            writer.Write((int)crossplayFlags);
+            gameFilterOptions.Serialize(writer);
             writer.EndMessage();
         }
 
-        /// <summary>
-        ///     Deserialize a packet.
-        /// </summary>
-        /// <param name="reader"><see cref="IMessageReader" /> with <see cref="IMessageReader.Tag" /> 0.</param>
-        /// <returns>Deserialized <see cref="GameOptionsData" />.</returns>
-        public static GameOptionsData Deserialize(IMessageReader reader)
+        public static void Deserialize(IMessageReader reader, out IGameOptions gameOptions, out CrossplayFlags crossplayFlags, out GameFilterOptions gameFilterOptions)
         {
-            var gameOptionsData = GameOptionsData.DeserializeCreate(reader);
-            reader.ReadInt32(); // crossplayFlags, not used yet
-
-            return gameOptionsData;
+            gameOptions = GameOptionsFactory.Deserialize(reader);
+            crossplayFlags = (CrossplayFlags)reader.ReadInt32();
+            gameFilterOptions = GameFilterOptions.Deserialize(reader);
         }
     }
 }
index 13c02ae10253643335d19b434658c8a0b30d1de3..9ba3b6cfeb1787e3458045d755e9f1b9723a20d0 100644 (file)
@@ -1,5 +1,6 @@
 using System;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 
 namespace Impostor.Api.Net.Messages.C2S
 {
@@ -10,7 +11,7 @@ namespace Impostor.Api.Net.Messages.C2S
             throw new NotImplementedException();
         }
 
-        public static void Deserialize(IMessageReader reader, out GameOptionsData options, out QuickChatModes chatMode)
+        public static void Deserialize(IMessageReader reader, out IGameOptions options, out QuickChatModes chatMode, out CrossplayFlags crossplayFlags, out GameFilterOptions gameFilterOptions)
         {
             var version = reader.ReadPackedInt32();
             if (version != 2)
@@ -18,9 +19,10 @@ namespace Impostor.Api.Net.Messages.C2S
                 throw new NotSupportedException($"Version {version} of {nameof(Message16GetGameListC2S)} is not supported");
             }
 
-            options = GameOptionsData.DeserializeCreate(reader);
+            options = GameOptionsFactory.Deserialize(reader);
             chatMode = (QuickChatModes)reader.ReadByte();
-            reader.ReadInt32(); // crossplayFlags, not used yet
+            crossplayFlags = (CrossplayFlags)reader.ReadInt32();
+            gameFilterOptions = GameFilterOptions.Deserialize(reader);
         }
     }
 }
index 047d21fafc43ac67b369fb862a49cf14514635e7..3541e9d185a2c89db9034c5d0d5572e112ea3512 100644 (file)
@@ -1,17 +1,22 @@
-using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 
 namespace Impostor.Api.Net.Messages.Rpcs
 {
     public static class Rpc02SyncSettings
     {
-        public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData)
+        public static void Serialize(IMessageWriter writer, IGameOptions gameOptionsData)
         {
-            gameOptionsData.Serialize(writer);
+            GameOptionsFactory.Serialize(writer, gameOptionsData);
         }
 
-        public static void Deserialize(IMessageReader reader, GameOptionsData gameOptionsData)
+        public static void Deserialize(IMessageReader reader, out IGameOptions gameOptionsData)
         {
-            gameOptionsData.Deserialize(reader.ReadBytesAndSize());
+            gameOptionsData = GameOptionsFactory.Deserialize(reader);
+        }
+
+        public static void DeserializeInto(IMessageReader reader, IGameOptions gameOptionsData)
+        {
+            GameOptionsFactory.DeserializeInto(reader, gameOptionsData);
         }
     }
 }
index 8fef2251e4a6e6e9f857e3ca679fcba2e35877d5..effd7458d07c8b29ccdaf4426e099c3340469547 100644 (file)
@@ -1,5 +1,5 @@
 using System;
-using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 
 namespace Impostor.Api.Net.Messages.S2C
 {
@@ -12,7 +12,7 @@ namespace Impostor.Api.Net.Messages.S2C
             writer.EndMessage();
         }
 
-        public static GameOptionsData Deserialize(IMessageReader reader)
+        public static LegacyGameOptionsData Deserialize(IMessageReader reader)
         {
             throw new NotImplementedException();
         }
index d7113bf5a196eb7e9b0c13f5329954c159bae8b9..0fa398cf292e41d5c70f4fb8c1b47afd507f7e2f 100644 (file)
@@ -65,6 +65,7 @@
         "resolved": "1.2.0.435",
         "contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg=="
       }
-    }
+    },
+    ".NETStandard,Version=v2.1/linux-x64": {}
   }
 }
\ No newline at end of file
index 057677ba53586ae60cc6385e493d327f4fe80aee..eeb328e4b00950d8efeec8d1d09291f83eb66665 100644 (file)
@@ -2,6 +2,7 @@
 using System.Threading;
 using System.Threading.Tasks;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net.Messages.C2S;
 using Impostor.Hazel;
 using Impostor.Hazel.Abstractions;
@@ -27,11 +28,11 @@ namespace Impostor.Client.App
 
             var writeGameCreate = MessageWriter.Get(MessageType.Reliable);
 
-            Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData
+            Message00HostGameC2S.Serialize(writeGameCreate, new LegacyGameOptionsData
             {
                 MaxPlayers = 4,
                 NumImpostors = 2,
-            });
+            }, CrossplayFlags.All, new GameFilterOptions());
 
             // TODO: ObjectPool for MessageReaders
             using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023), null))
index 07f4d86f768277512b992139901ca77a282a3143..e5c91b0f626b0ce54b3bf500f1e9987f2a72f69d 100644 (file)
@@ -1,6 +1,6 @@
 using System.Threading.Tasks;
 using Impostor.Api.Games.Managers;
-using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Plugins;
 using Microsoft.Extensions.Logging;
 
@@ -22,7 +22,7 @@ namespace Impostor.Plugins.Example
         {
             _logger.LogInformation("Example is being enabled.");
 
-            var game = await _gameManager.CreateAsync(new GameOptionsData());
+            var game = await _gameManager.CreateAsync(new NormalGameOptions());
             if (game == null)
             {
                 _logger.LogWarning("Example game creation was cancelled");
index cdadd8862c1f543147de64b6b0c8eec99df15023..6370e5fe528bae6c1e9f833f1780b40f48eb5470 100644 (file)
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
 using Impostor.Api.Events;
 using Impostor.Api.Events.Player;
 using Impostor.Api.Innersloth.Customization;
+using Impostor.Api.Innersloth.GameOptions;
 using Microsoft.Extensions.Logging;
 
 namespace Impostor.Plugins.Example.Handlers
@@ -65,9 +66,13 @@ namespace Impostor.Plugins.Example.Handlers
 
             if (e.Message == "test")
             {
-                e.Game.Options.KillCooldown = 0;
                 e.Game.Options.NumImpostors = 2;
-                e.Game.Options.PlayerSpeedMod = 5;
+
+                if (e.Game.Options is NormalGameOptions normalGameOptions)
+                {
+                    normalGameOptions.KillCooldown = 0;
+                    normalGameOptions.PlayerSpeedMod = 5;
+                }
 
                 await e.Game.SyncSettingsAsync();
             }
index 31f5a041c12cfd27f6d7f78acc1872ae199ab4e8..49488e83d5cd975b0bade479164495857f56d76f 100644 (file)
@@ -5,6 +5,7 @@ using Impostor.Api;
 using Impostor.Api.Config;
 using Impostor.Api.Games;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Custom;
 using Impostor.Api.Net.Messages;
@@ -65,10 +66,10 @@ namespace Impostor.Server.Net
                 case MessageFlags.HostGame:
                 {
                     // Read game settings.
-                    var gameInfo = Message00HostGameC2S.Deserialize(reader);
+                    Message00HostGameC2S.Deserialize(reader, out var gameOptions, out _, out var gameFilterOptions);
 
                     // Create game.
-                    var game = await _gameManager.CreateAsync(this, gameInfo);
+                    var game = await _gameManager.CreateAsync(this, gameOptions);
 
                     if (game == null)
                     {
@@ -259,7 +260,7 @@ namespace Impostor.Server.Net
 
                 case MessageFlags.GetGameListV2:
                 {
-                    Message16GetGameListC2S.Deserialize(reader, out var options, out _);
+                    Message16GetGameListC2S.Deserialize(reader, out var options, out _, out _, out _);
                     await OnRequestGameListAsync(options);
                     break;
                 }
@@ -358,7 +359,7 @@ namespace Impostor.Server.Net
         ///     All options given.
         ///     At this moment, the client can only specify the map, impostor count and chat language.
         /// </param>
-        private ValueTask OnRequestGameListAsync(GameOptionsData options)
+        private ValueTask OnRequestGameListAsync(IGameOptions options)
         {
             using var message = MessageWriter.Get(MessageType.Reliable);
 
index c36278f0ff45ad9883d4a8f882aa2be8814feb70..79af7895dce32b8f9a3e6856dd527069739e7216 100644 (file)
@@ -8,6 +8,7 @@ using Impostor.Api;
 using Impostor.Api.Events.Managers;
 using Impostor.Api.Events.Player;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Custom;
 using Impostor.Api.Net.Inner;
@@ -43,7 +44,15 @@ namespace Impostor.Server.Net.Inner.Objects
                 try
                 {
                     const float AnimationTime = 0.25f + 0.5f + 0.4f + 3f + 0.75f + 5f;
-                    await Task.Delay(TimeSpan.FromSeconds(AnimationTime + Game.Options.DiscussionTime + Game.Options.VotingTime), _timerToken.Token);
+                    if (Game.Options.GameMode == GameModes.Normal)
+                    {
+                        var options = (NormalGameOptions)Game.Options;
+                        await Task.Delay(TimeSpan.FromSeconds(AnimationTime + options.DiscussionTime + options.VotingTime), _timerToken.Token);
+                    }
+                    else
+                    {
+                        throw new NotImplementedException();
+                    }
                 }
                 catch (TaskCanceledException)
                 {
index 793d9ce98e842effacef93457837fcc1417b6429..e02f9986c28091daf4c35504940c1140bc083a2a 100644 (file)
@@ -117,7 +117,7 @@ namespace Impostor.Server.Net.Inner.Objects
                         return false;
                     }
 
-                    Rpc02SyncSettings.Deserialize(reader, Game.Options);
+                    Rpc02SyncSettings.DeserializeInto(reader, Game.Options);
                     break;
                 }
 
index 12e218653d6e39f0a332ea85981942f29a4f1c66..02a079a18379306af3e0a77bad02cb24f151dd5a 100644 (file)
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using Impostor.Api.Games;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.Customization;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Utils;
 
 namespace Impostor.Server.Net.Inner.Objects
@@ -59,7 +60,15 @@ namespace Impostor.Server.Net.Inner.Objects
             // the impostor has a cooldown of half the usual duration. As a workaround we always assume the kill was
             // prevented and the impostor only has half of its cooldown.
             // FIXME when the base game improved their implementation.
-            return dateTimeProvider.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown / 2;
+            if (game.Options.GameMode == GameModes.Normal)
+            {
+                var options = (NormalGameOptions)game.Options;
+                return dateTimeProvider.UtcNow.Subtract(LastMurder).TotalSeconds >= options.KillCooldown / 2;
+            }
+            else
+            {
+                return true;
+            }
         }
 
         public void Serialize(IMessageWriter writer)
index ec87434f86b18f953f5c263cdb273d70c56ab888..a4bf36afe8bfe07bbc19d85e032e7daaac601ba1 100644 (file)
@@ -10,6 +10,7 @@ using Impostor.Api.Events.Managers;
 using Impostor.Api.Games;
 using Impostor.Api.Games.Managers;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Server.Events;
 using Impostor.Server.Net.State;
@@ -59,7 +60,7 @@ namespace Impostor.Server.Net.Manager
                 x.Value.IsPublic &&
                 x.Value.GameState == GameStates.NotStarted &&
                 x.Value.PlayerCount < x.Value.Options.MaxPlayers &&
-                (_compatibilityConfig.AllowVersionMixing == true || x.Value.Host?.Client.GameVersion == gameVersion)))
+                (_compatibilityConfig.AllowVersionMixing || x.Value.Host == null || x.Value.Host.Client.GameVersion == gameVersion)))
             {
                 // Check for options.
                 if (!map.HasFlag((MapFlags)(1 << (byte)game.Options.Map)))
@@ -110,7 +111,7 @@ namespace Impostor.Server.Net.Manager
             await _eventManager.CallAsync(new GameDestroyedEvent(game));
         }
 
-        public async ValueTask<IGame?> CreateAsync(IClient? owner, GameOptionsData options)
+        public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options)
         {
             var @event = new GameCreationEvent(this, owner);
             await _eventManager.CallAsync(@event);
@@ -135,12 +136,12 @@ namespace Impostor.Server.Net.Manager
             return game;
         }
 
-        public ValueTask<IGame?> CreateAsync(GameOptionsData options)
+        public ValueTask<IGame?> CreateAsync(IGameOptions options)
         {
             return CreateAsync(null, options);
         }
 
-        private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(GameOptionsData options, GameCode? desiredGameCode = null)
+        private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameCode? desiredGameCode = null)
         {
             var gameCode = desiredGameCode ?? _gameCodeFactory.Create();
             var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options);
index 62a7ef1ad6d67ed12621bff4b70ae2ec83d66cd4..f685ca1a3d6cb2922fa80595455af4a0c58596d9 100644 (file)
@@ -1,11 +1,10 @@
-using System.IO;
-using System.Net;
+using System.Net;
 using System.Threading.Tasks;
 using Impostor.Api;
 using Impostor.Api.Games;
-using Impostor.Api.Innersloth;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Messages.Rpcs;
 using Impostor.Hazel;
 
 namespace Impostor.Server.Net.State
@@ -21,6 +20,7 @@ namespace Impostor.Server.Net.State
             _bannedIps.Add(ipAddress);
         }
 
+        // TODO This no longer does anything, it was replaced by LogicOptions
         public async ValueTask SyncSettingsAsync()
         {
             if (Host?.Character == null)
@@ -34,12 +34,7 @@ namespace Impostor.Server.Net.State
                 // If this is not done, the host will overwrite changes later with the defaults.
                 Options.IsDefaults = false;
 
-                await using (var memory = new MemoryStream())
-                await using (var writerBin = new BinaryWriter(memory))
-                {
-                    Options.Serialize(writerBin, GameOptionsData.LatestVersion);
-                    writer.WriteBytesAndSize(memory.ToArray());
-                }
+                Rpc02SyncSettings.Serialize(writer, Options);
 
                 await FinishRpcAsync(writer);
             }
index ed701f05ff0461bb392a5afd484ccc9101abc64a..2f800e7a190802a95c3393c89ddb6e1907be0465 100644 (file)
@@ -10,6 +10,7 @@ using Impostor.Api.Config;
 using Impostor.Api.Events.Managers;
 using Impostor.Api.Games;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Messages.S2C;
 using Impostor.Server.Events;
@@ -36,7 +37,7 @@ namespace Impostor.Server.Net.State
             GameManager gameManager,
             IPEndPoint publicIp,
             GameCode code,
-            GameOptionsData options,
+            IGameOptions options,
             ClientManager clientManager,
             IEventManager eventManager,
             IOptions<CompatibilityConfig> compatibilityConfig)
@@ -71,7 +72,7 @@ namespace Impostor.Server.Net.State
 
         public GameStates GameState { get; private set; }
 
-        public GameOptionsData Options { get; }
+        public IGameOptions Options { get; }
 
         public IDictionary<object, object> Items { get; }
 
index dff561515f72419bb2ebc65a0e56df6780afe9a2..990de641670c6fc2fcb89c742a636b72663034d0 100644 (file)
@@ -8,13 +8,14 @@ using Impostor.Api.Events.Managers;
 using Impostor.Api.Games;
 using Impostor.Api.Games.Managers;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.GameOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Custom;
 using Impostor.Api.Net.Messages;
+using Impostor.Hazel.Abstractions;
 using Impostor.Api.Net.Messages.C2S;
 using Impostor.Api.Utils;
 using Impostor.Hazel;
-using Impostor.Hazel.Abstractions;
 using Impostor.Hazel.Extensions;
 using Impostor.Server;
 using Impostor.Server.Events;
@@ -37,7 +38,7 @@ namespace Impostor.Tools.ServerReplay
     {
         private static readonly ILogger Logger = Log.ForContext(typeof(Program));
         private static readonly Dictionary<int, IHazelConnection> Connections = new Dictionary<int, IHazelConnection>();
-        private static readonly Dictionary<int, GameOptionsData> GameOptions = new Dictionary<int, GameOptionsData>();
+        private static readonly Dictionary<int, IGameOptions> GameOptions = new Dictionary<int, IGameOptions>();
 
         private static ServiceProvider _serviceProvider;
 
@@ -199,7 +200,8 @@ namespace Impostor.Tools.ServerReplay
 
                     if (tag == MessageFlags.HostGame)
                     {
-                        GameOptions.Add(clientId, Message00HostGameC2S.Deserialize(message));
+                        Message00HostGameC2S.Deserialize(message, out var gameOptions, out _, out _);
+                        GameOptions.Add(clientId, gameOptions);
                     }
                     else if (Connections.TryGetValue(clientId, out var client))
                     {