]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
v3.1.0
authorEisbison <eisbison.creator@gmail.com>
Wed, 17 Nov 2021 10:52:36 +0000 (11:52 +0100)
committerEisbison <eisbison.creator@gmail.com>
Wed, 17 Nov 2021 10:52:36 +0000 (11:52 +0100)
20 files changed:
README.md
TheOtherRoles/Buttons.cs
TheOtherRoles/CustomOptionHolder.cs [new file with mode: 0644]
TheOtherRoles/CustomOptions.cs [deleted file]
TheOtherRoles/Helpers.cs
TheOtherRoles/Main.cs
TheOtherRoles/MapOptions.cs
TheOtherRoles/Modules/CustomColors.cs
TheOtherRoles/Modules/CustomOptions.cs [new file with mode: 0644]
TheOtherRoles/Patches/CredentialsPatch.cs
TheOtherRoles/Patches/ExileControllerPatch.cs
TheOtherRoles/Patches/GameStartManagerPatch.cs
TheOtherRoles/Patches/MeetingPatch.cs
TheOtherRoles/Patches/PlayerControlPatch.cs
TheOtherRoles/Patches/UpdatePatch.cs
TheOtherRoles/Patches/UsablesPatch.cs
TheOtherRoles/RPC.cs
TheOtherRoles/Resources/PathfindButton.png [new file with mode: 0644]
TheOtherRoles/TheOtherRoles.cs
TheOtherRoles/TheOtherRoles.csproj

index e1876e141cc99ce78be0e9ebfd6319e67a866b97..c8e9135892eed79446a41d917dc7b7d0b4101822 100644 (file)
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 # Releases
 | Among Us - Version| Mod Version | Link |
 |----------|-------------|-----------------|
+| 2021.11.9.5s| v3.1.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.1.0/TheOtherRoles.zip)
 | 2021.11.9.5s| v3.0.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.0.0/TheOtherRoles.zip)
 | 2021.6.30s| v2.9.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.9.2/TheOtherRoles.zip)
 | 2021.6.30s| v2.9.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.9.1/TheOtherRoles.zip)
@@ -80,8 +81,21 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 <details>
   <summary>Click to show the Changelog</summary>
 
+**Version 3.1.0**
+- Hopefully temporary fixing the issue of being kicked by Innersloth servers for regular kills, until Innersloth fixes it on their side.
+- **NOTE:** Do not combine modded and unmodded versions of the game (even if you don't activate anything). Because of the kicking fix, your kills won't be performed for players that do not share the exact same modded version. Due to this you now can't start a game as the host, if not everyone in the lobby has the same version of the mod. Additionally you'll be kicked out of a lobby after 10 seconds, if the host doesn't have the mod installed (or the same mod version).
+- **Tracker:** The Tracker has been reworked by [Alex2911](https://github.com/Alex2911). The Tracker now has an additional optional ability that tracks all corpses on the map for a few seconds.
+- Add new option: Allow Parallel MedBay Scans
+- Add new [Guesser](#guesser) option: "Guesses Visible In Ghost Chat"
+- Add new [Guesser](#guesser) option: "Guesses Ignore The Medic Shield". If this option is set to false, no matter what the Guesser guessed, no one will die and the shielded player/Medic might be notified
+- Add new [Medic](#medic) option: "Medic Sees Murder Attempt On Shielded Player". This includes attempts from any kind of killer (Sheriff, Jackal, Guesser if the shield is not being ignored, ...)
+- During meetings the [Detective](#detective), [Hacker](#hacker) and [Medium](#medium) now display, whether a player wears a darker or lighter color
+- Bug fix: Bounty Hunter, Mini and Engineer in vent kills do not result in players being kicked anymore
+- Bug fix: The Trickster vent button now doesn't show the text "vent" twice anymore
+- Bug fix: Fixed the visual bug where both Lovers always showed dead during the meeting after a correct guess of one of them even if the option "Both Lovers Die" was disabled
+
 **Version 3.0.0**
-- Update to Among Us version v2021.11.9.5s
+- Updated to Among Us version v2021.11.9.5s
 - **Note:** We wanted to update as fast as possible, that's why you can't use both the Innersloth and mod roles at the same time. We'll make that possible in the future, but there are various things that need to be modified (e.g. Shifter, Guesser, ...) to make that work, so that'll take a little longer. Also, be aware that this version might contain more bugs than usual because Innersloth changed a lot of things and we might have missed some of them.
 - Ability buttons are now bind to the Q key (if it's a killing ability) or to the F key (otherwise). We'll make the binds adaptable in the future.
 - For now we removed the option "Jester Can Sabotage"
@@ -450,6 +464,7 @@ The mod adds a few new settings to Among Us (in addition to the role settings):
 - **Maximum Number Of Meetings:** You can set the maximum number of meetings that can be called in total (Every player still has personal maximum of buttons, but if the maximum number of meetings is reached you can't use your meetings even if you have some left. Impostor and Jackal meetings also count)
 - **Allow Skips On Emergency Meetings:** If set to false, there will not be a skip button in emergency meetings. If a player does not vote, he'll vote himself.
 - **Hide Player Names:** Hides the names of all players that have role which is unknown to you. Team Lovers/Impostors/Jackal still see the names of their teammates. Impostors can also see the name of the Spy and everyone can still see the age of the mini.
+- **Allow Parallel MedBay Scans:** Allows players to perform their MedBay scans at the same time
 - **Ghosts Can See Roles**
 - **Ghosts Can See Votes**
 - **Ghosts Can See The Number Of Remaining Tasks**
@@ -718,6 +733,7 @@ The Guesser can shoot players during the meeting, by guessing its role. If the g
 You can select how many players can be shot per game and if multiple players can be shot during a single meeting.\
 The guesses Impostor and Crewmate are only right, if the player is part of the corresponding team and has no special role.\
 You can only shoot during the voting time.\
+Depending on the options, the Guesser can't guess the shielded player and depending on the Medic options the Medic/shielded player might be notified (no one will die, independently of what the Guesser guessed).\
 \
 **NOTE:**
 - The vote of a shot player, won't be counted
@@ -731,8 +747,9 @@ You can only shoot during the voting time.\
 | Guesser Spawn Chance | -
 | Chance That The Guesser Is An Impostor | -
 | Guesser Number Of Shots Per Game| -
-| Guesser Number Of Shots Per Meeting| -
 | Guesser Can Shoot Multiple Times Per Meeting| -
+| Guesses Visible In Ghost Chat| -
+| Guesses Ignore The Medic Shield| -
 
 -----------------------
 
@@ -849,6 +866,7 @@ The Detective's other feature shows when they report a corpse: they receive clue
 - When people change their colors (because of a morph or camouflage), all the footprints also change their colors (also the ones that were already on the ground). If the effects are over, all footprints switch back to the original color.
 - The Detective does not see footprints of players that sit in vents
 - More information about the [colors](#colors)
+- During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names.
 
 ### Game Options
 | Name | Description |
@@ -903,9 +921,10 @@ The Mini cannot be killed until it turns 18 years old, however it can be voted o
 ### **Team: Crewmates**
 The Medic can shield (highlighted by an outline around the player) one player per game, which makes the player unkillable.\
 The shielded player can still be voted out and might also be an Impostor.\
-If set in the options, the shielded player will get a red flash on their screen if someone (Impostor, Sheriff, ...) tried to murder them.
+If set in the options, the shielded player and/or the Medic will get a red flash on their screen if someone (Impostor, Sheriff, ...) tried to murder them.
 If the Medic dies, the shield disappears with them.\
 The Sheriff will not die if they try to kill a shielded Crewmate and won't perform a kill if they try to kill a shielded Impostor.\
+Depending on the options, guesses from the Guesser will be blocked by the shield and the shielded player/medic might be notified.\
 The Medic's other feature shows when they report a corpse: they will see how long ago the player died.
 \
 **NOTE:**
@@ -921,6 +940,7 @@ The Medic's other feature shows when they report a corpse: they will see how lon
 | Show Shielded Player | Sets who sees if a player has a shield | "Everyone", "Shielded + Medic", "Medic"
 | Shielded Player Sees Murder Attempt| Whether a shielded player sees if someone tries to kill him | True/false |
 | Shield Will Be Set After Next Meeting | - | True/false
+| Medic Sees Murder Attempt On Shielded Player | - | If anyone tries to harm the shielded player (Impostor, Sheriff, Guesser, ...), the Medic will see a red flash
 -----------------------
 
 ## Mayor
@@ -944,6 +964,7 @@ Otherwise he sees the same information as everyone else.
 **NOTE:**
 - If the Morphling morphs or the Camouflager camouflages, the colors on the admin table change accordingly
 - More information about the [colors](#colors)
+- During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names.
 
 ### Game Options
 | Name | Description |
@@ -1020,10 +1041,10 @@ and can't fix lights and comms.
 
 ## Tracker
 ### **Team: Crewmates**
-The Tracker can select one player to track.
-Depending on the options the Tracker can track a different person after each meeting or the Tracker tracks the same person for the whole game.
+The Tracker can select one player to track. Depending on the options the Tracker can track a different person after each meeting or the Tracker tracks the same person for the whole game.
 An arrow points to the last tracked position of the player.
 The arrow updates its position every few seconds (configurable).
+Depending on the options, the Tracker has another ability: He can track all corpses on the map for a set amount of time. He will keep tracking corpses, even if they were cleaned or eaten by the Vulture.
 
 ### Game Options
 | Name | Description
@@ -1031,6 +1052,9 @@ The arrow updates its position every few seconds (configurable).
 | Tracker Spawn Chance | -
 | Tracker Update Interval | Sets how often the position is being updated
 | Tracker Reset Target After Meeting | -
+| Tracker Can Track Corpses | -
+| Corpses Tracking Cooldown | -
+| Corpses Tracking Duration | -
 -----------------------
 
 ## Snitch
@@ -1177,6 +1201,7 @@ an outline or all vents do).
 Created by [Mallöris](https://github.com/Mallaris)\
 \
 The medium is a crewmate who can ask the souls of dead players for information. Like the Seer, it sees the places where the players have died (after the next meeting) and can question them. It then gets random information about the soul or the killer in the chat. The souls only stay for one round, i.e. until the next meeting. Depending on the options, the souls can only be questioned once and then disappear.
+During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names.
 
 Questions:
 What is your Role?
index a3e5f91afd2235d724a4dc1ca6ee075213fd8f5d..f471fe7aa08b20323876eb7846a5211fcb044644 100644 (file)
@@ -19,7 +19,8 @@ namespace TheOtherRoles
         private static CustomButton morphlingButton;
         private static CustomButton camouflagerButton;
         private static CustomButton hackerButton;
-        private static CustomButton trackerButton;
+        private static CustomButton trackerTrackPlayerButton;
+        private static CustomButton trackerTrackCorpsesButton;
         private static CustomButton vampireKillButton;
         private static CustomButton garlicButton;
         private static CustomButton jackalKillButton;
@@ -48,7 +49,7 @@ namespace TheOtherRoles
             camouflagerButton.MaxTimer = Camouflager.cooldown;
             hackerButton.MaxTimer = Hacker.cooldown;
             vampireKillButton.MaxTimer = Vampire.cooldown;
-            trackerButton.MaxTimer = 0f;
+            trackerTrackPlayerButton.MaxTimer = 0f;
             garlicButton.MaxTimer = 0f;
             jackalKillButton.MaxTimer = Jackal.cooldown;
             sidekickKillButton.MaxTimer = Sidekick.cooldown;
@@ -63,6 +64,7 @@ namespace TheOtherRoles
             arsonistButton.MaxTimer = Arsonist.cooldown;
             vultureEatButton.MaxTimer = Vulture.cooldown;
             mediumButton.MaxTimer = Medium.cooldown;
+            trackerTrackCorpsesButton.MaxTimer = Tracker.corpsesTrackingCooldown;
 
             timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration;
             hackerButton.EffectDuration = Hacker.duration;
@@ -73,7 +75,7 @@ namespace TheOtherRoles
             lightsOutButton.EffectDuration = Trickster.lightsOutDuration;
             arsonistButton.EffectDuration = Arsonist.duration;
             mediumButton.EffectDuration = Medium.duration;
-
+            trackerTrackCorpsesButton.EffectDuration = Tracker.corpsesTrackingDuration;
             // Already set the timer to the max, as the button is enabled during the game and not available at the start
             lightsOutButton.Timer = lightsOutButton.MaxTimer;
         }
@@ -170,6 +172,7 @@ namespace TheOtherRoles
             // Sheriff Kill
             sheriffKillButton = new CustomButton(
                 () => {
+                    // Not using Helpers.checkMuderAttemptAndKill here, since the Sheriff can kill himself eventhough he has a shield, ...
                     if (Medic.shielded != null && Medic.shielded == Sheriff.currentTarget) {
                         MessageWriter attemptWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1);
                         AmongUsClient.Instance.FinishRpcImmediately(attemptWriter);
@@ -187,10 +190,12 @@ namespace TheOtherRoles
                     else {
                         targetId = PlayerControl.LocalPlayer.PlayerId;
                     }
-                    MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SheriffKill, Hazel.SendOption.Reliable, -1);
+
+                    MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
+                    killWriter.Write(Sheriff.sheriff.Data.PlayerId);
                     killWriter.Write(targetId);
                     AmongUsClient.Instance.FinishRpcImmediately(killWriter);
-                    RPCProcedure.sheriffKill(targetId);
+                    RPCProcedure.uncheckedMurderPlayer(Sheriff.sheriff.Data.PlayerId, targetId);
 
                     sheriffKillButton.Timer = sheriffKillButton.MaxTimer; 
                     Sheriff.currentTarget = null;
@@ -353,7 +358,7 @@ namespace TheOtherRoles
             );
 
             // Tracker button
-            trackerButton = new CustomButton(
+            trackerTrackPlayerButton = new CustomButton(
                 () => {
                     MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.TrackerUsedTracker, Hazel.SendOption.Reliable, -1);
                     writer.Write(Tracker.currentTarget.PlayerId);
@@ -369,9 +374,29 @@ namespace TheOtherRoles
                 KeyCode.F
             );
 
+            trackerTrackCorpsesButton = new CustomButton(
+                () => { Tracker.corpsesTrackingTimer = Tracker.corpsesTrackingDuration; },
+                () => { return Tracker.tracker != null && Tracker.tracker == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead && Tracker.canTrackCorpses; },
+                () => { return PlayerControl.LocalPlayer.CanMove; },
+                () => {
+                    trackerTrackCorpsesButton.Timer = trackerTrackCorpsesButton.MaxTimer;
+                    trackerTrackCorpsesButton.isEffectActive = false;
+                    trackerTrackCorpsesButton.actionButton.cooldownTimerText.color = Palette.EnabledColor;
+                },
+                Tracker.getTrackCorpsesButtonSprite(),
+                new Vector3(-2.7f, -0.06f, 0),
+                __instance,
+                KeyCode.Q,
+                true,
+                Tracker.corpsesTrackingDuration,
+                () => {
+                    trackerTrackCorpsesButton.Timer = trackerTrackCorpsesButton.MaxTimer;
+                }
+            );
+    
             vampireKillButton = new CustomButton(
                 () => {
-                    if (Helpers.handleMurderAttempt(Vampire.vampire, Vampire.currentTarget)) {
+                    if (Helpers.checkMuderAttempt(Vampire.vampire, Vampire.currentTarget)) {
                         if (Vampire.targetNearGarlic) {
                             MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
                             writer.Write(Vampire.vampire.PlayerId);
@@ -392,19 +417,13 @@ namespace TheOtherRoles
 
                             HudManager.Instance.StartCoroutine(Effects.Lerp(Vampire.delay, new Action<float>((p) => { // Delayed action
                                 if (p == 1f) {
-                                    if (Vampire.bitten != null && !Vampire.bitten.Data.IsDead && Helpers.handleMurderAttempt(Vampire.vampire, Vampire.bitten)) {
-                                        // Perform kill
-                                        MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireTryKill, Hazel.SendOption.Reliable, -1);
-                                        AmongUsClient.Instance.FinishRpcImmediately(killWriter);
-                                        RPCProcedure.vampireTryKill();
-                                    } else {
-                                        // Notify players about clearing bitten
-                                        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
-                                        writer.Write(byte.MaxValue);
-                                        writer.Write(byte.MaxValue);
-                                        AmongUsClient.Instance.FinishRpcImmediately(writer);
-                                        RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
-                                    }
+                                    // Perform kill if possible and reset bitten (regardless whether the kill was successful or not)
+                                    Helpers.checkMuderAttemptAndKill(Vampire.vampire, Vampire.bitten);
+                                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
+                                    writer.Write(byte.MaxValue);
+                                    writer.Write(byte.MaxValue);
+                                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                                    RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
                                 }
                             })));
 
@@ -486,12 +505,8 @@ namespace TheOtherRoles
             // Jackal Kill
             jackalKillButton = new CustomButton(
                 () => {
-                    if (!Helpers.handleMurderAttempt(Jackal.jackal, Jackal.currentTarget)) return;
-                    byte targetId = Jackal.currentTarget.PlayerId;
-                    MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.JackalKill, Hazel.SendOption.Reliable, -1);
-                    killWriter.Write(targetId);
-                    AmongUsClient.Instance.FinishRpcImmediately(killWriter);
-                    RPCProcedure.jackalKill(targetId);
+                    if (!Helpers.checkMuderAttemptAndKill(Jackal.jackal, Jackal.currentTarget)) return;
+
                     jackalKillButton.Timer = jackalKillButton.MaxTimer; 
                     Jackal.currentTarget = null;
                 },
@@ -507,12 +522,7 @@ namespace TheOtherRoles
             // Sidekick Kill
             sidekickKillButton = new CustomButton(
                 () => {
-                    if (!Helpers.handleMurderAttempt(Sidekick.sidekick, Sidekick.currentTarget)) return;
-                    byte targetId = Sidekick.currentTarget.PlayerId;
-                    MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SidekickKill, Hazel.SendOption.Reliable, -1);
-                    killWriter.Write(targetId);
-                    AmongUsClient.Instance.FinishRpcImmediately(killWriter);
-                    RPCProcedure.sidekickKill(targetId);
+                    if (!Helpers.checkMuderAttemptAndKill(Sidekick.sidekick, Sidekick.currentTarget)) return;
 
                     sidekickKillButton.Timer = sidekickKillButton.MaxTimer; 
                     Sidekick.currentTarget = null;
@@ -656,8 +666,9 @@ namespace TheOtherRoles
                         Warlock.curseVictim = Warlock.currentTarget;
                         warlockCurseButton.Sprite = Warlock.getCurseKillButtonSprite();
                         warlockCurseButton.Timer = 1f;
-                    } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null && Helpers.handleMurderAttempt(Warlock.warlock, Warlock.curseVictimTarget)) {
+                    } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null && Helpers.checkMuderAttempt(Warlock.warlock, Warlock.curseVictimTarget)) {
                         // Curse Kill
+                        // Not using Helpers.checkMuderAttemptAndKill here directly, since we need to share the Warlock.curseKillTarget
                         Warlock.curseKillTarget = Warlock.curseVictimTarget;
 
                         MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.WarlockCurseKill, Hazel.SendOption.Reliable, -1);
diff --git a/TheOtherRoles/CustomOptionHolder.cs b/TheOtherRoles/CustomOptionHolder.cs
new file mode 100644 (file)
index 0000000..0e08a33
--- /dev/null
@@ -0,0 +1,401 @@
+using System.Collections.Generic;
+using UnityEngine;
+using BepInEx.Configuration;
+using System;
+using System.Linq;
+using HarmonyLib;
+using Hazel;
+using System.Reflection;
+using System.Text;
+using static TheOtherRoles.TheOtherRoles;
+
+namespace TheOtherRoles {
+    public class CustomOptionHolder {
+        public static string[] rates = new string[]{"0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%"};
+        public static string[] presets = new string[]{"Preset 1", "Preset 2", "Preset 3", "Preset 4", "Preset 5"};
+
+        public static CustomOption presetSelection;
+        public static CustomOption activateRoles;
+        public static CustomOption crewmateRolesCountMin;
+        public static CustomOption crewmateRolesCountMax;
+        public static CustomOption neutralRolesCountMin;
+        public static CustomOption neutralRolesCountMax;
+        public static CustomOption impostorRolesCountMin;
+        public static CustomOption impostorRolesCountMax;
+
+        public static CustomOption mafiaSpawnRate;
+        public static CustomOption janitorCooldown;
+
+        public static CustomOption morphlingSpawnRate;
+        public static CustomOption morphlingCooldown;
+        public static CustomOption morphlingDuration;
+
+        public static CustomOption camouflagerSpawnRate;
+        public static CustomOption camouflagerCooldown;
+        public static CustomOption camouflagerDuration;
+
+        public static CustomOption vampireSpawnRate;
+        public static CustomOption vampireKillDelay;
+        public static CustomOption vampireCooldown;
+        public static CustomOption vampireCanKillNearGarlics;
+
+        public static CustomOption eraserSpawnRate;
+        public static CustomOption eraserCooldown;
+        public static CustomOption eraserCanEraseAnyone;
+
+        public static CustomOption miniSpawnRate;
+        public static CustomOption miniGrowingUpDuration;
+
+        public static CustomOption loversSpawnRate;
+        public static CustomOption loversImpLoverRate;
+        public static CustomOption loversBothDie;
+        public static CustomOption loversCanHaveAnotherRole;
+
+        public static CustomOption guesserSpawnRate;
+        public static CustomOption guesserIsImpGuesserRate;
+        public static CustomOption guesserNumberOfShots;
+        public static CustomOption guesserHasMultipleShotsPerMeeting;
+        public static CustomOption guesserShowInfoInGhostChat;
+        public static CustomOption guesserKillsThroughShield;
+
+        public static CustomOption jesterSpawnRate;
+        public static CustomOption jesterCanCallEmergency;
+
+        public static CustomOption arsonistSpawnRate;
+        public static CustomOption arsonistCooldown;
+        public static CustomOption arsonistDuration;
+
+        public static CustomOption jackalSpawnRate;
+        public static CustomOption jackalKillCooldown;
+        public static CustomOption jackalCreateSidekickCooldown;
+        public static CustomOption jackalCanUseVents;
+        public static CustomOption jackalCanCreateSidekick;
+        public static CustomOption sidekickPromotesToJackal;
+        public static CustomOption sidekickCanKill;
+        public static CustomOption sidekickCanUseVents;
+        public static CustomOption jackalPromotedFromSidekickCanCreateSidekick;
+        public static CustomOption jackalCanCreateSidekickFromImpostor;
+        public static CustomOption jackalAndSidekickHaveImpostorVision;
+
+        public static CustomOption bountyHunterSpawnRate;
+        public static CustomOption bountyHunterBountyDuration;
+        public static CustomOption bountyHunterReducedCooldown;
+        public static CustomOption bountyHunterPunishmentTime;
+        public static CustomOption bountyHunterShowArrow;
+        public static CustomOption bountyHunterArrowUpdateIntervall;
+
+        public static CustomOption shifterSpawnRate;
+        public static CustomOption shifterShiftsModifiers;
+
+        public static CustomOption mayorSpawnRate;
+
+        public static CustomOption engineerSpawnRate;
+        public static CustomOption engineerNumberOfFixes;
+        public static CustomOption engineerHighlightForImpostors;
+        public static CustomOption engineerHighlightForTeamJackal;
+
+        public static CustomOption sheriffSpawnRate;
+        public static CustomOption sheriffCooldown;
+        public static CustomOption sheriffCanKillNeutrals;
+
+        public static CustomOption lighterSpawnRate;
+        public static CustomOption lighterModeLightsOnVision;
+        public static CustomOption lighterModeLightsOffVision;
+        public static CustomOption lighterCooldown;
+        public static CustomOption lighterDuration;
+
+        public static CustomOption detectiveSpawnRate;
+        public static CustomOption detectiveAnonymousFootprints;
+        public static CustomOption detectiveFootprintIntervall;
+        public static CustomOption detectiveFootprintDuration;
+        public static CustomOption detectiveReportNameDuration;
+        public static CustomOption detectiveReportColorDuration;
+
+        public static CustomOption timeMasterSpawnRate;
+        public static CustomOption timeMasterCooldown;
+        public static CustomOption timeMasterRewindTime;
+        public static CustomOption timeMasterShieldDuration;
+
+        public static CustomOption medicSpawnRate;
+        public static CustomOption medicShowShielded;
+        public static CustomOption medicShowAttemptToShielded;
+        public static CustomOption medicSetShieldAfterMeeting;
+        public static CustomOption medicShowAttemptToMedic;
+
+        public static CustomOption swapperSpawnRate;
+        public static CustomOption swapperCanCallEmergency;
+        public static CustomOption swapperCanOnlySwapOthers;
+
+        public static CustomOption seerSpawnRate;
+        public static CustomOption seerMode;
+        public static CustomOption seerSoulDuration;
+        public static CustomOption seerLimitSoulDuration;
+
+        public static CustomOption hackerSpawnRate;
+        public static CustomOption hackerCooldown;
+        public static CustomOption hackerHackeringDuration;
+        public static CustomOption hackerOnlyColorType;
+
+        public static CustomOption trackerSpawnRate;
+        public static CustomOption trackerUpdateIntervall;
+        public static CustomOption trackerResetTargetAfterMeeting;
+        public static CustomOption trackerCanTrackCorpses;
+        public static CustomOption trackerCorpsesTrackingCooldown;
+        public static CustomOption trackerCorpsesTrackingDuration;
+
+        public static CustomOption snitchSpawnRate;
+        public static CustomOption snitchLeftTasksForReveal;
+        public static CustomOption snitchIncludeTeamJackal;
+        public static CustomOption snitchTeamJackalUseDifferentArrowColor;
+
+        public static CustomOption spySpawnRate;
+        public static CustomOption spyCanDieToSheriff;
+        public static CustomOption spyImpostorsCanKillAnyone;
+        public static CustomOption spyCanEnterVents;
+        public static CustomOption spyHasImpostorVision;
+
+        public static CustomOption tricksterSpawnRate;
+        public static CustomOption tricksterPlaceBoxCooldown;
+        public static CustomOption tricksterLightsOutCooldown;
+        public static CustomOption tricksterLightsOutDuration;
+
+        public static CustomOption cleanerSpawnRate;
+        public static CustomOption cleanerCooldown;
+        
+        public static CustomOption warlockSpawnRate;
+        public static CustomOption warlockCooldown;
+        public static CustomOption warlockRootTime;
+
+        public static CustomOption securityGuardSpawnRate;
+        public static CustomOption securityGuardCooldown;
+        public static CustomOption securityGuardTotalScrews;
+        public static CustomOption securityGuardCamPrice;
+        public static CustomOption securityGuardVentPrice;
+
+        public static CustomOption baitSpawnRate;
+        public static CustomOption baitHighlightAllVents;
+        public static CustomOption baitReportDelay;
+
+        public static CustomOption vultureSpawnRate;
+        public static CustomOption vultureCooldown;
+        public static CustomOption vultureNumberToWin;
+        public static CustomOption vultureCanUseVents;
+        public static CustomOption vultureShowArrows;
+
+        public static CustomOption mediumSpawnRate;
+        public static CustomOption mediumCooldown;
+        public static CustomOption mediumDuration;
+        public static CustomOption mediumOneTimeUse;
+
+        public static CustomOption maxNumberOfMeetings;
+        public static CustomOption blockSkippingInEmergencyMeetings;
+        public static CustomOption noVoteIsSelfVote;
+        public static CustomOption hidePlayerNames;
+        public static CustomOption allowParallelMedBayScans;
+
+
+        internal static Dictionary<byte, byte[]> blockedRolePairings = new Dictionary<byte, byte[]>();
+
+        public static string cs(Color c, string s) {
+            return string.Format("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s);
+        }
+        private static byte ToByte(float f) {
+            f = Mathf.Clamp01(f);
+            return (byte)(f * 255);
+        }
+
+        public static void Load() {
+            
+            // Role Options
+            presetSelection = CustomOption.Create(0, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true);
+            activateRoles = CustomOption.Create(7, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Enable Mod Roles And Block Vanilla Roles"), true, null, true);
+
+            // Using new id's for the options to not break compatibilty with older versions
+            crewmateRolesCountMin = CustomOption.Create(300, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 0f, 0f, 15f, 1f, null, true);
+            crewmateRolesCountMax = CustomOption.Create(301, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 0f, 0f, 15f, 1f);
+            neutralRolesCountMin = CustomOption.Create(302, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Neutral Roles"), 0f, 0f, 15f, 1f);
+            neutralRolesCountMax = CustomOption.Create(303, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Neutral Roles"), 0f, 0f, 15f, 1f);
+            impostorRolesCountMin = CustomOption.Create(304, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Impostor Roles"), 0f, 0f, 3f, 1f);
+            impostorRolesCountMax = CustomOption.Create(305, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Impostor Roles"), 0f, 0f, 3f, 1f);
+
+            mafiaSpawnRate = CustomOption.Create(10, cs(Janitor.color, "Mafia"), rates, null, true);
+            janitorCooldown = CustomOption.Create(11, "Janitor Cooldown", 30f, 10f, 60f, 2.5f, mafiaSpawnRate);
+
+            morphlingSpawnRate = CustomOption.Create(20, cs(Morphling.color, "Morphling"), rates, null, true);
+            morphlingCooldown = CustomOption.Create(21, "Morphling Cooldown", 30f, 10f, 60f, 2.5f, morphlingSpawnRate);
+            morphlingDuration = CustomOption.Create(22, "Morph Duration", 10f, 1f, 20f, 0.5f, morphlingSpawnRate);
+
+            camouflagerSpawnRate = CustomOption.Create(30, cs(Camouflager.color, "Camouflager"), rates, null, true);
+            camouflagerCooldown = CustomOption.Create(31, "Camouflager Cooldown", 30f, 10f, 60f, 2.5f, camouflagerSpawnRate);
+            camouflagerDuration = CustomOption.Create(32, "Camo Duration", 10f, 1f, 20f, 0.5f, camouflagerSpawnRate);
+
+            vampireSpawnRate = CustomOption.Create(40, cs(Vampire.color, "Vampire"), rates, null, true);
+            vampireKillDelay = CustomOption.Create(41, "Vampire Kill Delay", 10f, 1f, 20f, 1f, vampireSpawnRate);
+            vampireCooldown = CustomOption.Create(42, "Vampire Cooldown", 30f, 10f, 60f, 2.5f, vampireSpawnRate);
+            vampireCanKillNearGarlics = CustomOption.Create(43, "Vampire Can Kill Near Garlics", true, vampireSpawnRate);
+
+            eraserSpawnRate = CustomOption.Create(230, cs(Eraser.color, "Eraser"), rates, null, true);
+            eraserCooldown = CustomOption.Create(231, "Eraser Cooldown", 30f, 10f, 120f, 5f, eraserSpawnRate);
+            eraserCanEraseAnyone = CustomOption.Create(232, "Eraser Can Erase Anyone", false, eraserSpawnRate);
+
+            tricksterSpawnRate = CustomOption.Create(250, cs(Trickster.color, "Trickster"), rates, null, true);
+            tricksterPlaceBoxCooldown = CustomOption.Create(251, "Trickster Box Cooldown", 10f, 0f, 30f, 2.5f, tricksterSpawnRate);
+            tricksterLightsOutCooldown = CustomOption.Create(252, "Trickster Lights Out Cooldown", 30f, 10f, 60f, 5f, tricksterSpawnRate);
+            tricksterLightsOutDuration = CustomOption.Create(253, "Trickster Lights Out Duration", 15f, 5f, 60f, 2.5f, tricksterSpawnRate);
+
+            cleanerSpawnRate = CustomOption.Create(260, cs(Cleaner.color, "Cleaner"), rates, null, true);
+            cleanerCooldown = CustomOption.Create(261, "Cleaner Cooldown", 30f, 10f, 60f, 2.5f, cleanerSpawnRate);
+
+            warlockSpawnRate = CustomOption.Create(270, cs(Cleaner.color, "Warlock"), rates, null, true);
+            warlockCooldown = CustomOption.Create(271, "Warlock Cooldown", 30f, 10f, 60f, 2.5f, warlockSpawnRate);
+            warlockRootTime = CustomOption.Create(272, "Warlock Root Time", 5f, 0f, 15f, 1f, warlockSpawnRate);
+
+            bountyHunterSpawnRate = CustomOption.Create(320, cs(BountyHunter.color, "Bounty Hunter"), rates, null, true);
+            bountyHunterBountyDuration = CustomOption.Create(321, "Duration After Which Bounty Changes",  60f, 10f, 180f, 10f, bountyHunterSpawnRate);
+            bountyHunterReducedCooldown = CustomOption.Create(322, "Cooldown After Killing Bounty", 2.5f, 0f, 30f, 2.5f, bountyHunterSpawnRate);
+            bountyHunterPunishmentTime = CustomOption.Create(323, "Additional Cooldown After Killing Others", 20f, 0f, 60f, 2.5f, bountyHunterSpawnRate);
+            bountyHunterShowArrow = CustomOption.Create(324, "Show Arrow Pointing Towards The Bounty", true, bountyHunterSpawnRate);
+            bountyHunterArrowUpdateIntervall = CustomOption.Create(325, "Arrow Update Intervall", 15f, 2.5f, 60f, 2.5f, bountyHunterShowArrow);
+
+
+            miniSpawnRate = CustomOption.Create(180, cs(Mini.color, "Mini"), rates, null, true);
+            miniGrowingUpDuration = CustomOption.Create(181, "Mini Growing Up Duration", 400f, 100f, 1500f, 100f, miniSpawnRate);
+
+            loversSpawnRate = CustomOption.Create(50, cs(Lovers.color, "Lovers"), rates, null, true);
+            loversImpLoverRate = CustomOption.Create(51, "Chance That One Lover Is Impostor", rates, loversSpawnRate);
+            loversBothDie = CustomOption.Create(52, "Both Lovers Die", true, loversSpawnRate);
+            loversCanHaveAnotherRole = CustomOption.Create(53, "Lovers Can Have Another Role", true, loversSpawnRate);
+
+            guesserSpawnRate = CustomOption.Create(310, cs(Guesser.color, "Guesser"), rates, null, true);
+            guesserIsImpGuesserRate = CustomOption.Create(311, "Chance That The Guesser Is An Impostor", rates, guesserSpawnRate);
+            guesserNumberOfShots = CustomOption.Create(312, "Guesser Number Of Shots", 2f, 1f, 15f, 1f, guesserSpawnRate);
+            guesserHasMultipleShotsPerMeeting = CustomOption.Create(313, "Guesser Can Shoot Multiple Times Per Meeting", false, guesserSpawnRate);
+            guesserShowInfoInGhostChat = CustomOption.Create(314, "Guesses Visible In Ghost Chat", true, guesserSpawnRate);
+            guesserKillsThroughShield  = CustomOption.Create(315, "Guesses Ignore The Medic Shield", true, guesserSpawnRate);
+
+            jesterSpawnRate = CustomOption.Create(60, cs(Jester.color, "Jester"), rates, null, true);
+            jesterCanCallEmergency = CustomOption.Create(61, "Jester can call emergency meeting", true, jesterSpawnRate);
+
+            arsonistSpawnRate = CustomOption.Create(290, cs(Arsonist.color, "Arsonist"), rates, null, true);
+            arsonistCooldown = CustomOption.Create(291, "Arsonist Cooldown", 12.5f, 2.5f, 60f, 2.5f, arsonistSpawnRate);
+            arsonistDuration = CustomOption.Create(292, "Arsonist Douse Duration", 3f, 1f, 10f, 1f, arsonistSpawnRate);
+
+            jackalSpawnRate = CustomOption.Create(220, cs(Jackal.color, "Jackal"), rates, null, true);
+            jackalKillCooldown = CustomOption.Create(221, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
+            jackalCreateSidekickCooldown = CustomOption.Create(222, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
+            jackalCanUseVents = CustomOption.Create(223, "Jackal Can Use Vents", true, jackalSpawnRate);
+            jackalCanCreateSidekick = CustomOption.Create(224, "Jackal Can Create A Sidekick", false, jackalSpawnRate);
+            sidekickPromotesToJackal = CustomOption.Create(225, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalSpawnRate);
+            sidekickCanKill = CustomOption.Create(226, "Sidekick Can Kill", false, jackalSpawnRate);
+            sidekickCanUseVents = CustomOption.Create(227, "Sidekick Can Use Vents", true, jackalSpawnRate);
+            jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, "Jackals Promoted From Sidekick Can Create A Sidekick", true, jackalSpawnRate);
+            jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, "Jackals Can Make An Impostor To His Sidekick", true, jackalSpawnRate);
+            jackalAndSidekickHaveImpostorVision = CustomOption.Create(430, "Jackal And Sidekick Have Impostor Vision", false, jackalSpawnRate);
+
+            vultureSpawnRate = CustomOption.Create(340, cs(Vulture.color, "Vulture"), rates, null, true);
+            vultureCooldown = CustomOption.Create(341, "Vulture Cooldown", 15f, 10f, 60f, 2.5f, vultureSpawnRate);
+            vultureNumberToWin = CustomOption.Create(342, "Number Of Corpses Needed To Be Eaten", 4f, 0f, 10f, 1f, vultureSpawnRate);
+            vultureCanUseVents = CustomOption.Create(343, "Vulture Can Use Vents", true, vultureSpawnRate);
+            vultureShowArrows = CustomOption.Create(344, "Show Arrows Pointing Towards The Corpes", true, vultureSpawnRate);
+
+            shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true);
+            shifterShiftsModifiers = CustomOption.Create(71, "Shifter Shifts Modifiers", false, shifterSpawnRate);
+
+            mayorSpawnRate = CustomOption.Create(80, cs(Mayor.color, "Mayor"), rates, null, true);
+
+            engineerSpawnRate = CustomOption.Create(90, cs(Engineer.color, "Engineer"), rates, null, true);
+            engineerNumberOfFixes = CustomOption.Create(91, "Number Of Sabotage Fixes", 1f, 0f, 3f, 1f, engineerSpawnRate);
+            engineerHighlightForImpostors = CustomOption.Create(92, "Impostors See Vents Highlighted", true, engineerSpawnRate);
+            engineerHighlightForTeamJackal = CustomOption.Create(93, "Jackal and Sidekick See Vents Highlighted ", true, engineerSpawnRate);
+
+            sheriffSpawnRate = CustomOption.Create(100, cs(Sheriff.color, "Sheriff"), rates, null, true);
+            sheriffCooldown = CustomOption.Create(101, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate);
+            sheriffCanKillNeutrals = CustomOption.Create(102, "Sheriff Can Kill Neutrals", false, sheriffSpawnRate);
+
+            lighterSpawnRate = CustomOption.Create(110, cs(Lighter.color, "Lighter"), rates, null, true);
+            lighterModeLightsOnVision = CustomOption.Create(111, "Lighter Mode Vision On Lights On", 2f, 0.25f, 5f, 0.25f, lighterSpawnRate);
+            lighterModeLightsOffVision = CustomOption.Create(112, "Lighter Mode Vision On Lights Off", 0.75f, 0.25f, 5f, 0.25f, lighterSpawnRate);
+            lighterCooldown = CustomOption.Create(113, "Lighter Cooldown", 30f, 5f, 120f, 5f, lighterSpawnRate);
+            lighterDuration = CustomOption.Create(114, "Lighter Duration", 5f, 2.5f, 60f, 2.5f, lighterSpawnRate);
+
+            detectiveSpawnRate = CustomOption.Create(120, cs(Detective.color, "Detective"), rates, null, true);
+            detectiveAnonymousFootprints = CustomOption.Create(121, "Anonymous Footprints", false, detectiveSpawnRate); 
+            detectiveFootprintIntervall = CustomOption.Create(122, "Footprint Intervall", 0.5f, 0.25f, 10f, 0.25f, detectiveSpawnRate);
+            detectiveFootprintDuration = CustomOption.Create(123, "Footprint Duration", 5f, 0.25f, 10f, 0.25f, detectiveSpawnRate);
+            detectiveReportNameDuration = CustomOption.Create(124, "Time Where Detective Reports Will Have Name", 0, 0, 60, 2.5f, detectiveSpawnRate);
+            detectiveReportColorDuration = CustomOption.Create(125, "Time Where Detective Reports Will Have Color Type", 20, 0, 120, 2.5f, detectiveSpawnRate);
+
+            timeMasterSpawnRate = CustomOption.Create(130, cs(TimeMaster.color, "Time Master"), rates, null, true);
+            timeMasterCooldown = CustomOption.Create(131, "Time Master Cooldown", 30f, 10f, 120f, 2.5f, timeMasterSpawnRate);
+            timeMasterRewindTime = CustomOption.Create(132, "Rewind Time", 3f, 1f, 10f, 1f, timeMasterSpawnRate);
+            timeMasterShieldDuration = CustomOption.Create(133, "Time Master Shield Duration", 3f, 1f, 20f, 1f, timeMasterSpawnRate);
+
+            medicSpawnRate = CustomOption.Create(140, cs(Medic.color, "Medic"), rates, null, true);
+            medicShowShielded = CustomOption.Create(143, "Show Shielded Player", new string[] {"Everyone", "Shielded + Medic", "Medic"}, medicSpawnRate);
+            medicShowAttemptToShielded = CustomOption.Create(144, "Shielded Player Sees Murder Attempt", false, medicSpawnRate);
+            medicSetShieldAfterMeeting = CustomOption.Create(145, "Shield Will Be Set After The Next Meeting", false, medicSpawnRate);
+            medicShowAttemptToMedic = CustomOption.Create(146, "Medic Sees Murder Attempt On Shielded Player", false, medicSpawnRate);
+
+            swapperSpawnRate = CustomOption.Create(150, cs(Swapper.color, "Swapper"), rates, null, true);
+            swapperCanCallEmergency = CustomOption.Create(151, "Swapper can call emergency meeting", false, swapperSpawnRate);
+            swapperCanOnlySwapOthers = CustomOption.Create(152, "Swapper can only swap others", false, swapperSpawnRate);
+
+            seerSpawnRate = CustomOption.Create(160, cs(Seer.color, "Seer"), rates, null, true);
+            seerMode = CustomOption.Create(161, "Seer Mode", new string[]{ "Show Death Flash + Souls", "Show Death Flash", "Show Souls"}, seerSpawnRate);
+            seerLimitSoulDuration = CustomOption.Create(163, "Seer Limit Soul Duration", false, seerSpawnRate);
+            seerSoulDuration = CustomOption.Create(162, "Seer Soul Duration", 15f, 0f, 60f, 5f, seerLimitSoulDuration);
+        
+            hackerSpawnRate = CustomOption.Create(170, cs(Hacker.color, "Hacker"), rates, null, true);
+            hackerCooldown = CustomOption.Create(171, "Hacker Cooldown", 30f, 0f, 60f, 5f, hackerSpawnRate);
+            hackerHackeringDuration = CustomOption.Create(172, "Hacker Duration", 10f, 2.5f, 60f, 2.5f, hackerSpawnRate);
+            hackerOnlyColorType = CustomOption.Create(173, "Hacker Only Sees Color Type", false, hackerSpawnRate);
+
+            trackerSpawnRate = CustomOption.Create(200, cs(Tracker.color, "Tracker"), rates, null, true);
+            trackerUpdateIntervall = CustomOption.Create(201, "Tracker Update Intervall", 5f, 2.5f, 30f, 2.5f, trackerSpawnRate);
+            trackerResetTargetAfterMeeting = CustomOption.Create(202, "Tracker Reset Target After Meeting", false, trackerSpawnRate);
+            trackerCanTrackCorpses = CustomOption.Create(203, "Tracker Can Track Corpses", true, trackerSpawnRate);
+            trackerCorpsesTrackingCooldown = CustomOption.Create(204, "Corpses Tracking Cooldown", 30f, 0f, 120f, 5f, trackerCanTrackCorpses);
+            trackerCorpsesTrackingDuration = CustomOption.Create(205, "Corpses Tracking Duration", 5f, 2.5f, 30f, 2.5f, trackerCanTrackCorpses);
+                           
+            snitchSpawnRate = CustomOption.Create(210, cs(Snitch.color, "Snitch"), rates, null, true);
+            snitchLeftTasksForReveal = CustomOption.Create(211, "Task Count Where The Snitch Will Be Revealed", 1f, 0f, 5f, 1f, snitchSpawnRate);
+            snitchIncludeTeamJackal = CustomOption.Create(212, "Include Team Jackal", false, snitchSpawnRate);
+            snitchTeamJackalUseDifferentArrowColor = CustomOption.Create(213, "Use Different Arrow Color For Team Jackal", true, snitchIncludeTeamJackal);
+
+            spySpawnRate = CustomOption.Create(240, cs(Spy.color, "Spy"), rates, null, true);
+            spyCanDieToSheriff = CustomOption.Create(241, "Spy Can Die To Sheriff", false, spySpawnRate);
+            spyImpostorsCanKillAnyone = CustomOption.Create(242, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate);
+            spyCanEnterVents = CustomOption.Create(243, "Spy Can Enter Vents", false, spySpawnRate);
+            spyHasImpostorVision = CustomOption.Create(244, "Spy Has Impostor Vision", false, spySpawnRate);
+
+            securityGuardSpawnRate = CustomOption.Create(280, cs(SecurityGuard.color, "Security Guard"), rates, null, true);
+            securityGuardCooldown = CustomOption.Create(281, "Security Guard Cooldown", 30f, 10f, 60f, 2.5f, securityGuardSpawnRate);
+            securityGuardTotalScrews = CustomOption.Create(282, "Security Guard Number Of Screws", 7f, 1f, 15f, 1f, securityGuardSpawnRate);
+            securityGuardCamPrice = CustomOption.Create(283, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate);
+            securityGuardVentPrice = CustomOption.Create(284, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate);
+
+            baitSpawnRate = CustomOption.Create(330, cs(Bait.color, "Bait"), rates, null, true);
+            baitHighlightAllVents = CustomOption.Create(331, "Highlight All Vents If A Vent Is Occupied", false, baitSpawnRate);
+            baitReportDelay = CustomOption.Create(332, "Bait Report Delay", 0f, 0f, 10f, 1f, baitSpawnRate);
+
+            mediumSpawnRate = CustomOption.Create(360, cs(Medium.color, "Medium"), rates, null, true);
+            mediumCooldown = CustomOption.Create(361, "Medium Questioning Cooldown", 30f, 5f, 120f, 5f, mediumSpawnRate);
+            mediumDuration = CustomOption.Create(362, "Medium Questioning Duration", 3f, 0f, 15f, 1f, mediumSpawnRate);
+            mediumOneTimeUse = CustomOption.Create(363, "Each Soul Can Only Be Questioned Once", false, mediumSpawnRate);
+
+            // Other options
+            maxNumberOfMeetings = CustomOption.Create(3, "Number Of Meetings (excluding Mayor meeting)", 10, 0, 15, 1, null, true);
+            blockSkippingInEmergencyMeetings = CustomOption.Create(4, "Block Skipping In Emergency Meetings", false);
+            noVoteIsSelfVote = CustomOption.Create(5, "No Vote Is Self Vote", false, blockSkippingInEmergencyMeetings);
+            hidePlayerNames = CustomOption.Create(6, "Hide Player Names", false);
+            allowParallelMedBayScans = CustomOption.Create(7, "Allow Parallel MedBay Scans", false);
+
+            blockedRolePairings.Add((byte)RoleId.Vampire, new [] { (byte)RoleId.Warlock});
+            blockedRolePairings.Add((byte)RoleId.Warlock, new [] { (byte)RoleId.Vampire});
+            blockedRolePairings.Add((byte)RoleId.Spy, new [] { (byte)RoleId.Mini});
+            blockedRolePairings.Add((byte)RoleId.Mini, new [] { (byte)RoleId.Spy});
+            
+        }
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/CustomOptions.cs b/TheOtherRoles/CustomOptions.cs
deleted file mode 100644 (file)
index 30d9fee..0000000
+++ /dev/null
@@ -1,806 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using BepInEx.Configuration;
-using System;
-using System.Linq;
-using HarmonyLib;
-using Hazel;
-using System.Reflection;
-using System.Text;
-using static TheOtherRoles.TheOtherRoles;
-
-namespace TheOtherRoles {
-    public class CustomOptionHolder {
-        public static string[] rates = new string[]{"0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%"};
-        public static string[] presets = new string[]{"Preset 1", "Preset 2", "Preset 3", "Preset 4", "Preset 5"};
-
-        public static CustomOption presetSelection;
-        public static CustomOption activateRoles;
-        public static CustomOption crewmateRolesCountMin;
-        public static CustomOption crewmateRolesCountMax;
-        public static CustomOption neutralRolesCountMin;
-        public static CustomOption neutralRolesCountMax;
-        public static CustomOption impostorRolesCountMin;
-        public static CustomOption impostorRolesCountMax;
-
-        public static CustomOption mafiaSpawnRate;
-        public static CustomOption janitorCooldown;
-
-        public static CustomOption morphlingSpawnRate;
-        public static CustomOption morphlingCooldown;
-        public static CustomOption morphlingDuration;
-
-        public static CustomOption camouflagerSpawnRate;
-        public static CustomOption camouflagerCooldown;
-        public static CustomOption camouflagerDuration;
-
-        public static CustomOption vampireSpawnRate;
-        public static CustomOption vampireKillDelay;
-        public static CustomOption vampireCooldown;
-        public static CustomOption vampireCanKillNearGarlics;
-
-        public static CustomOption eraserSpawnRate;
-        public static CustomOption eraserCooldown;
-        public static CustomOption eraserCanEraseAnyone;
-
-        public static CustomOption miniSpawnRate;
-        public static CustomOption miniGrowingUpDuration;
-
-        public static CustomOption loversSpawnRate;
-        public static CustomOption loversImpLoverRate;
-        public static CustomOption loversBothDie;
-        public static CustomOption loversCanHaveAnotherRole;
-
-        public static CustomOption guesserSpawnRate;
-        public static CustomOption guesserIsImpGuesserRate;
-        public static CustomOption guesserNumberOfShots;
-        public static CustomOption guesserHasMultipleShotsPerMeeting;
-
-        public static CustomOption jesterSpawnRate;
-        public static CustomOption jesterCanCallEmergency;
-
-        public static CustomOption arsonistSpawnRate;
-        public static CustomOption arsonistCooldown;
-        public static CustomOption arsonistDuration;
-
-        public static CustomOption jackalSpawnRate;
-        public static CustomOption jackalKillCooldown;
-        public static CustomOption jackalCreateSidekickCooldown;
-        public static CustomOption jackalCanUseVents;
-        public static CustomOption jackalCanCreateSidekick;
-        public static CustomOption sidekickPromotesToJackal;
-        public static CustomOption sidekickCanKill;
-        public static CustomOption sidekickCanUseVents;
-        public static CustomOption jackalPromotedFromSidekickCanCreateSidekick;
-        public static CustomOption jackalCanCreateSidekickFromImpostor;
-        public static CustomOption jackalAndSidekickHaveImpostorVision;
-
-        public static CustomOption bountyHunterSpawnRate;
-        public static CustomOption bountyHunterBountyDuration;
-        public static CustomOption bountyHunterReducedCooldown;
-        public static CustomOption bountyHunterPunishmentTime;
-        public static CustomOption bountyHunterShowArrow;
-        public static CustomOption bountyHunterArrowUpdateIntervall;
-
-        public static CustomOption shifterSpawnRate;
-        public static CustomOption shifterShiftsModifiers;
-
-        public static CustomOption mayorSpawnRate;
-
-        public static CustomOption engineerSpawnRate;
-        public static CustomOption engineerNumberOfFixes;
-        public static CustomOption engineerHighlightForImpostors;
-        public static CustomOption engineerHighlightForTeamJackal;
-
-        public static CustomOption sheriffSpawnRate;
-        public static CustomOption sheriffCooldown;
-        public static CustomOption sheriffCanKillNeutrals;
-
-        public static CustomOption lighterSpawnRate;
-        public static CustomOption lighterModeLightsOnVision;
-        public static CustomOption lighterModeLightsOffVision;
-        public static CustomOption lighterCooldown;
-        public static CustomOption lighterDuration;
-
-        public static CustomOption detectiveSpawnRate;
-        public static CustomOption detectiveAnonymousFootprints;
-        public static CustomOption detectiveFootprintIntervall;
-        public static CustomOption detectiveFootprintDuration;
-        public static CustomOption detectiveReportNameDuration;
-        public static CustomOption detectiveReportColorDuration;
-
-        public static CustomOption timeMasterSpawnRate;
-        public static CustomOption timeMasterCooldown;
-        public static CustomOption timeMasterRewindTime;
-        public static CustomOption timeMasterShieldDuration;
-
-        public static CustomOption medicSpawnRate;
-        public static CustomOption medicShowShielded;
-        public static CustomOption medicShowAttemptToShielded;
-        public static CustomOption medicSetShieldAfterMeeting;
-
-        public static CustomOption swapperSpawnRate;
-        public static CustomOption swapperCanCallEmergency;
-        public static CustomOption swapperCanOnlySwapOthers;
-
-        public static CustomOption seerSpawnRate;
-        public static CustomOption seerMode;
-        public static CustomOption seerSoulDuration;
-        public static CustomOption seerLimitSoulDuration;
-
-        public static CustomOption hackerSpawnRate;
-        public static CustomOption hackerCooldown;
-        public static CustomOption hackerHackeringDuration;
-        public static CustomOption hackerOnlyColorType;
-
-        public static CustomOption trackerSpawnRate;
-        public static CustomOption trackerUpdateIntervall;
-        public static CustomOption trackerResetTargetAfterMeeting;
-
-        public static CustomOption snitchSpawnRate;
-        public static CustomOption snitchLeftTasksForReveal;
-        public static CustomOption snitchIncludeTeamJackal;
-        public static CustomOption snitchTeamJackalUseDifferentArrowColor;
-
-        public static CustomOption spySpawnRate;
-        public static CustomOption spyCanDieToSheriff;
-        public static CustomOption spyImpostorsCanKillAnyone;
-        public static CustomOption spyCanEnterVents;
-        public static CustomOption spyHasImpostorVision;
-
-        public static CustomOption tricksterSpawnRate;
-        public static CustomOption tricksterPlaceBoxCooldown;
-        public static CustomOption tricksterLightsOutCooldown;
-        public static CustomOption tricksterLightsOutDuration;
-
-        public static CustomOption cleanerSpawnRate;
-        public static CustomOption cleanerCooldown;
-        
-        public static CustomOption warlockSpawnRate;
-        public static CustomOption warlockCooldown;
-        public static CustomOption warlockRootTime;
-
-        public static CustomOption securityGuardSpawnRate;
-        public static CustomOption securityGuardCooldown;
-        public static CustomOption securityGuardTotalScrews;
-        public static CustomOption securityGuardCamPrice;
-        public static CustomOption securityGuardVentPrice;
-
-        public static CustomOption baitSpawnRate;
-        public static CustomOption baitHighlightAllVents;
-        public static CustomOption baitReportDelay;
-
-        public static CustomOption vultureSpawnRate;
-        public static CustomOption vultureCooldown;
-        public static CustomOption vultureNumberToWin;
-        public static CustomOption vultureCanUseVents;
-        public static CustomOption vultureShowArrows;
-
-        public static CustomOption mediumSpawnRate;
-        public static CustomOption mediumCooldown;
-        public static CustomOption mediumDuration;
-        public static CustomOption mediumOneTimeUse;
-
-        public static CustomOption maxNumberOfMeetings;
-        public static CustomOption blockSkippingInEmergencyMeetings;
-        public static CustomOption noVoteIsSelfVote;
-        public static CustomOption hidePlayerNames;
-
-        internal static Dictionary<byte, byte[]> blockedRolePairings = new Dictionary<byte, byte[]>();
-
-        public static string cs(Color c, string s) {
-            return string.Format("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s);
-        }
-        private static byte ToByte(float f) {
-            f = Mathf.Clamp01(f);
-            return (byte)(f * 255);
-        }
-
-        public static void Load() {
-            
-            // Role Options
-            presetSelection = CustomOption.Create(0, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true);
-            activateRoles = CustomOption.Create(7, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Enable Mod Roles And Block Vanilla Roles"), true, null, true);
-
-            // Using new id's for the options to not break compatibilty with older versions
-            crewmateRolesCountMin = CustomOption.Create(300, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 0f, 0f, 15f, 1f, null, true);
-            crewmateRolesCountMax = CustomOption.Create(301, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 0f, 0f, 15f, 1f);
-            neutralRolesCountMin = CustomOption.Create(302, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Neutral Roles"), 0f, 0f, 15f, 1f);
-            neutralRolesCountMax = CustomOption.Create(303, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Neutral Roles"), 0f, 0f, 15f, 1f);
-            impostorRolesCountMin = CustomOption.Create(304, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Impostor Roles"), 0f, 0f, 3f, 1f);
-            impostorRolesCountMax = CustomOption.Create(305, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Impostor Roles"), 0f, 0f, 3f, 1f);
-
-            mafiaSpawnRate = CustomOption.Create(10, cs(Janitor.color, "Mafia"), rates, null, true);
-            janitorCooldown = CustomOption.Create(11, "Janitor Cooldown", 30f, 10f, 60f, 2.5f, mafiaSpawnRate);
-
-            morphlingSpawnRate = CustomOption.Create(20, cs(Morphling.color, "Morphling"), rates, null, true);
-            morphlingCooldown = CustomOption.Create(21, "Morphling Cooldown", 30f, 10f, 60f, 2.5f, morphlingSpawnRate);
-            morphlingDuration = CustomOption.Create(22, "Morph Duration", 10f, 1f, 20f, 0.5f, morphlingSpawnRate);
-
-            camouflagerSpawnRate = CustomOption.Create(30, cs(Camouflager.color, "Camouflager"), rates, null, true);
-            camouflagerCooldown = CustomOption.Create(31, "Camouflager Cooldown", 30f, 10f, 60f, 2.5f, camouflagerSpawnRate);
-            camouflagerDuration = CustomOption.Create(32, "Camo Duration", 10f, 1f, 20f, 0.5f, camouflagerSpawnRate);
-
-            vampireSpawnRate = CustomOption.Create(40, cs(Vampire.color, "Vampire"), rates, null, true);
-            vampireKillDelay = CustomOption.Create(41, "Vampire Kill Delay", 10f, 1f, 20f, 1f, vampireSpawnRate);
-            vampireCooldown = CustomOption.Create(42, "Vampire Cooldown", 30f, 10f, 60f, 2.5f, vampireSpawnRate);
-            vampireCanKillNearGarlics = CustomOption.Create(43, "Vampire Can Kill Near Garlics", true, vampireSpawnRate);
-
-            eraserSpawnRate = CustomOption.Create(230, cs(Eraser.color, "Eraser"), rates, null, true);
-            eraserCooldown = CustomOption.Create(231, "Eraser Cooldown", 30f, 10f, 120f, 5f, eraserSpawnRate);
-            eraserCanEraseAnyone = CustomOption.Create(232, "Eraser Can Erase Anyone", false, eraserSpawnRate);
-
-            tricksterSpawnRate = CustomOption.Create(250, cs(Trickster.color, "Trickster"), rates, null, true);
-            tricksterPlaceBoxCooldown = CustomOption.Create(251, "Trickster Box Cooldown", 10f, 0f, 30f, 2.5f, tricksterSpawnRate);
-            tricksterLightsOutCooldown = CustomOption.Create(252, "Trickster Lights Out Cooldown", 30f, 10f, 60f, 5f, tricksterSpawnRate);
-            tricksterLightsOutDuration = CustomOption.Create(253, "Trickster Lights Out Duration", 15f, 5f, 60f, 2.5f, tricksterSpawnRate);
-
-            cleanerSpawnRate = CustomOption.Create(260, cs(Cleaner.color, "Cleaner"), rates, null, true);
-            cleanerCooldown = CustomOption.Create(261, "Cleaner Cooldown", 30f, 10f, 60f, 2.5f, cleanerSpawnRate);
-
-            warlockSpawnRate = CustomOption.Create(270, cs(Cleaner.color, "Warlock"), rates, null, true);
-            warlockCooldown = CustomOption.Create(271, "Warlock Cooldown", 30f, 10f, 60f, 2.5f, warlockSpawnRate);
-            warlockRootTime = CustomOption.Create(272, "Warlock Root Time", 5f, 0f, 15f, 1f, warlockSpawnRate);
-
-            bountyHunterSpawnRate = CustomOption.Create(320, cs(BountyHunter.color, "Bounty Hunter"), rates, null, true);
-            bountyHunterBountyDuration = CustomOption.Create(321, "Duration After Which Bounty Changes",  60f, 10f, 180f, 10f, bountyHunterSpawnRate);
-            bountyHunterReducedCooldown = CustomOption.Create(322, "Cooldown After Killing Bounty", 2.5f, 0f, 30f, 2.5f, bountyHunterSpawnRate);
-            bountyHunterPunishmentTime = CustomOption.Create(323, "Additional Cooldown After Killing Others", 20f, 0f, 60f, 2.5f, bountyHunterSpawnRate);
-            bountyHunterShowArrow = CustomOption.Create(324, "Show Arrow Pointing Towards The Bounty", true, bountyHunterSpawnRate);
-            bountyHunterArrowUpdateIntervall = CustomOption.Create(325, "Arrow Update Intervall", 15f, 2.5f, 60f, 2.5f, bountyHunterShowArrow);
-
-
-            miniSpawnRate = CustomOption.Create(180, cs(Mini.color, "Mini"), rates, null, true);
-            miniGrowingUpDuration = CustomOption.Create(181, "Mini Growing Up Duration", 400f, 100f, 1500f, 100f, miniSpawnRate);
-
-            loversSpawnRate = CustomOption.Create(50, cs(Lovers.color, "Lovers"), rates, null, true);
-            loversImpLoverRate = CustomOption.Create(51, "Chance That One Lover Is Impostor", rates, loversSpawnRate);
-            loversBothDie = CustomOption.Create(52, "Both Lovers Die", true, loversSpawnRate);
-            loversCanHaveAnotherRole = CustomOption.Create(53, "Lovers Can Have Another Role", true, loversSpawnRate);
-
-            guesserSpawnRate = CustomOption.Create(310, cs(Guesser.color, "Guesser"), rates, null, true);
-            guesserIsImpGuesserRate = CustomOption.Create(311, "Chance That The Guesser Is An Impostor", rates, guesserSpawnRate);
-            guesserNumberOfShots = CustomOption.Create(312, "Guesser Number Of Shots", 2f, 1f, 15f, 1f, guesserSpawnRate);
-            guesserHasMultipleShotsPerMeeting = CustomOption.Create(313, "Guesser Can Shoot Multiple Times Per Meeting", false, guesserSpawnRate);
-
-            jesterSpawnRate = CustomOption.Create(60, cs(Jester.color, "Jester"), rates, null, true);
-            jesterCanCallEmergency = CustomOption.Create(61, "Jester can call emergency meeting", true, jesterSpawnRate);
-
-            arsonistSpawnRate = CustomOption.Create(290, cs(Arsonist.color, "Arsonist"), rates, null, true);
-            arsonistCooldown = CustomOption.Create(291, "Arsonist Cooldown", 12.5f, 2.5f, 60f, 2.5f, arsonistSpawnRate);
-            arsonistDuration = CustomOption.Create(292, "Arsonist Douse Duration", 3f, 1f, 10f, 1f, arsonistSpawnRate);
-
-            jackalSpawnRate = CustomOption.Create(220, cs(Jackal.color, "Jackal"), rates, null, true);
-            jackalKillCooldown = CustomOption.Create(221, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
-            jackalCreateSidekickCooldown = CustomOption.Create(222, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
-            jackalCanUseVents = CustomOption.Create(223, "Jackal Can Use Vents", true, jackalSpawnRate);
-            jackalCanCreateSidekick = CustomOption.Create(224, "Jackal Can Create A Sidekick", false, jackalSpawnRate);
-            sidekickPromotesToJackal = CustomOption.Create(225, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalSpawnRate);
-            sidekickCanKill = CustomOption.Create(226, "Sidekick Can Kill", false, jackalSpawnRate);
-            sidekickCanUseVents = CustomOption.Create(227, "Sidekick Can Use Vents", true, jackalSpawnRate);
-            jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, "Jackals Promoted From Sidekick Can Create A Sidekick", true, jackalSpawnRate);
-            jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, "Jackals Can Make An Impostor To His Sidekick", true, jackalSpawnRate);
-            jackalAndSidekickHaveImpostorVision = CustomOption.Create(430, "Jackal And Sidekick Have Impostor Vision", false, jackalSpawnRate);
-
-            vultureSpawnRate = CustomOption.Create(340, cs(Vulture.color, "Vulture"), rates, null, true);
-            vultureCooldown = CustomOption.Create(341, "Vulture Cooldown", 15f, 10f, 60f, 2.5f, vultureSpawnRate);
-            vultureNumberToWin = CustomOption.Create(342, "Number Of Corpses Needed To Be Eaten", 4f, 0f, 10f, 1f, vultureSpawnRate);
-            vultureCanUseVents = CustomOption.Create(343, "Vulture Can Use Vents", true, vultureSpawnRate);
-            vultureShowArrows = CustomOption.Create(344, "Show Arrows Pointing Towards The Corpes", true, vultureSpawnRate);
-
-            shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true);
-            shifterShiftsModifiers = CustomOption.Create(71, "Shifter Shifts Modifiers", false, shifterSpawnRate);
-
-            mayorSpawnRate = CustomOption.Create(80, cs(Mayor.color, "Mayor"), rates, null, true);
-
-            engineerSpawnRate = CustomOption.Create(90, cs(Engineer.color, "Engineer"), rates, null, true);
-            engineerNumberOfFixes = CustomOption.Create(91, "Number Of Sabotage Fixes", 1f, 0f, 3f, 1f, engineerSpawnRate);
-            engineerHighlightForImpostors = CustomOption.Create(92, "Impostors See Vents Highlighted", true, engineerSpawnRate);
-            engineerHighlightForTeamJackal = CustomOption.Create(93, "Jackal and Sidekick See Vents Highlighted ", true, engineerSpawnRate);
-
-            sheriffSpawnRate = CustomOption.Create(100, cs(Sheriff.color, "Sheriff"), rates, null, true);
-            sheriffCooldown = CustomOption.Create(101, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate);
-            sheriffCanKillNeutrals = CustomOption.Create(102, "Sheriff Can Kill Neutrals", false, sheriffSpawnRate);
-
-            lighterSpawnRate = CustomOption.Create(110, cs(Lighter.color, "Lighter"), rates, null, true);
-            lighterModeLightsOnVision = CustomOption.Create(111, "Lighter Mode Vision On Lights On", 2f, 0.25f, 5f, 0.25f, lighterSpawnRate);
-            lighterModeLightsOffVision = CustomOption.Create(112, "Lighter Mode Vision On Lights Off", 0.75f, 0.25f, 5f, 0.25f, lighterSpawnRate);
-            lighterCooldown = CustomOption.Create(113, "Lighter Cooldown", 30f, 5f, 120f, 5f, lighterSpawnRate);
-            lighterDuration = CustomOption.Create(114, "Lighter Duration", 5f, 2.5f, 60f, 2.5f, lighterSpawnRate);
-
-            detectiveSpawnRate = CustomOption.Create(120, cs(Detective.color, "Detective"), rates, null, true);
-            detectiveAnonymousFootprints = CustomOption.Create(121, "Anonymous Footprints", false, detectiveSpawnRate); 
-            detectiveFootprintIntervall = CustomOption.Create(122, "Footprint Intervall", 0.5f, 0.25f, 10f, 0.25f, detectiveSpawnRate);
-            detectiveFootprintDuration = CustomOption.Create(123, "Footprint Duration", 5f, 0.25f, 10f, 0.25f, detectiveSpawnRate);
-            detectiveReportNameDuration = CustomOption.Create(124, "Time Where Detective Reports Will Have Name", 0, 0, 60, 2.5f, detectiveSpawnRate);
-            detectiveReportColorDuration = CustomOption.Create(125, "Time Where Detective Reports Will Have Color Type", 20, 0, 120, 2.5f, detectiveSpawnRate);
-
-            timeMasterSpawnRate = CustomOption.Create(130, cs(TimeMaster.color, "Time Master"), rates, null, true);
-            timeMasterCooldown = CustomOption.Create(131, "Time Master Cooldown", 30f, 10f, 120f, 2.5f, timeMasterSpawnRate);
-            timeMasterRewindTime = CustomOption.Create(132, "Rewind Time", 3f, 1f, 10f, 1f, timeMasterSpawnRate);
-            timeMasterShieldDuration = CustomOption.Create(133, "Time Master Shield Duration", 3f, 1f, 20f, 1f, timeMasterSpawnRate);
-
-            medicSpawnRate = CustomOption.Create(140, cs(Medic.color, "Medic"), rates, null, true);
-            medicShowShielded = CustomOption.Create(143, "Show Shielded Player", new string[] {"Everyone", "Shielded + Medic", "Medic"}, medicSpawnRate);
-            medicShowAttemptToShielded = CustomOption.Create(144, "Shielded Player Sees Murder Attempt", false, medicSpawnRate);
-            medicSetShieldAfterMeeting = CustomOption.Create(145, "Shield Will Be Set After The Next Meeting", false, medicSpawnRate);
-
-            swapperSpawnRate = CustomOption.Create(150, cs(Swapper.color, "Swapper"), rates, null, true);
-            swapperCanCallEmergency = CustomOption.Create(151, "Swapper can call emergency meeting", false, swapperSpawnRate);
-            swapperCanOnlySwapOthers = CustomOption.Create(152, "Swapper can only swap others", false, swapperSpawnRate);
-
-            seerSpawnRate = CustomOption.Create(160, cs(Seer.color, "Seer"), rates, null, true);
-            seerMode = CustomOption.Create(161, "Seer Mode", new string[]{ "Show Death Flash + Souls", "Show Death Flash", "Show Souls"}, seerSpawnRate);
-            seerLimitSoulDuration = CustomOption.Create(163, "Seer Limit Soul Duration", false, seerSpawnRate);
-            seerSoulDuration = CustomOption.Create(162, "Seer Soul Duration", 15f, 0f, 60f, 5f, seerLimitSoulDuration);
-        
-            hackerSpawnRate = CustomOption.Create(170, cs(Hacker.color, "Hacker"), rates, null, true);
-            hackerCooldown = CustomOption.Create(171, "Hacker Cooldown", 30f, 0f, 60f, 5f, hackerSpawnRate);
-            hackerHackeringDuration = CustomOption.Create(172, "Hacker Duration", 10f, 2.5f, 60f, 2.5f, hackerSpawnRate);
-            hackerOnlyColorType = CustomOption.Create(173, "Hacker Only Sees Color Type", false, hackerSpawnRate);
-
-            trackerSpawnRate = CustomOption.Create(200, cs(Tracker.color, "Tracker"), rates, null, true);
-            trackerUpdateIntervall = CustomOption.Create(201, "Tracker Update Intervall", 5f, 2.5f, 30f, 2.5f, trackerSpawnRate);
-            trackerResetTargetAfterMeeting = CustomOption.Create(202, "Tracker Reset Target After Meeting", false, trackerSpawnRate);
-            snitchSpawnRate = CustomOption.Create(210, cs(Snitch.color, "Snitch"), rates, null, true);
-            snitchLeftTasksForReveal = CustomOption.Create(211, "Task Count Where The Snitch Will Be Revealed", 1f, 0f, 5f, 1f, snitchSpawnRate);
-            snitchIncludeTeamJackal = CustomOption.Create(212, "Include Team Jackal", false, snitchSpawnRate);
-            snitchTeamJackalUseDifferentArrowColor = CustomOption.Create(213, "Use Different Arrow Color For Team Jackal", true, snitchIncludeTeamJackal);
-
-            spySpawnRate = CustomOption.Create(240, cs(Spy.color, "Spy"), rates, null, true);
-            spyCanDieToSheriff = CustomOption.Create(241, "Spy Can Die To Sheriff", false, spySpawnRate);
-            spyImpostorsCanKillAnyone = CustomOption.Create(242, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate);
-            spyCanEnterVents = CustomOption.Create(243, "Spy Can Enter Vents", false, spySpawnRate);
-            spyHasImpostorVision = CustomOption.Create(244, "Spy Has Impostor Vision", false, spySpawnRate);
-
-            securityGuardSpawnRate = CustomOption.Create(280, cs(SecurityGuard.color, "Security Guard"), rates, null, true);
-            securityGuardCooldown = CustomOption.Create(281, "Security Guard Cooldown", 30f, 10f, 60f, 2.5f, securityGuardSpawnRate);
-            securityGuardTotalScrews = CustomOption.Create(282, "Security Guard Number Of Screws", 7f, 1f, 15f, 1f, securityGuardSpawnRate);
-            securityGuardCamPrice = CustomOption.Create(283, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate);
-            securityGuardVentPrice = CustomOption.Create(284, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate);
-
-            baitSpawnRate = CustomOption.Create(330, cs(Bait.color, "Bait"), rates, null, true);
-            baitHighlightAllVents = CustomOption.Create(331, "Highlight All Vents If A Vent Is Occupied", false, baitSpawnRate);
-            baitReportDelay = CustomOption.Create(332, "Bait Report Delay", 0f, 0f, 10f, 1f, baitSpawnRate);
-
-            mediumSpawnRate = CustomOption.Create(360, cs(Medium.color, "Medium"), rates, null, true);
-            mediumCooldown = CustomOption.Create(361, "Medium Questioning Cooldown", 30f, 5f, 120f, 5f, mediumSpawnRate);
-            mediumDuration = CustomOption.Create(362, "Medium Questioning Duration", 3f, 0f, 15f, 1f, mediumSpawnRate);
-            mediumOneTimeUse = CustomOption.Create(363, "Each Soul Can Only Be Questioned Once", false, mediumSpawnRate);
-
-            // Other options
-            maxNumberOfMeetings = CustomOption.Create(3, "Number Of Meetings (excluding Mayor meeting)", 10, 0, 15, 1, null, true);
-            blockSkippingInEmergencyMeetings = CustomOption.Create(4, "Block Skipping In Emergency Meetings", false);
-            noVoteIsSelfVote = CustomOption.Create(5, "No Vote Is Self Vote", false, blockSkippingInEmergencyMeetings);
-            hidePlayerNames = CustomOption.Create(6, "Hide Player Names", false);
-
-            blockedRolePairings.Add((byte)RoleId.Vampire, new [] { (byte)RoleId.Warlock});
-            blockedRolePairings.Add((byte)RoleId.Warlock, new [] { (byte)RoleId.Vampire});
-            blockedRolePairings.Add((byte)RoleId.Spy, new [] { (byte)RoleId.Mini});
-            blockedRolePairings.Add((byte)RoleId.Mini, new [] { (byte)RoleId.Spy});
-            
-        }
-    }
-
-    public class CustomOption {
-        public static List<CustomOption> options = new List<CustomOption>();
-        public static int preset = 0;
-
-        public int id;
-        public string name;
-        public System.Object[] selections;
-
-        public int defaultSelection;
-        public ConfigEntry<int> entry;
-        public int selection;
-        public OptionBehaviour optionBehaviour;
-        public CustomOption parent;
-        public bool isHeader;
-
-        // Option creation
-
-        public CustomOption(int id, string name,  System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader) {
-            this.id = id;
-            this.name = parent == null ? name : "- " + name;
-            this.selections = selections;
-            int index = Array.IndexOf(selections, defaultValue);
-            this.defaultSelection = index >= 0 ? index : 0;
-            this.parent = parent;
-            this.isHeader = isHeader;
-            selection = 0;
-            if (id != 0) {
-                entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", id.ToString(), defaultSelection);
-                selection = Mathf.Clamp(entry.Value, 0, selections.Length - 1);
-            }
-            options.Add(this);
-        }
-
-        public static CustomOption Create(int id, string name, string[] selections, CustomOption parent = null, bool isHeader = false) {
-            return new CustomOption(id, name, selections, "", parent, isHeader);
-        }
-
-        public static CustomOption Create(int id, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false) {
-            List<float> selections = new List<float>();
-            for (float s = min; s <= max; s += step)
-                selections.Add(s);
-            return new CustomOption(id, name, selections.Cast<object>().ToArray(), defaultValue, parent, isHeader);
-        }
-
-        public static CustomOption Create(int id, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false) {
-            return new CustomOption(id, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader);
-        }
-
-        // Static behaviour
-
-        public static void switchPreset(int newPreset) {
-            CustomOption.preset = newPreset;
-            foreach (CustomOption option in CustomOption.options) {
-                if (option.id == 0) continue;
-
-                option.entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", option.id.ToString(), option.defaultSelection);
-                option.selection = Mathf.Clamp(option.entry.Value, 0, option.selections.Length - 1);
-                if (option.optionBehaviour != null && option.optionBehaviour is StringOption stringOption) {
-                    stringOption.oldValue = stringOption.Value = option.selection;
-                    stringOption.ValueText.text = option.selections[option.selection].ToString();
-                }
-            }
-        }
-
-        public static void ShareOptionSelections() {
-            if (PlayerControl.AllPlayerControls.Count <= 1 || AmongUsClient.Instance?.AmHost == false && PlayerControl.LocalPlayer == null) return;
-            foreach (CustomOption option in CustomOption.options) {
-                MessageWriter messageWriter = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShareOptionSelection, Hazel.SendOption.Reliable);
-                messageWriter.WritePacked((uint)option.id);
-                messageWriter.WritePacked((uint)Convert.ToUInt32(option.selection));
-                messageWriter.EndMessage();
-            }
-        }
-
-        // Getter
-
-        public int getSelection() {
-            return selection;
-        }
-
-        public bool getBool() {
-            return selection > 0;
-        }
-
-        public float getFloat() {
-            return (float)selections[selection];
-        }
-
-        // Option changes
-
-        public void updateSelection(int newSelection) {
-            selection = Mathf.Clamp((newSelection + selections.Length) % selections.Length, 0, selections.Length - 1);
-            if (optionBehaviour != null && optionBehaviour is StringOption stringOption) {
-                stringOption.oldValue = stringOption.Value = selection;
-                stringOption.ValueText.text = selections[selection].ToString();
-
-                if (AmongUsClient.Instance?.AmHost == true && PlayerControl.LocalPlayer) {
-                    if (id == 0) switchPreset(selection); // Switch presets
-                    else if (entry != null) entry.Value = selection; // Save selection to config
-
-                    ShareOptionSelections();// Share all selections
-                }
-           }
-        }
-    }
-
-    [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Start))]
-    class GameOptionsMenuStartPatch {
-        public static void Postfix(GameOptionsMenu __instance) {
-            if (GameObject.Find("TORSettings") != null) { // Settings setup has already been performed, fixing the title of the tab and returning
-                GameObject.Find("TORSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent<TMPro.TextMeshPro>().SetText("The Other Roles Settings");
-                return;
-            }
-
-            // Setup TOR tab
-            var template = UnityEngine.Object.FindObjectsOfType<StringOption>().FirstOrDefault();
-            if (template == null) return;
-            var gameSettings = GameObject.Find("Game Settings");
-            var gameSettingMenu = UnityEngine.Object.FindObjectsOfType<GameSettingMenu>().FirstOrDefault();
-            var torSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent);
-            var torMenu = torSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent<GameOptionsMenu>();
-            torSettings.name = "TORSettings";
-
-            var roleTab = GameObject.Find("RoleTab");
-            var gameTab = GameObject.Find("GameTab");
-
-            var torTab = UnityEngine.Object.Instantiate(roleTab, roleTab.transform.parent);
-            var torTabHighlight = torTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent<SpriteRenderer>();
-            torTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent<SpriteRenderer>().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIcon.png", 100f);
-    
-            gameTab.transform.position += Vector3.left * 0.5f;
-            torTab.transform.position += Vector3.right * 0.5f;
-            roleTab.transform.position += Vector3.left * 0.5f;  
-
-            var tabs = new GameObject[]{gameTab, roleTab, torTab};
-            for (int i = 0; i < tabs.Length; i++) {
-                var button = tabs[i].GetComponentInChildren<PassiveButton>();
-                if (button == null) continue;
-                int copiedIndex = i;
-                button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
-                button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => {
-                    gameSettingMenu.RegularGameSettings.SetActive(false);
-                    gameSettingMenu.RolesSettings.gameObject.SetActive(false);
-                    torSettings.gameObject.SetActive(false);
-                    gameSettingMenu.GameSettingsHightlight.enabled = false;
-                    gameSettingMenu.RolesSettingsHightlight.enabled = false;
-                    torTabHighlight.enabled = false;
-                    if (copiedIndex == 0) {
-                        gameSettingMenu.RegularGameSettings.SetActive(true);
-                        gameSettingMenu.GameSettingsHightlight.enabled = true;  
-                    } else if (copiedIndex == 1) {
-                        gameSettingMenu.RolesSettings.gameObject.SetActive(true);
-                        gameSettingMenu.RolesSettingsHightlight.enabled = true;
-                    } else if (copiedIndex == 2) {
-                        torSettings.gameObject.SetActive(true);
-                        torTabHighlight.enabled = true;
-                    }
-               }));
-            }
-
-            foreach (OptionBehaviour option in torMenu.GetComponentsInChildren<OptionBehaviour>())
-                UnityEngine.Object.Destroy(option.gameObject);
-            List<OptionBehaviour> torOptions = new List<OptionBehaviour>();
-
-            for (int i = 0; i < CustomOption.options.Count; i++) {
-                CustomOption option = CustomOption.options[i];
-                if (option.optionBehaviour == null) {
-                    StringOption stringOption = UnityEngine.Object.Instantiate(template, torMenu.transform);
-                    torOptions.Add(stringOption);
-                    stringOption.OnValueChanged = new Action<OptionBehaviour>((o) => {});
-                    stringOption.TitleText.text = option.name;
-                    stringOption.Value = stringOption.oldValue = option.selection;
-                    stringOption.ValueText.text = option.selections[option.selection].ToString();
-
-                    option.optionBehaviour = stringOption;
-                }
-                option.optionBehaviour.gameObject.SetActive(true);
-            }
-
-            torMenu.Children = torOptions.ToArray();
-            torSettings.gameObject.SetActive(false);
-
-            // Adapt task count for main options
-
-            var commonTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumCommonTasks").TryCast<NumberOption>();
-            if(commonTasksOption != null) commonTasksOption.ValidRange = new FloatRange(0f, 4f);
-
-            var shortTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumShortTasks").TryCast<NumberOption>();
-            if(shortTasksOption != null) shortTasksOption.ValidRange = new FloatRange(0f, 23f);
-
-            var longTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumLongTasks").TryCast<NumberOption>();
-            if(longTasksOption != null) longTasksOption.ValidRange = new FloatRange(0f, 15f);
-        }
-    }
-
-    [HarmonyPatch(typeof(StringOption), nameof(StringOption.OnEnable))]
-    public class StringOptionEnablePatch {
-        public static bool Prefix(StringOption __instance) {
-            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
-            if (option == null) return true;
-
-            __instance.OnValueChanged = new Action<OptionBehaviour>((o) => {});
-            __instance.TitleText.text = option.name;
-            __instance.Value = __instance.oldValue = option.selection;
-            __instance.ValueText.text = option.selections[option.selection].ToString();
-            
-            return false;
-        }
-    }
-
-    [HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))]
-    public class StringOptionIncreasePatch
-    {
-        public static bool Prefix(StringOption __instance)
-        {
-            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
-            if (option == null) return true;
-            option.updateSelection(option.selection + 1);
-            return false;
-        }
-    }
-
-    [HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))]
-    public class StringOptionDecreasePatch
-    {
-        public static bool Prefix(StringOption __instance)
-        {
-            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
-            if (option == null) return true;
-            option.updateSelection(option.selection - 1);
-            return false;
-        }
-    }
-
-    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSyncSettings))]
-    public class RpcSyncSettingsPatch
-    {
-        public static void Postfix()
-        {
-            CustomOption.ShareOptionSelections();
-        }
-    }
-
-
-    [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Update))]
-    class GameOptionsMenuUpdatePatch
-    {
-        private static float timer = 1f;
-        public static void Postfix(GameOptionsMenu __instance) {
-            if (__instance.Children.Length < 100) return; // TODO: Introduce a cleaner way to seperate the TOR settings from the game settings
-
-            __instance.GetComponentInParent<Scroller>().YBounds.max = -0.5F + __instance.Children.Length * 0.55F; 
-            timer += Time.deltaTime;
-            if (timer < 0.1f) return;
-            timer = 0f;
-
-            float offset = 2.75f;
-            foreach (CustomOption option in CustomOption.options) {
-                if (option?.optionBehaviour != null && option.optionBehaviour.gameObject != null) {
-                    bool enabled = true;
-                    var parent = option.parent;
-                    while (parent != null && enabled) {
-                        enabled = parent.selection != 0;
-                        parent = parent.parent;
-                    }
-                    option.optionBehaviour.gameObject.SetActive(enabled);
-                    if (enabled) {
-                        offset -= option.isHeader ? 0.75f : 0.5f;
-                        option.optionBehaviour.transform.localPosition = new Vector3(option.optionBehaviour.transform.localPosition.x, offset, option.optionBehaviour.transform.localPosition.z);
-                    }
-                }
-            }
-        }
-    }
-
-    [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Start))]
-    class GameSettingMenuStartPatch {
-        public static void Prefix(GameSettingMenu __instance) {
-            __instance.HideForOnline = new Transform[]{};
-        }
-
-        public static void Postfix(GameSettingMenu __instance) {
-            // Setup mapNameTransform
-            var mapNameTransform = __instance.AllItems.FirstOrDefault(x => x.gameObject.activeSelf && x.name.Equals("MapName", StringComparison.OrdinalIgnoreCase));
-            if (mapNameTransform == null) return;
-
-            var options = new Il2CppSystem.Collections.Generic.List<Il2CppSystem.Collections.Generic.KeyValuePair<string, int>>();
-            for (int i = 0; i < Constants.MapNames.Length; i++) {
-                var kvp = new Il2CppSystem.Collections.Generic.KeyValuePair<string, int>();
-                kvp.key = Constants.MapNames[i];
-                kvp.value = i;
-                options.Add(kvp);
-            }
-            mapNameTransform.GetComponent<KeyValueOption>().Values = options;
-        }
-    }
-
-    [HarmonyPatch(typeof(Constants), nameof(Constants.ShouldFlipSkeld))]
-    class ConstantsShouldFlipSkeldPatch {
-        public static bool Prefix(ref bool __result) {
-            if (PlayerControl.GameOptions == null) return true;
-            __result = PlayerControl.GameOptions.MapId == 3;
-            return false;
-        }
-    }
-
-    [HarmonyPatch] 
-    class GameOptionsDataPatch
-    {
-        private static IEnumerable<MethodBase> TargetMethods() {
-            return typeof(GameOptionsData).GetMethods().Where(x => x.ReturnType == typeof(string) && x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(int));
-        }
-
-        private static void Postfix(ref string __result)
-        {
-            StringBuilder sb = new StringBuilder(__result);
-            foreach (CustomOption option in CustomOption.options) {
-                if (option.parent == null) {
-                    if (option == CustomOptionHolder.crewmateRolesCountMin) {
-                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Crewmate Roles");
-                        var min = CustomOptionHolder.crewmateRolesCountMin.getSelection();
-                        var max = CustomOptionHolder.crewmateRolesCountMax.getSelection();
-                        if (min > max) min = max;
-                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
-                        sb.AppendLine($"{optionName}: {optionValue}");
-                    } else if (option == CustomOptionHolder.neutralRolesCountMin) {
-                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Neutral Roles");
-                        var min = CustomOptionHolder.neutralRolesCountMin.getSelection();
-                        var max = CustomOptionHolder.neutralRolesCountMax.getSelection();
-                        if (min > max) min = max;
-                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
-                        sb.AppendLine($"{optionName}: {optionValue}");
-                    } else if (option == CustomOptionHolder.impostorRolesCountMin) {
-                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Impostor Roles");
-                        var min = CustomOptionHolder.impostorRolesCountMin.getSelection();
-                        var max = CustomOptionHolder.impostorRolesCountMax.getSelection();
-                        if (min > max) min = max;
-                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
-                        sb.AppendLine($"{optionName}: {optionValue}");
-                    } else if ((option == CustomOptionHolder.crewmateRolesCountMax) || (option == CustomOptionHolder.neutralRolesCountMax) || (option == CustomOptionHolder.impostorRolesCountMax)) {
-                        continue;
-                    } else {
-                        sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}");
-                    }
-                    
-                }
-            }
-            CustomOption parent = null;
-            foreach (CustomOption option in CustomOption.options)
-                if (option.parent != null) {
-                    if (option.parent != parent) {
-                        sb.AppendLine();
-                        parent = option.parent;
-                    }
-                    sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}");
-                }
-
-            var hudString = sb.ToString();
-
-            int defaultSettingsLines = 23;
-            int roleSettingsLines = defaultSettingsLines + 38;
-            int detailedSettingsP1 = roleSettingsLines + 40;
-            int detailedSettingsP2 = detailedSettingsP1 + 42;
-            int detailedSettingsP3 = detailedSettingsP2 + 42;
-            int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
-            int end2 = hudString.TakeWhile(c => (roleSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
-            int end3 = hudString.TakeWhile(c => (detailedSettingsP1 -= (c == '\n' ? 1 : 0)) > 0).Count();
-            int end4 = hudString.TakeWhile(c => (detailedSettingsP2 -= (c == '\n' ? 1 : 0)) > 0).Count();
-            int end5 = hudString.TakeWhile(c => (detailedSettingsP3 -= (c == '\n' ? 1 : 0)) > 0).Count();
-            int counter = TheOtherRolesPlugin.optionsPage;
-            if (counter == 0) {
-                hudString = hudString.Substring(0, end1) + "\n";   
-            } else if (counter == 1) {
-                hudString = hudString.Substring(end1 + 1, end2 - end1);
-                // Temporary fix, should add a new CustomOption for spaces
-                int gap = 2;
-                int index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
-                hudString = hudString.Insert(index, "\n");
-                gap = 6;
-                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
-                hudString = hudString.Insert(index, "\n");
-                gap = 19;
-                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
-                hudString = hudString.Insert(index + 1, "\n");
-                gap = 24;
-                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
-                hudString = hudString.Insert(index + 1, "\n");
-            } else if (counter == 2) {
-                hudString = hudString.Substring(end2 + 1, end3 - end2);
-            } else if (counter == 3) {
-                hudString = hudString.Substring(end3 + 1, end4 - end3);
-            } else if (counter == 4) {
-                hudString = hudString.Substring(end4 + 1, end5 - end4);
-            } else if (counter == 5) {
-                hudString = hudString.Substring(end5 + 1);
-            }
-
-            hudString += $"\n Press tab for more... ({counter+1}/6)";
-            __result = hudString;
-        }
-    }
-
-    [HarmonyPatch(typeof(KeyboardJoystick), nameof(KeyboardJoystick.Update))]
-    public static class GameOptionsNextPagePatch
-    {
-        public static void Postfix(KeyboardJoystick __instance)
-        {
-            if(Input.GetKeyDown(KeyCode.Tab)) {
-                TheOtherRolesPlugin.optionsPage = (TheOtherRolesPlugin.optionsPage + 1) % 6;
-            }
-        }
-    }
-
-    
-    [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
-    public class GameSettingsScalePatch {
-        public static void Prefix(HudManager __instance) {
-            if (__instance.GameSettings != null) __instance.GameSettings.fontSize = 1.2f; 
-        }
-    }
-}
\ No newline at end of file
index c1ef9f0f4a7e133c095ab0c4ddcfa40642d3f637..c557d3498ca1f22d047c87711c88c64d51733756 100644 (file)
@@ -78,44 +78,14 @@ namespace TheOtherRoles {
             return res;
         }
 
-        public static bool handleMurderAttempt(PlayerControl killer, PlayerControl target, bool isMeetingStart = false) {
-            // Block impostor shielded kill
-            if (Medic.shielded != null && Medic.shielded == target) {
-                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1);
-                AmongUsClient.Instance.FinishRpcImmediately(writer);
-                RPCProcedure.shieldedMurderAttempt();
-
-                return false;
-            }
-            // Block impostor not fully grown mini kill
-            else if (Mini.mini != null && target == Mini.mini && !Mini.isGrownUp()) {
-                return false;
-            }
-            // Block Time Master with time shield kill
-            else if (TimeMaster.shieldActive && TimeMaster.timeMaster != null && TimeMaster.timeMaster == target) {
-                if (!isMeetingStart) { // Only rewind the attempt was not called because a meeting startet 
-                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.TimeMasterRewindTime, Hazel.SendOption.Reliable, -1);
-                    AmongUsClient.Instance.FinishRpcImmediately(writer);
-                    RPCProcedure.timeMasterRewindTime();
-                }
-                return false;
-            }
-            return true;
-        }
-
         public static void handleVampireBiteOnBodyReport() {
-            // Murder the bitten player before the meeting starts or reset the bitten player
-            if (Vampire.bitten != null && !Vampire.bitten.Data.IsDead && Helpers.handleMurderAttempt(Vampire.vampire, Vampire.bitten, true)) {
-                MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireTryKill, Hazel.SendOption.Reliable, -1);
-                AmongUsClient.Instance.FinishRpcImmediately(killWriter);
-                RPCProcedure.vampireTryKill();
-            } else {
-                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
-                writer.Write(byte.MaxValue);
-                writer.Write(byte.MaxValue);
-                AmongUsClient.Instance.FinishRpcImmediately(writer);
-                RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
-            }
+            // Murder the bitten player and reset bitten (regardless whether the kill was successful or not)
+            Helpers.checkMuderAttemptAndKill(Vampire.vampire, Vampire.bitten, true);
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
+            writer.Write(byte.MaxValue);
+            writer.Write(byte.MaxValue);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
         }
 
         public static void refreshRoleDescription(PlayerControl player) {
@@ -294,5 +264,63 @@ namespace TheOtherRoles {
             }
             return roleCouldUse;
         }
+
+        public static bool checkMuderAttempt(PlayerControl killer, PlayerControl target, bool isMeetingStart = false) {
+            // Modified vanilla checks
+            if (AmongUsClient.Instance.IsGameOver) return false;
+            if (killer == null || killer.Data == null || killer.Data.IsDead || killer.Data.Disconnected) return false; // Allow non Impostor kills compared to vanilla code
+            if (target == null || target.Data == null || target.Data.IsDead || target.Data.Disconnected) return false; // Allow killing players in vents compared to vanilla code
+
+            // Block impostor shielded kill
+            if (Medic.shielded != null && Medic.shielded == target) {
+                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1);
+                AmongUsClient.Instance.FinishRpcImmediately(writer);
+                RPCProcedure.shieldedMurderAttempt();
+                return false;
+            }
+
+            // Block impostor not fully grown mini kill
+            else if (Mini.mini != null && target == Mini.mini && !Mini.isGrownUp()) {
+                return false;
+            }
+
+            // Block Time Master with time shield kill
+            else if (TimeMaster.shieldActive && TimeMaster.timeMaster != null && TimeMaster.timeMaster == target) {
+                if (!isMeetingStart) { // Only rewind the attempt was not called because a meeting startet 
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.TimeMasterRewindTime, Hazel.SendOption.Reliable, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.timeMasterRewindTime();
+                }
+                return false;
+            }
+            return true;
+        }
+
+        public static bool checkMuderAttemptAndKill(PlayerControl killer, PlayerControl target, bool isMeetingStart = false)  {
+            // The local player checks for the validity of the kill and performs it afterwards (different to vanilla, where the host performs all the checks)
+            // The kill attempt will be shared using a custom RPC, hence combining modded and unmodded versions is impossible
+
+            bool attemptIsSuccessful = checkMuderAttempt(killer, target, isMeetingStart);
+            if (attemptIsSuccessful) {
+                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
+                writer.Write(killer.PlayerId);
+                writer.Write(target.PlayerId);
+                AmongUsClient.Instance.FinishRpcImmediately(writer);
+                RPCProcedure.uncheckedMurderPlayer(killer.PlayerId, target.PlayerId);
+            }
+            return attemptIsSuccessful;            
+        }
+    
+        public static void shareGameVersion() {
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VersionHandshake, Hazel.SendOption.Reliable, -1);
+            writer.Write((byte)TheOtherRolesPlugin.Version.Major);
+            writer.Write((byte)TheOtherRolesPlugin.Version.Minor);
+            writer.Write((byte)TheOtherRolesPlugin.Version.Build);
+            writer.WritePacked(AmongUsClient.Instance.ClientId);
+            writer.Write((byte)(TheOtherRolesPlugin.Version.Revision < 0 ? 0xFF : TheOtherRolesPlugin.Version.Revision));
+            writer.Write(Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId.ToByteArray());
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            RPCProcedure.versionHandshake(TheOtherRolesPlugin.Version.Major, TheOtherRolesPlugin.Version.Minor, TheOtherRolesPlugin.Version.Build, TheOtherRolesPlugin.Version.Revision, Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId, AmongUsClient.Instance.ClientId);
+        }
     }
 }
index 81e84f9b0bda829197928707237c5a350032ac28..a54986a6ba3511a38ed74704002068e272605a3b 100644 (file)
@@ -21,7 +21,7 @@ namespace TheOtherRoles
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string VersionString = "3.0.0";
+        public const string VersionString = "3.1.0";
         public static System.Version Version = System.Version.Parse(VersionString);
         internal static BepInEx.Logging.ManualLogSource Logger;
 
index 488f9cdf20463400b145e06f74b24bc5d137c55e..94531fe7d0c811957ddc9e6651fc14217ede6858 100644 (file)
@@ -15,6 +15,7 @@ namespace TheOtherRoles{
         public static bool ghostsSeeTasks = true;
         public static bool ghostsSeeVotes = true;
         public static bool showRoleSummary = true;
+        public static bool allowParallelMedBayScans = false;
 
         // Updating values
         public static int meetingsCount = 0;
@@ -32,6 +33,7 @@ public static void clearAndReloadMapOptions() {
             blockSkippingInEmergencyMeetings = CustomOptionHolder.blockSkippingInEmergencyMeetings.getBool();
             noVoteIsSelfVote = CustomOptionHolder.noVoteIsSelfVote.getBool();
             hidePlayerNames = CustomOptionHolder.hidePlayerNames.getBool();
+            allowParallelMedBayScans = CustomOptionHolder.allowParallelMedBayScans.getBool();
             ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value;
             ghostsSeeTasks = TheOtherRolesPlugin.GhostsSeeTasks.Value;
             ghostsSeeVotes = TheOtherRolesPlugin.GhostsSeeVotes.Value;
index 87b96cb17c23f1016db1d1f96d91ab134fc9a54a..d0c9265bfbafd3e51b3d065e7700753c08980f6b 100644 (file)
@@ -14,14 +14,6 @@ namespace TheOtherRoles.Modules {
         public static List<int> lighterColors = new List<int>(){ 3, 4, 5, 7, 10, 11, 13, 14, 17 };
         public static uint pickableColors = (uint)Palette.ColorNames.Length;
 
-        /* version 1
-        private static readonly List<int> ORDER = new List<int>() { 7, 17, 5, 33, 4, 
-                                                                    30, 0, 19, 27, 3,
-                                                                    13, 25, 18, 15, 23,
-                                                                    8, 32, 1, 21, 31,
-                                                                    10, 34, 12, 14, 28,
-                                                                    22, 29, 11, 26, 2,
-                                                                    20, 24, 9, 16, 6 }; */
         private static readonly List<int> ORDER = new List<int>() { 7, 14, 5, 33, 4, 
                                                                     30, 0, 19, 27, 3,
                                                                     17, 25, 18, 13, 23,
diff --git a/TheOtherRoles/Modules/CustomOptions.cs b/TheOtherRoles/Modules/CustomOptions.cs
new file mode 100644 (file)
index 0000000..4629a38
--- /dev/null
@@ -0,0 +1,433 @@
+using System.Collections.Generic;
+using UnityEngine;
+using BepInEx.Configuration;
+using System;
+using System.Linq;
+using HarmonyLib;
+using Hazel;
+using System.Reflection;
+using System.Text;
+using static TheOtherRoles.TheOtherRoles;
+
+namespace TheOtherRoles {
+    public class CustomOption {
+        public static List<CustomOption> options = new List<CustomOption>();
+        public static int preset = 0;
+
+        public int id;
+        public string name;
+        public System.Object[] selections;
+
+        public int defaultSelection;
+        public ConfigEntry<int> entry;
+        public int selection;
+        public OptionBehaviour optionBehaviour;
+        public CustomOption parent;
+        public bool isHeader;
+
+        // Option creation
+
+        public CustomOption(int id, string name,  System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader) {
+            this.id = id;
+            this.name = parent == null ? name : "- " + name;
+            this.selections = selections;
+            int index = Array.IndexOf(selections, defaultValue);
+            this.defaultSelection = index >= 0 ? index : 0;
+            this.parent = parent;
+            this.isHeader = isHeader;
+            selection = 0;
+            if (id != 0) {
+                entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", id.ToString(), defaultSelection);
+                selection = Mathf.Clamp(entry.Value, 0, selections.Length - 1);
+            }
+            options.Add(this);
+        }
+
+        public static CustomOption Create(int id, string name, string[] selections, CustomOption parent = null, bool isHeader = false) {
+            return new CustomOption(id, name, selections, "", parent, isHeader);
+        }
+
+        public static CustomOption Create(int id, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false) {
+            List<float> selections = new List<float>();
+            for (float s = min; s <= max; s += step)
+                selections.Add(s);
+            return new CustomOption(id, name, selections.Cast<object>().ToArray(), defaultValue, parent, isHeader);
+        }
+
+        public static CustomOption Create(int id, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false) {
+            return new CustomOption(id, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader);
+        }
+
+        // Static behaviour
+
+        public static void switchPreset(int newPreset) {
+            CustomOption.preset = newPreset;
+            foreach (CustomOption option in CustomOption.options) {
+                if (option.id == 0) continue;
+
+                option.entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", option.id.ToString(), option.defaultSelection);
+                option.selection = Mathf.Clamp(option.entry.Value, 0, option.selections.Length - 1);
+                if (option.optionBehaviour != null && option.optionBehaviour is StringOption stringOption) {
+                    stringOption.oldValue = stringOption.Value = option.selection;
+                    stringOption.ValueText.text = option.selections[option.selection].ToString();
+                }
+            }
+        }
+
+        public static void ShareOptionSelections() {
+            if (PlayerControl.AllPlayerControls.Count <= 1 || AmongUsClient.Instance?.AmHost == false && PlayerControl.LocalPlayer == null) return;
+            foreach (CustomOption option in CustomOption.options) {
+                MessageWriter messageWriter = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShareOptionSelection, Hazel.SendOption.Reliable);
+                messageWriter.WritePacked((uint)option.id);
+                messageWriter.WritePacked((uint)Convert.ToUInt32(option.selection));
+                messageWriter.EndMessage();
+            }
+        }
+
+        // Getter
+
+        public int getSelection() {
+            return selection;
+        }
+
+        public bool getBool() {
+            return selection > 0;
+        }
+
+        public float getFloat() {
+            return (float)selections[selection];
+        }
+
+        // Option changes
+
+        public void updateSelection(int newSelection) {
+            selection = Mathf.Clamp((newSelection + selections.Length) % selections.Length, 0, selections.Length - 1);
+            if (optionBehaviour != null && optionBehaviour is StringOption stringOption) {
+                stringOption.oldValue = stringOption.Value = selection;
+                stringOption.ValueText.text = selections[selection].ToString();
+
+                if (AmongUsClient.Instance?.AmHost == true && PlayerControl.LocalPlayer) {
+                    if (id == 0) switchPreset(selection); // Switch presets
+                    else if (entry != null) entry.Value = selection; // Save selection to config
+
+                    ShareOptionSelections();// Share all selections
+                }
+           }
+        }
+    }
+
+    [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Start))]
+    class GameOptionsMenuStartPatch {
+        public static void Postfix(GameOptionsMenu __instance) {
+            if (GameObject.Find("TORSettings") != null) { // Settings setup has already been performed, fixing the title of the tab and returning
+                GameObject.Find("TORSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent<TMPro.TextMeshPro>().SetText("The Other Roles Settings");
+                return;
+            }
+
+            // Setup TOR tab
+            var template = UnityEngine.Object.FindObjectsOfType<StringOption>().FirstOrDefault();
+            if (template == null) return;
+            var gameSettings = GameObject.Find("Game Settings");
+            var gameSettingMenu = UnityEngine.Object.FindObjectsOfType<GameSettingMenu>().FirstOrDefault();
+            var torSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent);
+            var torMenu = torSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent<GameOptionsMenu>();
+            torSettings.name = "TORSettings";
+
+            var roleTab = GameObject.Find("RoleTab");
+            var gameTab = GameObject.Find("GameTab");
+
+            var torTab = UnityEngine.Object.Instantiate(roleTab, roleTab.transform.parent);
+            var torTabHighlight = torTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent<SpriteRenderer>();
+            torTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent<SpriteRenderer>().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIcon.png", 100f);
+    
+            gameTab.transform.position += Vector3.left * 0.5f;
+            torTab.transform.position += Vector3.right * 0.5f;
+            roleTab.transform.position += Vector3.left * 0.5f;  
+
+            var tabs = new GameObject[]{gameTab, roleTab, torTab};
+            for (int i = 0; i < tabs.Length; i++) {
+                var button = tabs[i].GetComponentInChildren<PassiveButton>();
+                if (button == null) continue;
+                int copiedIndex = i;
+                button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
+                button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => {
+                    gameSettingMenu.RegularGameSettings.SetActive(false);
+                    gameSettingMenu.RolesSettings.gameObject.SetActive(false);
+                    torSettings.gameObject.SetActive(false);
+                    gameSettingMenu.GameSettingsHightlight.enabled = false;
+                    gameSettingMenu.RolesSettingsHightlight.enabled = false;
+                    torTabHighlight.enabled = false;
+                    if (copiedIndex == 0) {
+                        gameSettingMenu.RegularGameSettings.SetActive(true);
+                        gameSettingMenu.GameSettingsHightlight.enabled = true;  
+                    } else if (copiedIndex == 1) {
+                        gameSettingMenu.RolesSettings.gameObject.SetActive(true);
+                        gameSettingMenu.RolesSettingsHightlight.enabled = true;
+                    } else if (copiedIndex == 2) {
+                        torSettings.gameObject.SetActive(true);
+                        torTabHighlight.enabled = true;
+                    }
+               }));
+            }
+
+            foreach (OptionBehaviour option in torMenu.GetComponentsInChildren<OptionBehaviour>())
+                UnityEngine.Object.Destroy(option.gameObject);
+            List<OptionBehaviour> torOptions = new List<OptionBehaviour>();
+
+            for (int i = 0; i < CustomOption.options.Count; i++) {
+                CustomOption option = CustomOption.options[i];
+                if (option.optionBehaviour == null) {
+                    StringOption stringOption = UnityEngine.Object.Instantiate(template, torMenu.transform);
+                    torOptions.Add(stringOption);
+                    stringOption.OnValueChanged = new Action<OptionBehaviour>((o) => {});
+                    stringOption.TitleText.text = option.name;
+                    stringOption.Value = stringOption.oldValue = option.selection;
+                    stringOption.ValueText.text = option.selections[option.selection].ToString();
+
+                    option.optionBehaviour = stringOption;
+                }
+                option.optionBehaviour.gameObject.SetActive(true);
+            }
+
+            torMenu.Children = torOptions.ToArray();
+            torSettings.gameObject.SetActive(false);
+
+            // Adapt task count for main options
+
+            var commonTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumCommonTasks").TryCast<NumberOption>();
+            if(commonTasksOption != null) commonTasksOption.ValidRange = new FloatRange(0f, 4f);
+
+            var shortTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumShortTasks").TryCast<NumberOption>();
+            if(shortTasksOption != null) shortTasksOption.ValidRange = new FloatRange(0f, 23f);
+
+            var longTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumLongTasks").TryCast<NumberOption>();
+            if(longTasksOption != null) longTasksOption.ValidRange = new FloatRange(0f, 15f);
+        }
+    }
+
+    [HarmonyPatch(typeof(StringOption), nameof(StringOption.OnEnable))]
+    public class StringOptionEnablePatch {
+        public static bool Prefix(StringOption __instance) {
+            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
+            if (option == null) return true;
+
+            __instance.OnValueChanged = new Action<OptionBehaviour>((o) => {});
+            __instance.TitleText.text = option.name;
+            __instance.Value = __instance.oldValue = option.selection;
+            __instance.ValueText.text = option.selections[option.selection].ToString();
+            
+            return false;
+        }
+    }
+
+    [HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))]
+    public class StringOptionIncreasePatch
+    {
+        public static bool Prefix(StringOption __instance)
+        {
+            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
+            if (option == null) return true;
+            option.updateSelection(option.selection + 1);
+            return false;
+        }
+    }
+
+    [HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))]
+    public class StringOptionDecreasePatch
+    {
+        public static bool Prefix(StringOption __instance)
+        {
+            CustomOption option = CustomOption.options.FirstOrDefault(option => option.optionBehaviour == __instance);
+            if (option == null) return true;
+            option.updateSelection(option.selection - 1);
+            return false;
+        }
+    }
+
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSyncSettings))]
+    public class RpcSyncSettingsPatch
+    {
+        public static void Postfix()
+        {
+            CustomOption.ShareOptionSelections();
+        }
+    }
+
+
+    [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Update))]
+    class GameOptionsMenuUpdatePatch
+    {
+        private static float timer = 1f;
+        public static void Postfix(GameOptionsMenu __instance) {
+            if (__instance.Children.Length < 100) return; // TODO: Introduce a cleaner way to seperate the TOR settings from the game settings
+
+            __instance.GetComponentInParent<Scroller>().YBounds.max = -0.5F + __instance.Children.Length * 0.55F; 
+            timer += Time.deltaTime;
+            if (timer < 0.1f) return;
+            timer = 0f;
+
+            float offset = 2.75f;
+            foreach (CustomOption option in CustomOption.options) {
+                if (option?.optionBehaviour != null && option.optionBehaviour.gameObject != null) {
+                    bool enabled = true;
+                    var parent = option.parent;
+                    while (parent != null && enabled) {
+                        enabled = parent.selection != 0;
+                        parent = parent.parent;
+                    }
+                    option.optionBehaviour.gameObject.SetActive(enabled);
+                    if (enabled) {
+                        offset -= option.isHeader ? 0.75f : 0.5f;
+                        option.optionBehaviour.transform.localPosition = new Vector3(option.optionBehaviour.transform.localPosition.x, offset, option.optionBehaviour.transform.localPosition.z);
+                    }
+                }
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Start))]
+    class GameSettingMenuStartPatch {
+        public static void Prefix(GameSettingMenu __instance) {
+            __instance.HideForOnline = new Transform[]{};
+        }
+
+        public static void Postfix(GameSettingMenu __instance) {
+            // Setup mapNameTransform
+            var mapNameTransform = __instance.AllItems.FirstOrDefault(x => x.gameObject.activeSelf && x.name.Equals("MapName", StringComparison.OrdinalIgnoreCase));
+            if (mapNameTransform == null) return;
+
+            var options = new Il2CppSystem.Collections.Generic.List<Il2CppSystem.Collections.Generic.KeyValuePair<string, int>>();
+            for (int i = 0; i < Constants.MapNames.Length; i++) {
+                var kvp = new Il2CppSystem.Collections.Generic.KeyValuePair<string, int>();
+                kvp.key = Constants.MapNames[i];
+                kvp.value = i;
+                options.Add(kvp);
+            }
+            mapNameTransform.GetComponent<KeyValueOption>().Values = options;
+        }
+    }
+
+    [HarmonyPatch(typeof(Constants), nameof(Constants.ShouldFlipSkeld))]
+    class ConstantsShouldFlipSkeldPatch {
+        public static bool Prefix(ref bool __result) {
+            if (PlayerControl.GameOptions == null) return true;
+            __result = PlayerControl.GameOptions.MapId == 3;
+            return false;
+        }
+    }
+
+    [HarmonyPatch] 
+    class GameOptionsDataPatch
+    {
+        private static IEnumerable<MethodBase> TargetMethods() {
+            return typeof(GameOptionsData).GetMethods().Where(x => x.ReturnType == typeof(string) && x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(int));
+        }
+
+        private static void Postfix(ref string __result)
+        {
+            StringBuilder sb = new StringBuilder(__result);
+            foreach (CustomOption option in CustomOption.options) {
+                if (option.parent == null) {
+                    if (option == CustomOptionHolder.crewmateRolesCountMin) {
+                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Crewmate Roles");
+                        var min = CustomOptionHolder.crewmateRolesCountMin.getSelection();
+                        var max = CustomOptionHolder.crewmateRolesCountMax.getSelection();
+                        if (min > max) min = max;
+                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
+                        sb.AppendLine($"{optionName}: {optionValue}");
+                    } else if (option == CustomOptionHolder.neutralRolesCountMin) {
+                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Neutral Roles");
+                        var min = CustomOptionHolder.neutralRolesCountMin.getSelection();
+                        var max = CustomOptionHolder.neutralRolesCountMax.getSelection();
+                        if (min > max) min = max;
+                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
+                        sb.AppendLine($"{optionName}: {optionValue}");
+                    } else if (option == CustomOptionHolder.impostorRolesCountMin) {
+                        var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Impostor Roles");
+                        var min = CustomOptionHolder.impostorRolesCountMin.getSelection();
+                        var max = CustomOptionHolder.impostorRolesCountMax.getSelection();
+                        if (min > max) min = max;
+                        var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
+                        sb.AppendLine($"{optionName}: {optionValue}");
+                    } else if ((option == CustomOptionHolder.crewmateRolesCountMax) || (option == CustomOptionHolder.neutralRolesCountMax) || (option == CustomOptionHolder.impostorRolesCountMax)) {
+                        continue;
+                    } else {
+                        sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}");
+                    }
+                    
+                }
+            }
+            CustomOption parent = null;
+            foreach (CustomOption option in CustomOption.options)
+                if (option.parent != null) {
+                    if (option.parent != parent) {
+                        sb.AppendLine();
+                        parent = option.parent;
+                    }
+                    sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}");
+                }
+
+            var hudString = sb.ToString();
+
+            int defaultSettingsLines = 23;
+            int roleSettingsLines = defaultSettingsLines + 38;
+            int detailedSettingsP1 = roleSettingsLines + 40;
+            int detailedSettingsP2 = detailedSettingsP1 + 42;
+            int detailedSettingsP3 = detailedSettingsP2 + 42;
+            int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
+            int end2 = hudString.TakeWhile(c => (roleSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
+            int end3 = hudString.TakeWhile(c => (detailedSettingsP1 -= (c == '\n' ? 1 : 0)) > 0).Count();
+            int end4 = hudString.TakeWhile(c => (detailedSettingsP2 -= (c == '\n' ? 1 : 0)) > 0).Count();
+            int end5 = hudString.TakeWhile(c => (detailedSettingsP3 -= (c == '\n' ? 1 : 0)) > 0).Count();
+            int counter = TheOtherRolesPlugin.optionsPage;
+            if (counter == 0) {
+                hudString = hudString.Substring(0, end1) + "\n";   
+            } else if (counter == 1) {
+                hudString = hudString.Substring(end1 + 1, end2 - end1);
+                // Temporary fix, should add a new CustomOption for spaces
+                int gap = 2;
+                int index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
+                hudString = hudString.Insert(index, "\n");
+                gap = 6;
+                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
+                hudString = hudString.Insert(index, "\n");
+                gap = 19;
+                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
+                hudString = hudString.Insert(index + 1, "\n");
+                gap = 24;
+                index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
+                hudString = hudString.Insert(index + 1, "\n");
+            } else if (counter == 2) {
+                hudString = hudString.Substring(end2 + 1, end3 - end2);
+            } else if (counter == 3) {
+                hudString = hudString.Substring(end3 + 1, end4 - end3);
+            } else if (counter == 4) {
+                hudString = hudString.Substring(end4 + 1, end5 - end4);
+            } else if (counter == 5) {
+                hudString = hudString.Substring(end5 + 1);
+            }
+
+            hudString += $"\n Press tab for more... ({counter+1}/6)";
+            __result = hudString;
+        }
+    }
+
+    [HarmonyPatch(typeof(KeyboardJoystick), nameof(KeyboardJoystick.Update))]
+    public static class GameOptionsNextPagePatch
+    {
+        public static void Postfix(KeyboardJoystick __instance)
+        {
+            if(Input.GetKeyDown(KeyCode.Tab)) {
+                TheOtherRolesPlugin.optionsPage = (TheOtherRolesPlugin.optionsPage + 1) % 6;
+            }
+        }
+    }
+
+    
+    [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
+    public class GameSettingsScalePatch {
+        public static void Prefix(HudManager __instance) {
+            if (__instance.GameSettings != null) __instance.GameSettings.fontSize = 1.2f; 
+        }
+    }
+}
\ No newline at end of file
index bf04743acc71bd88ee9c2bfec334545ff817e151..d241b3995d622c38000f90776a2ac25e66d90e02 100644 (file)
@@ -11,7 +11,7 @@ namespace TheOtherRoles.Patches {
     public static class CredentialsPatch {
         public static string fullCredentials = 
 $@"<size=130%><color=#ff351f>TheOtherRoles</color></size> v{TheOtherRolesPlugin.Version.ToString()}
-<size=80%>Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>EndOfFile</color>
+<size=60%>Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>EndOfFile</color>
 <color=#FCCE03FF>Thunderstorm584</color> & <color=#FCCE03FF>Mallöris</color>
 Button design by <color=#FCCE03FF>Bavari</color></size>";
 
@@ -19,6 +19,8 @@ Button design by <color=#FCCE03FF>Bavari</color></size>";
 $@"Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>Thunderstorm584</color>, <color=#FCCE03FF>EndOfFile</color> & <color=#FCCE03FF>Mallöris</color>
 Design by <color=#FCCE03FF>Bavari</color>";
 
+        public static string contributorsCredentials = "<size=80%>GitHub Contributors: Alex2911, gendelo3</size>";
+
         [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
         private static class VersionShowerPatch
         {
@@ -27,17 +29,12 @@ Design by <color=#FCCE03FF>Bavari</color>";
                 if (amongUsLogo == null) return;
 
                 var credentials = UnityEngine.Object.Instantiate<TMPro.TextMeshPro>(__instance.text);
-                credentials.transform.position = new Vector3(0, 0.1f, 0);
-                credentials.SetText(mainMenuCredentials);
+                credentials.transform.position = new Vector3(0, 0, 0);
+                credentials.SetText($"v{TheOtherRolesPlugin.Version.ToString()}\n<size=30f%>\n</size>{mainMenuCredentials}\n<size=30%>\n</size>{contributorsCredentials}");
                 credentials.alignment = TMPro.TextAlignmentOptions.Center;
                 credentials.fontSize *= 0.75f;
 
-                var version = UnityEngine.Object.Instantiate<TMPro.TextMeshPro>(credentials);
-                version.transform.position = new Vector3(0, -0.25f, 0);
-                version.SetText($"v{TheOtherRolesPlugin.Version.ToString()}");
-
                 credentials.transform.SetParent(amongUsLogo.transform);
-                version.transform.SetParent(amongUsLogo.transform);
             }
         }
 
index 12fc88f4a2d557fbc26a516bc3feb44c124c4ae5..a0648a0aa016441b6bf5e415dc8139632eba535b 100644 (file)
@@ -132,6 +132,9 @@ namespace TheOtherRoles.Patches {
                 Seer.deadBodyPositions = new List<Vector3>();
             }
 
+            // Tracker reset deadBodyPositions
+            Tracker.deadBodyPositions = new List<Vector3>();
+
             // Arsonist deactivate dead poolable players
             if (Arsonist.arsonist != null && Arsonist.arsonist == PlayerControl.LocalPlayer) {
                 int visibleCounter = 0;
index 1a52349300050c624f3779cf15a2d1582d41dc70..21ed43b34c6b39f65c51f7e075508f1417187fe1 100644 (file)
@@ -11,6 +11,7 @@ namespace TheOtherRoles.Patches {
     public class GameStartManagerPatch  {
         public static Dictionary<int, PlayerVersion> playerVersions = new Dictionary<int, PlayerVersion>();
         private static float timer = 600f;
+        private static float kickingTimer = 0f;
         private static bool versionSent = false;
         private static string lobbyCodeText = "";
 
@@ -21,6 +22,8 @@ namespace TheOtherRoles.Patches {
                 versionSent = false;
                 // Reset lobby countdown timer
                 timer = 600f; 
+                // Reset kicking timer
+                kickingTimer = 0f;
                 // Copy lobby code
                 string code = InnerNet.GameCode.IntToGameName(AmongUsClient.Instance.GameId);
                 GUIUtility.systemCopyBuffer = code;
@@ -42,15 +45,7 @@ namespace TheOtherRoles.Patches {
                 // Send version as soon as PlayerControl.LocalPlayer exists
                 if (PlayerControl.LocalPlayer != null && !versionSent) {
                     versionSent = true;
-                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VersionHandshake, Hazel.SendOption.Reliable, -1);
-                    writer.Write((byte)TheOtherRolesPlugin.Version.Major);
-                    writer.Write((byte)TheOtherRolesPlugin.Version.Minor);
-                    writer.Write((byte)TheOtherRolesPlugin.Version.Build);
-                    writer.WritePacked(AmongUsClient.Instance.ClientId);
-                    writer.Write((byte)(TheOtherRolesPlugin.Version.Revision < 0 ? 0xFF : TheOtherRolesPlugin.Version.Revision));
-                    writer.Write(Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId.ToByteArray());
-                    AmongUsClient.Instance.FinishRpcImmediately(writer);
-                    RPCProcedure.versionHandshake(TheOtherRolesPlugin.Version.Major, TheOtherRolesPlugin.Version.Minor, TheOtherRolesPlugin.Version.Build, TheOtherRolesPlugin.Version.Revision, Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId, AmongUsClient.Instance.ClientId);
+                    Helpers.shareGameVersion();
                 }
 
                 // Host update with version handshake infos
@@ -81,15 +76,35 @@ namespace TheOtherRoles.Patches {
                         }
                     }
                     if (blockStart) {
-                        // __instance.StartButton.color = Palette.DisabledClear; // Allow the start for this version to test the feature, blocking it with the next version
+                        __instance.StartButton.color = __instance.startLabelText.color = Palette.DisabledClear;
                         __instance.GameStartText.text = message;
                         __instance.GameStartText.transform.localPosition = __instance.StartButton.transform.localPosition + Vector3.up * 2;
                     } else {
-                        // __instance.StartButton.color = ((__instance.LastPlayerCount >= __instance.MinPlayers) ? Palette.EnabledColor : Palette.DisabledClear); // Allow the start for this version to test the feature, blocking it with the next version
+                        __instance.StartButton.color = __instance.startLabelText.color = ((__instance.LastPlayerCount >= __instance.MinPlayers) ? Palette.EnabledColor : Palette.DisabledClear);
                         __instance.GameStartText.transform.localPosition = __instance.StartButton.transform.localPosition;
                     }
                 }
 
+                // Client update with handshake infos
+                if (!AmongUsClient.Instance.AmHost) {
+                    if (!playerVersions.ContainsKey(AmongUsClient.Instance.HostId) || TheOtherRolesPlugin.Version.CompareTo(playerVersions[AmongUsClient.Instance.HostId].version) != 0) {
+                        kickingTimer += Time.deltaTime;
+                        if (kickingTimer > 10) {
+                            kickingTimer = 0;
+                                       AmongUsClient.Instance.ExitGame(DisconnectReasons.ExitGame);
+                            SceneChanger.ChangeScene("MainMenu");
+                        }
+
+                        __instance.GameStartText.text = $"<color=#FF0000FF>The host has no or a different version of The Other Roles\nYou will be kicked in {Math.Round(10 - kickingTimer)}s</color>";
+                        __instance.GameStartText.transform.localPosition = __instance.StartButton.transform.localPosition + Vector3.up * 2;
+                    } else {
+                        __instance.GameStartText.transform.localPosition = __instance.StartButton.transform.localPosition;
+                        if (__instance.startState != GameStartManager.StartingStates.Countdown) {
+                            __instance.GameStartText.text = String.Empty;
+                        }
+                    }
+                }
+
                 // Lobby code replacement
                 __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"<color={TheOtherRolesPlugin.StreamerModeReplacementColor.Value}>{TheOtherRolesPlugin.StreamerModeReplacementText.Value}</color>" : lobbyCodeText;
 
@@ -115,16 +130,26 @@ namespace TheOtherRoles.Patches {
                 // Block game start if not everyone has the same mod version
                 bool continueStart = true;
 
-                // Allow the start for this version to test the feature, blocking it with the next version
-                // if (AmongUsClient.Instance.AmHost) {
-                //     foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients) {
-                //         if (client.Character == null) continue;
-                //         var dummyComponent = client.Character.GetComponent<DummyBehaviour>();
-                //         if (dummyComponent != null && dummyComponent.enabled) continue;
-                //         if (!playerVersions.ContainsKey(client.Id) || (playerVersions[client.Id].Item1 != TheOtherRolesPlugin.Major || playerVersions[client.Id].Item2 != TheOtherRolesPlugin.Minor || playerVersions[client.Id].Item3 != TheOtherRolesPlugin.Patch))
-                //             continueStart = false;
-                //     }
-                // }
+                if (AmongUsClient.Instance.AmHost) {
+                    foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients) {
+                        if (client.Character == null) continue;
+                        var dummyComponent = client.Character.GetComponent<DummyBehaviour>();
+                        if (dummyComponent != null && dummyComponent.enabled)
+                            continue;
+                        
+                        if (!playerVersions.ContainsKey(client.Id)) {
+                            continueStart = false;
+                            break;
+                        }
+                        
+                        PlayerVersion PV = playerVersions[client.Id];
+                        int diff = TheOtherRolesPlugin.Version.CompareTo(PV.version);
+                        if (diff != 0 || !PV.GuidMatches()) {
+                            continueStart = false;
+                            break;
+                        }
+                    }
+                }
                 return continueStart;
             }
         }
index 5b58012a3b71467e88e5fc2e7e9418da6a6459df..1d725d65cc02c2b0fc36059ef0ce8b1dd82a7a84 100644 (file)
@@ -278,6 +278,16 @@ namespace TheOtherRoles.Patches {
                         PlayerControl target = Helpers.playerById((byte)__instance.playerStates[buttonTarget].TargetPlayerId);
                         if (!(__instance.state == MeetingHud.VoteStates.Voted || __instance.state == MeetingHud.VoteStates.NotVoted) || target == null || Guesser.remainingShots <= 0 ) return;
 
+                        if (!Guesser.killsThroughShield && target == Medic.shielded) { // Depending on the options, shooting the shielded player will not allow the guess, notifiy everyone about the kill attempt and close the window
+                            __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true));
+                            UnityEngine.Object.Destroy(container.gameObject);
+
+                            MessageWriter murderAttemptWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1);
+                            AmongUsClient.Instance.FinishRpcImmediately(murderAttemptWriter);
+                            RPCProcedure.shieldedMurderAttempt();
+                            return;
+                        }
+
                         var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(target).FirstOrDefault();
                         if (mainRoleInfo == null) return;
 
@@ -291,11 +301,15 @@ namespace TheOtherRoles.Patches {
                         else
                             __instance.playerStates.ToList().ForEach(x => { if (x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); });
 
-                        // Shoot player
+                        // Shoot player and send chat info if activated
                         MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.GuesserShoot, Hazel.SendOption.Reliable, -1);
                         writer.Write(target.PlayerId);
                         AmongUsClient.Instance.FinishRpcImmediately(writer);
                         RPCProcedure.guesserShoot(target.PlayerId);
+                        if (Guesser.showInfoInGhostChat) {
+                            string msg = $"Guesser guessed the role {roleInfo.name} for {target.name}!";
+                            target.RpcSendChat(msg);  // As the target is dead at this point, only ghosts will see the message.
+                        }
                     }
                 }));
 
index 055fa6d6887bec512445ca043b9f2a73db12fc80..4a737023c51e511b0f108011a3a3a866b4baf7da 100644 (file)
@@ -286,33 +286,56 @@ namespace TheOtherRoles.Patches {
         }
 
         static void trackerUpdate() {
-            if (Tracker.arrow?.arrow == null) return;
+            // Handle player tracking
+            if (Tracker.arrow?.arrow != null) {
+                if (Tracker.tracker == null || PlayerControl.LocalPlayer != Tracker.tracker) {
+                    Tracker.arrow.arrow.SetActive(false);
+                    return;
+                }
+
+                if (Tracker.tracker != null && Tracker.tracked != null && PlayerControl.LocalPlayer == Tracker.tracker && !Tracker.tracker.Data.IsDead) {
+                    Tracker.timeUntilUpdate -= Time.fixedDeltaTime;
+
+                    if (Tracker.timeUntilUpdate <= 0f) {
+                        bool trackedOnMap = !Tracker.tracked.Data.IsDead;
+                        Vector3 position = Tracker.tracked.transform.position;
+                        if (!trackedOnMap) { // Check for dead body
+                            DeadBody body = UnityEngine.Object.FindObjectsOfType<DeadBody>().FirstOrDefault(b => b.ParentId == Tracker.tracked.PlayerId);
+                            if (body != null) {
+                                trackedOnMap = true;
+                                position = body.transform.position;
+                            }
+                        }
 
-            if (Tracker.tracker == null || PlayerControl.LocalPlayer != Tracker.tracker) {
-                Tracker.arrow.arrow.SetActive(false);
-                return;
+                        Tracker.arrow.Update(position);
+                        Tracker.arrow.arrow.SetActive(trackedOnMap);
+                        Tracker.timeUntilUpdate = Tracker.updateIntervall;
+                    } else {
+                        Tracker.arrow.Update();
+                    }
+                }
             }
 
-            if (Tracker.tracker != null && Tracker.tracked != null && PlayerControl.LocalPlayer == Tracker.tracker && !Tracker.tracker.Data.IsDead) {
-                Tracker.timeUntilUpdate -= Time.fixedDeltaTime;
+            // Handle corpses tracking
+            if (Tracker.tracker != null && Tracker.tracker == PlayerControl.LocalPlayer && Tracker.corpsesTrackingTimer >= 0f && !Tracker.tracker.Data.IsDead) {
+                bool arrowsCountChanged = Tracker.localArrows.Count != Tracker.deadBodyPositions.Count();
+                int index = 0;
 
-                if (Tracker.timeUntilUpdate <= 0f) {
-                    bool trackedOnMap = !Tracker.tracked.Data.IsDead;
-                    Vector3 position = Tracker.tracked.transform.position;
-                    if (!trackedOnMap) { // Check for dead body
-                        DeadBody body = UnityEngine.Object.FindObjectsOfType<DeadBody>().FirstOrDefault(b => b.ParentId == Tracker.tracked.PlayerId);
-                        if (body != null) {
-                            trackedOnMap = true;
-                            position = body.transform.position;
-                        }
+                if (arrowsCountChanged) {
+                    foreach (Arrow arrow in Tracker.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
+                    Tracker.localArrows = new List<Arrow>();
+                }
+                foreach (Vector3 position in Tracker.deadBodyPositions) {
+                    if (arrowsCountChanged) {
+                        Tracker.localArrows.Add(new Arrow(Tracker.color));
+                        Tracker.localArrows[index].arrow.SetActive(true);
                     }
-
-                    Tracker.arrow.Update(position);
-                    Tracker.arrow.arrow.SetActive(trackedOnMap);
-                    Tracker.timeUntilUpdate = Tracker.updateIntervall;
-                } else {
-                    Tracker.arrow.Update();
+                    if (Tracker.localArrows[index] != null) Tracker.localArrows[index].Update(position);
+                    index++;
                 }
+            } else if (Tracker.localArrows.Count > 0) { 
+                foreach (Arrow arrow in Tracker.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
+                Tracker.localArrows = new List<Arrow>();
             }
         }
 
@@ -562,6 +585,7 @@ namespace TheOtherRoles.Patches {
                 }
             }
         }
+
         static void vultureUpdate() {
             if (Vulture.vulture == null || PlayerControl.LocalPlayer != Vulture.vulture || Vulture.localArrows == null || !Vulture.showArrows) return;
             if (Vulture.vulture.Data.IsDead) {
@@ -714,28 +738,6 @@ namespace TheOtherRoles.Patches {
             Helpers.handleVampireBiteOnBodyReport();
         }
     }
-    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckMurder))]
-    class RpcMurderPlayer {
-        public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)]PlayerControl target) {
-            PlayerControl killer = __instance;
-            if (AmongUsClient.Instance.IsGameOver || !AmongUsClient.Instance.AmHost) return false;
-            if (!target || killer.Data.IsDead || killer.Data.Disconnected) return false; // Allow non Impostor kills compared to vanilla code
-            if (target.Data == null || target.Data.IsDead)  return false; // Allow killing players in vents compared to vanilla code
-
-            if (Helpers.handleMurderAttempt(killer, target)) { // Custom checks
-                if (Mini.mini != null && killer == Mini.mini || BountyHunter.bountyHunter != null && killer == BountyHunter.bountyHunter) { // Not checked by official servers
-                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
-                    writer.Write(killer.PlayerId);
-                    writer.Write(target.PlayerId);
-                    AmongUsClient.Instance.FinishRpcImmediately(writer);
-                    RPCProcedure.uncheckedMurderPlayer(killer.PlayerId, target.PlayerId);
-                } else { // Checked by official servers
-                    killer.RpcMurderPlayer(target);
-                }
-            }
-            return false;
-        }
-    }
 
     [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.LocalPlayer.CmdReportDeadBody))]
     class BodyReportPatch
@@ -854,6 +856,9 @@ namespace TheOtherRoles.Patches {
             }
             if (Seer.deadBodyPositions != null) Seer.deadBodyPositions.Add(target.transform.position);
 
+            // Tracker store body positions
+            if (Tracker.deadBodyPositions != null) Tracker.deadBodyPositions.Add(target.transform.position);
+
             // Medium add body
             if (Medium.deadBodies != null) {
                 Medium.featureDeadBodies.Add(new Tuple<DeadPlayer, Vector3>(deadPlayer, target.transform.position));
index 62d50853b4a3047464300a63094a1e1ecea68ad9..dc96ff7998b32b0064f0266e4d0d17230630a055 100644 (file)
@@ -166,6 +166,16 @@ namespace TheOtherRoles.Patches {
                         if (Lovers.lover1.PlayerId == player.TargetPlayerId || Lovers.lover2.PlayerId == player.TargetPlayerId)
                             player.NameText.text += suffix;
             }
+
+            // Hacker and Detective
+            if (PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead && (PlayerControl.LocalPlayer == Hacker.hacker || PlayerControl.LocalPlayer == Detective.detective || PlayerControl.LocalPlayer == Medium.medium)) {
+                if (MeetingHud.Instance != null) {
+                    foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates) {
+                        var target = Helpers.playerById(player.TargetPlayerId);
+                        if (target != null)  player.NameText.text += $" ({(Helpers.isLighterColor(target.Data.DefaultOutfit.ColorId) ? "L" : "D")})";
+                    }
+                }
+            }
         }
 
         static void updateShielded() {
@@ -180,6 +190,7 @@ namespace TheOtherRoles.Patches {
             Hacker.hackerTimer -= Time.deltaTime;
             Lighter.lighterTimer -= Time.deltaTime;
             Trickster.lightsOutTimer -= Time.deltaTime;
+            Tracker.corpsesTrackingTimer -= Time.deltaTime;
         }
 
         public static void miniUpdate() {
index f2693016f0eeabe6d4219368d9437e81e28834c2..05007e4d230e9a8b2cdd66d649855409b0b7f3ff 100644 (file)
@@ -99,17 +99,31 @@ namespace TheOtherRoles.Patches {
 
     [HarmonyPatch(typeof(VentButton), nameof(VentButton.SetTarget))]
     class VentButtonSetTargetPatch {
+        static Sprite defaultVentSprite = null;
         static void Postfix(VentButton __instance) {
             // Trickster render special vent button
             if (Trickster.trickster != null && Trickster.trickster == PlayerControl.LocalPlayer) {
-                if (__instance.currentTarget != null && __instance.currentTarget.gameObject != null && __instance.currentTarget.gameObject.name.StartsWith("JackInTheBoxVent_"))
-                    __instance.graphic.sprite = Trickster.getTricksterVentButtonSprite();
-                else
-                    __instance.graphic.sprite = DestroyableSingleton<TranslationController>.Instance.GetImage(ImageNames.VentButton);
+                if (defaultVentSprite == null) defaultVentSprite = __instance.graphic.sprite;
+                bool isSpecialVent = __instance.currentTarget != null && __instance.currentTarget.gameObject != null && __instance.currentTarget.gameObject.name.StartsWith("JackInTheBoxVent_");
+                __instance.graphic.sprite = isSpecialVent ?  Trickster.getTricksterVentButtonSprite() : defaultVentSprite;
+                __instance.buttonLabelText.enabled = !isSpecialVent;
             }
         }
     }
 
+    [HarmonyPatch(typeof(KillButton), nameof(KillButton.DoClick))]
+    class KillButtonDoClickPatch {
+        public static bool Prefix(KillButton __instance) {
+            if (__instance.isActiveAndEnabled && __instance.currentTarget && !__instance.isCoolingDown && !PlayerControl.LocalPlayer.Data.IsDead && PlayerControl.LocalPlayer.CanMove) {
+                // Use an unchecked kill command, to allow shorter kill cooldowns etc. without getting kicked
+                Helpers.checkMuderAttemptAndKill(PlayerControl.LocalPlayer, __instance.currentTarget);
+                __instance.SetTarget(null);
+            }
+            return false;
+        }
+    }
+
+
     [HarmonyPatch(typeof(SabotageButton), nameof(SabotageButton.Refresh))]
     class SabotageButtonRefreshPatch {
         static void Postfix() {
@@ -447,4 +461,15 @@ namespace TheOtherRoles.Patches {
             }
         }
     }
+
+    [HarmonyPatch(typeof(MedScanMinigame), nameof(MedScanMinigame.FixedUpdate))]
+    class MedScanMinigameFixedUpdatePatch {
+        static void Prefix(MedScanMinigame __instance) {
+            if (MapOptions.allowParallelMedBayScans) {
+                __instance.medscan.CurrentUser = PlayerControl.LocalPlayer.PlayerId;
+                __instance.medscan.UsersList.Clear();
+            }
+        }
+    }
+
 }
\ No newline at end of file
index 602a823c92e434fea8df3768f07fbc2d242ba53f..df4edfa80bbe19d8e91cc950c503153d006c8ca8 100644 (file)
@@ -58,7 +58,7 @@ namespace TheOtherRoles
     {
         // Main Controls
 
-        ResetVaribles = 50,
+        ResetVaribles = 60,
         ShareOptionSelection,
         ForceEnd,
         SetRole,
@@ -69,10 +69,9 @@ namespace TheOtherRoles
 
         // Role functionality
 
-        EngineerFixLights = 81,
+        EngineerFixLights = 91,
         EngineerUsedRepair,
         CleanBody,
-        SheriffKill,
         MedicSetShielded,
         ShieldedMurderAttempt,
         TimeMasterShield,
@@ -83,10 +82,7 @@ namespace TheOtherRoles
         CamouflagerCamouflage,
         TrackerUsedTracker,
         VampireSetBitten,
-        VampireTryKill,
         PlaceGarlic,
-        JackalKill,
-        SidekickKill,
         JackalCreatesSidekick,
         SidekickPromotes,
         ErasePlayerRoles,
@@ -256,8 +252,10 @@ namespace TheOtherRoles
                 ver = new System.Version(major, minor, build);
             else 
                 ver = new System.Version(major, minor, build, revision);
-
             GameStartManagerPatch.playerVersions[clientId] = new GameStartManagerPatch.PlayerVersion(ver, guid);
+
+            if (AmongUsClient.Instance.AmHost && clientId != AmongUsClient.Instance.ClientId) // Host returns version whenever another player sends one
+                Helpers.shareGameVersion();
         }
 
         public static void useUncheckedVent(int ventId, byte playerId, byte isEnter) {
@@ -308,17 +306,6 @@ namespace TheOtherRoles
             }
         }
 
-        public static void sheriffKill(byte targetId) {
-            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
-            {
-                if (player.PlayerId == targetId)
-                {
-                    Sheriff.sheriff.MurderPlayer(player);
-                    return;
-                }
-            }
-        }
-
         public static void timeMasterRewindTime() {
             TimeMaster.shieldActive = false; // Shield is no longer active when rewinding
             if(TimeMaster.timeMaster != null && TimeMaster.timeMaster == PlayerControl.LocalPlayer) {
@@ -355,7 +342,12 @@ namespace TheOtherRoles
         }
 
         public static void shieldedMurderAttempt() {
-            if (Medic.shielded != null && Medic.shielded == PlayerControl.LocalPlayer && Medic.showAttemptToShielded && HudManager.Instance?.FullScreen != null) {
+            if (Medic.shielded == null || Medic.medic == null) return;
+            
+            bool isShieldedAndShow = Medic.shielded == PlayerControl.LocalPlayer && Medic.showAttemptToShielded;
+            bool isMedicAndShow = Medic.medic == PlayerControl.LocalPlayer && Medic.showAttemptToMedic;
+
+            if ((isShieldedAndShow || isMedicAndShow) && HudManager.Instance?.FullScreen != null) {
                 HudManager.Instance.FullScreen.enabled = true;
                 HudManager.Instance.StartCoroutine(Effects.Lerp(0.5f, new Action<float>((p) => {
                     var renderer = HudManager.Instance.FullScreen;
@@ -486,13 +478,6 @@ namespace TheOtherRoles
             }
         }
 
-        public static void vampireTryKill() {
-            if (Vampire.bitten != null && !Vampire.bitten.Data.IsDead) {
-                Vampire.vampire.MurderPlayer(Vampire.bitten);
-            }
-            Vampire.bitten = null;
-        }
-
         public static void placeGarlic(byte[] buff) {
             Vector3 position = Vector3.zero;
             position.x = BitConverter.ToSingle(buff, 0*sizeof(float));
@@ -507,28 +492,6 @@ namespace TheOtherRoles
                     Tracker.tracked = player;
         }
 
-        public static void jackalKill(byte targetId) {
-            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
-            {
-                if (player.PlayerId == targetId)
-                {
-                    Jackal.jackal.MurderPlayer(player);
-                    return;
-                }
-            }
-        }
-
-        public static void sidekickKill(byte targetId) {
-            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
-            {
-                if (player.PlayerId == targetId)
-                {
-                    Sidekick.sidekick.MurderPlayer(player);
-                    return;
-                }
-            }
-        }
-
         public static void jackalCreatesSidekick(byte targetId) {
             foreach (PlayerControl player in PlayerControl.AllPlayerControls)
             {
@@ -708,8 +671,8 @@ namespace TheOtherRoles
             PlayerControl target = Helpers.playerById(playerId);
             if (target == null) return;
             target.Exiled();
-            PlayerControl partner = target.getPartner(); // Lover check
-            byte partnerId = partner != null ? partner.PlayerId : playerId;
+            PlayerControl dyingLoverPartner = Lovers.bothDie ? target.getPartner() : null; // Lover check
+            byte partnerId = dyingLoverPartner != null ? dyingLoverPartner.PlayerId : playerId;
             Guesser.remainingShots = Mathf.Max(0, Guesser.remainingShots - 1);
             if (Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(target.KillSfx, false, 0.8f);
             if (MeetingHud.Instance) {
@@ -725,8 +688,8 @@ namespace TheOtherRoles
             if (HudManager.Instance != null && Guesser.guesser != null)
                 if (PlayerControl.LocalPlayer == target) 
                     HudManager.Instance.KillOverlay.ShowKillAnimation(Guesser.guesser.Data, target.Data);
-                else if (partner != null && PlayerControl.LocalPlayer == partner) 
-                    HudManager.Instance.KillOverlay.ShowKillAnimation(partner.Data, partner.Data);
+                else if (dyingLoverPartner != null && PlayerControl.LocalPlayer == dyingLoverPartner) 
+                    HudManager.Instance.KillOverlay.ShowKillAnimation(dyingLoverPartner.Data, dyingLoverPartner.Data);
         }
     }   
 
@@ -802,9 +765,6 @@ namespace TheOtherRoles
                 case (byte)CustomRPC.CleanBody:
                     RPCProcedure.cleanBody(reader.ReadByte());
                     break;
-                case (byte)CustomRPC.SheriffKill:
-                    RPCProcedure.sheriffKill(reader.ReadByte());
-                    break;
                 case (byte)CustomRPC.TimeMasterRewindTime:
                     RPCProcedure.timeMasterRewindTime();
                     break;
@@ -836,21 +796,12 @@ namespace TheOtherRoles
                     byte reset = reader.ReadByte();
                     RPCProcedure.vampireSetBitten(bittenId, reset);
                     break;
-                case (byte)CustomRPC.VampireTryKill:
-                    RPCProcedure.vampireTryKill();
-                    break;
                 case (byte)CustomRPC.PlaceGarlic:
                     RPCProcedure.placeGarlic(reader.ReadBytesAndSize());
                     break;
                 case (byte)CustomRPC.TrackerUsedTracker:
                     RPCProcedure.trackerUsedTracker(reader.ReadByte());
                     break;
-                case (byte)CustomRPC.JackalKill:
-                    RPCProcedure.jackalKill(reader.ReadByte());
-                    break;
-                case (byte)CustomRPC.SidekickKill:
-                    RPCProcedure.sidekickKill(reader.ReadByte());
-                    break;
                 case (byte)CustomRPC.JackalCreatesSidekick:
                     RPCProcedure.jackalCreatesSidekick(reader.ReadByte());
                     break;
diff --git a/TheOtherRoles/Resources/PathfindButton.png b/TheOtherRoles/Resources/PathfindButton.png
new file mode 100644 (file)
index 0000000..32be1b1
Binary files /dev/null and b/TheOtherRoles/Resources/PathfindButton.png differ
index 1fdbc26b8b1d63e5214b0323873aaafa0c38ce0b..b173d4e7c3e354309b6e9dd606e4f93efaa63195 100644 (file)
@@ -252,6 +252,7 @@ namespace TheOtherRoles
 
         public static int showShielded = 0;
         public static bool showAttemptToShielded = false;
+        public static bool showAttemptToMedic = false;
         public static bool setShieldAfterMeeting = false;
 
         public static Color shieldedColor = new Color32(0, 221, 255, byte.MaxValue);
@@ -272,6 +273,7 @@ namespace TheOtherRoles
             usedShield = false;
             showShielded = CustomOptionHolder.medicShowShielded.getSelection();
             showAttemptToShielded = CustomOptionHolder.medicShowAttemptToShielded.getBool();
+            showAttemptToMedic = CustomOptionHolder.medicShowAttemptToMedic.getBool();
             setShieldAfterMeeting = CustomOptionHolder.medicSetShieldAfterMeeting.getBool();
         }
     }
@@ -528,9 +530,15 @@ namespace TheOtherRoles
     public static class Tracker {
         public static PlayerControl tracker;
         public static Color color = new Color32(100, 58, 220, byte.MaxValue);
+        public static List<Arrow> localArrows = new List<Arrow>();
 
         public static float updateIntervall = 5f;
         public static bool resetTargetAfterMeeting = false;
+        public static bool canTrackCorpses = false;
+        public static float corpsesTrackingCooldown = 30f;
+        public static float corpsesTrackingDuration = 5f;
+        public static float corpsesTrackingTimer = 0f;
+        public static List<Vector3> deadBodyPositions = new List<Vector3>();
 
         public static PlayerControl currentTarget;
         public static PlayerControl tracked;
@@ -538,6 +546,14 @@ namespace TheOtherRoles
         public static float timeUntilUpdate = 0f;
         public static Arrow arrow = new Arrow(Color.blue);
 
+        private static Sprite trackCorpsesButtonSprite;
+        public static Sprite getTrackCorpsesButtonSprite()
+        {
+            if (trackCorpsesButtonSprite) return trackCorpsesButtonSprite;
+            trackCorpsesButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PathfindButton.png", 115f);
+            return trackCorpsesButtonSprite;
+        }
+
         private static Sprite buttonSprite;
         public static Sprite getButtonSprite() {
             if (buttonSprite) return buttonSprite;
@@ -559,6 +575,16 @@ namespace TheOtherRoles
             timeUntilUpdate = 0f;
             updateIntervall = CustomOptionHolder.trackerUpdateIntervall.getFloat();
             resetTargetAfterMeeting = CustomOptionHolder.trackerResetTargetAfterMeeting.getBool();
+            if (localArrows != null) {
+                foreach (Arrow arrow in localArrows)
+                    if (arrow?.arrow != null)
+                        UnityEngine.Object.Destroy(arrow.arrow);
+            }
+            deadBodyPositions = new List<Vector3>();
+            corpsesTrackingTimer = 0f;
+            corpsesTrackingCooldown = CustomOptionHolder.trackerCorpsesTrackingCooldown.getFloat();
+            corpsesTrackingDuration = CustomOptionHolder.trackerCorpsesTrackingDuration.getFloat();
+            canTrackCorpses = CustomOptionHolder.trackerCanTrackCorpses.getBool();
         }
     }
 
@@ -949,6 +975,8 @@ namespace TheOtherRoles
 
         public static int remainingShots = 2;
         public static bool hasMultipleShotsPerMeeting = false;
+        public static bool showInfoInGhostChat = true;
+        public static bool killsThroughShield = true;
 
         public static Sprite getTargetSprite() {
             if (targetSprite) return targetSprite;
@@ -961,6 +989,8 @@ namespace TheOtherRoles
             
             remainingShots = Mathf.RoundToInt(CustomOptionHolder.guesserNumberOfShots.getFloat());
             hasMultipleShotsPerMeeting = CustomOptionHolder.guesserHasMultipleShotsPerMeeting.getBool();
+            showInfoInGhostChat = CustomOptionHolder.guesserShowInfoInGhostChat.getBool();
+            killsThroughShield = CustomOptionHolder.guesserKillsThroughShield.getBool();
         }
     }
 
index 49971c9404be01e989454f355fa36bef20f229f3..8bceeed13e0866331a9619be18500638a6ea2d6f 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>netstandard2.1</TargetFramework>
-        <Version>3.0.0</Version>
+        <Version>3.1.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
     </PropertyGroup>
         <Reference Include="$(AmongUs)/BepInEx/unhollowed/*.dll" />
     </ItemGroup>
 
-    <ItemGroup>
-      <None Remove="Resources\MediumButton.png" />
-      <None Remove="Resources\VultureButton.png" />
-    </ItemGroup>
-
     <Target Name="CopyCustomContent" AfterTargets="AfterBuild">
         <Message Text="Second occurrence" />
         <Copy SourceFiles="$(ProjectDir)\bin\$(Configuration)\netstandard2.1\TheOtherRoles.dll" DestinationFolder="$(AmongUsLatest)/BepInEx/plugins/" />