]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Restructure repository
authorEisbison <eisbison.creator@gmail.com>
Thu, 11 Mar 2021 15:25:28 +0000 (16:25 +0100)
committerEisbison <eisbison.creator@gmail.com>
Thu, 11 Mar 2021 15:25:28 +0000 (16:25 +0100)
33 files changed:
README.md
Source Code.zip [deleted file]
Source Code/BonusRoles.cs [new file with mode: 0644]
Source Code/BonusRoles.csproj [new file with mode: 0644]
Source Code/Buttons.cs [new file with mode: 0644]
Source Code/CredentialsPatch.cs [new file with mode: 0644]
Source Code/CustomButton.cs [new file with mode: 0644]
Source Code/EndGamePatch.cs [new file with mode: 0644]
Source Code/Footprint.cs [new file with mode: 0644]
Source Code/GameHistory.cs [new file with mode: 0644]
Source Code/GameOptionsPatch.cs [new file with mode: 0644]
Source Code/Helpers.cs [new file with mode: 0644]
Source Code/IntroPatch.cs [new file with mode: 0644]
Source Code/Main.cs [new file with mode: 0644]
Source Code/MeetingPatch.cs [new file with mode: 0644]
Source Code/PlayerControlPatch.cs [new file with mode: 0644]
Source Code/RPC.cs [new file with mode: 0644]
Source Code/Resources/AdminTableIcon.png [new file with mode: 0644]
Source Code/Resources/CamoButton.png [new file with mode: 0644]
Source Code/Resources/CleanButton.png [new file with mode: 0644]
Source Code/Resources/Footprint.png [new file with mode: 0644]
Source Code/Resources/MorphButton.png [new file with mode: 0644]
Source Code/Resources/RepairButton.png [new file with mode: 0644]
Source Code/Resources/RewindButton.png [new file with mode: 0644]
Source Code/Resources/SampleButton.png [new file with mode: 0644]
Source Code/Resources/SeerButton.png [new file with mode: 0644]
Source Code/Resources/ShieldButton.png [new file with mode: 0644]
Source Code/Resources/ShiftButton.png [new file with mode: 0644]
Source Code/Resources/SwapperCheck.png [new file with mode: 0644]
Source Code/RoleAssignmentPatch.cs [new file with mode: 0644]
Source Code/ShipStatusPatch.cs [new file with mode: 0644]
Source Code/UpdatePatch.cs [new file with mode: 0644]
Source Code/UsablesPatch.cs [new file with mode: 0644]

index c52e80e1d3d584cf4b35c39aea1e046c2afef5fb..4b691ecc432bc6c796f5ad3bf82cc0bde5cd544a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -35,10 +35,18 @@ Even more roles are coming soon :)
 # Releases :\r
 | Among Us - Version| Mod Version | Link |\r
 |----------|-------------|-----------------|\r
+| **2021.3.5s** | v1.4 | [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v1.4/TheOtherRoles.zip)\r
 | **2021.3.5s** | v1.3 | [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v1.3/TheOtherRoles.zip)\r
 | 2020.12.19s | v1.1 | [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v1.1/TheOtherRoles.zip)\r
 | 2020.12.19s | v1.0 | [Download](https://github.com/Eisbison/TheOtherRoles/files/6097191/TheOtherRoles.zip) |\r
 \r
+\\r
+**Changes in v1.4:**\r
+- Fixing a Camoflager/Morphling animation bug\r
+- Fixing a bug where the Swapper could swap votes even if he was dead\r
+- The custom cooldown buttons now render the cooldown progress (the grey overlay) in the right way (v1.3 introduced the bug)\r
+- Players in vents are not targetable anymore by the role actions, the button does not activate (e.g. Seer revealing, Morphling sample). Exception: Impostor killing an Engineer in a vent\r
+\r
 **Changes in v1.3:**\r
 - Adds support for the Among Us version **2021.3.5s**\r
 - Fixes a bug where a an edge case caused all players to start the game with the camouflaged look\r
@@ -51,7 +59,7 @@ Even more roles are coming soon :)
 - A bug was removed where the game of the Lovers crashed if they were the last players killed by the host of the lobby.\r
 \r
 # Installation \r
-1. Download and unzip the newest [release](https://github.com/Eisbison/TheOtherRoles/releases/download/v1.3/TheOtherRoles.zip)\r
+1. Download and unzip the newest [release](https://github.com/Eisbison/TheOtherRoles/releases/download/v1.4/TheOtherRoles.zip)\r
 2. Find the folder of your game, for steams players you can right click in steam, on the game, a menu will appear proposing you to go to the folders.3. Make a copy of your game, it's not obligatory but advise, put it where you want.\r
 4. Drag or extract the files from the zip into your game, at the .exe level.\r
 6. Run the game (the first launch might take a while)\r
@@ -204,6 +212,9 @@ Depending on the selected option the player will get a blue flash on his screen
 ### **Team: Crewmates**\r
 The Engineer (if alive) can fix one sabotage per game from anywhere on the map.\\r
 The Engineer can use vents.\r
+\\r
+**NOTE**\r
+- The kill button of Impostors activates if they stand next to a vent where the Engineer is. They can also kill him there. No other action (e.g. Morphling sample, Shifter shift, ...) can affect players inside vents.\r
 \r
 ### Game Options\r
 | Name | Description |\r
@@ -358,4 +369,4 @@ It's bad I know, this is a side project and my second week of modding. So there
 You can use parts of the codes but don't copy paste the whole thing. Make sure you give credits to the other developers, because some parts of the code are base on theirs. And yes it is in a zip file... I will change that.\r
 \r
 # Bugs, suggestions and requests\r
-If you found any bugs, have an idea for an idea for a new role or any other request, join my [Discord server](https://discord.gg/77RkMJHWsM)\r
+If you found any bugs, have an idea for an idea for a new role or any other request, join my [Discord server](https://discord.gg/77RkMJHWsM)
\ No newline at end of file
diff --git a/Source Code.zip b/Source Code.zip
deleted file mode 100644 (file)
index 3eb0c35..0000000
Binary files a/Source Code.zip and /dev/null differ
diff --git a/Source Code/BonusRoles.cs b/Source Code/BonusRoles.cs
new file mode 100644 (file)
index 0000000..fe6f1d7
--- /dev/null
@@ -0,0 +1,405 @@
+using System.Net;
+using System.Linq;
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using Hazel;
+using Reactor;
+using System;
+using System.Collections.Generic;
+using System.Collections;
+using System.IO;
+using UnityEngine;
+using Reactor.Unstrip;
+using Reactor.Extensions;
+
+namespace BonusRoles
+{
+    [HarmonyPatch]
+    public static class BonusRoles
+    {
+        public static System.Random rnd = new System.Random();
+
+        public static void clearAndReloadRoles() {
+            Jester.clearAndReload();
+            Mayor.clearAndReload();
+            Engineer.clearAndReload();
+            Sheriff.clearAndReload();
+            Lighter.clearAndReload();
+            Godfather.clearAndReload();
+            Mafioso.clearAndReload();
+            Janitor.clearAndReload();
+            Detective.clearAndReload();
+            TimeMaster.clearAndReload();
+            Medic.clearAndReload();
+            Shifter.clearAndReload();
+            Swapper.clearAndReload();
+            Lovers.clearAndReload();
+            Seer.clearAndReload();
+            Morphling.clearAndReload();
+            Camouflager.clearAndReload();
+            Spy.clearAndReload();
+            Child.clearAndReload();
+        }
+
+        public static class Jester {
+            public static PlayerControl jester;
+            public static Color color = new Color(255f / 255f, 84f / 255f, 167f / 255f, 1);
+
+            public static void clearAndReload() {
+                jester = null;
+            }
+        }
+
+        public static class Mayor {
+            public static PlayerControl mayor;
+            public static Color color = new Color(105f / 255f, 58f / 255f, 58f / 255f, 1);
+
+            public static void clearAndReload() {
+                mayor = null;
+            }
+        }
+
+        public static class Engineer {
+            public static PlayerControl engineer;
+            public static Color color = new Color(98f / 255f, 216f / 255f, 240f / 255f, 1);
+            public static bool usedRepair;
+            private static Sprite buttonSprite;
+
+            public static Sprite getButtonSprite() {
+                if (buttonSprite) return buttonSprite;
+                buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.RepairButton.png", 100f);
+                return buttonSprite;
+            }
+
+            public static void clearAndReload() {
+                engineer = null;
+                usedRepair = false;
+            }
+        }
+
+        public static class Godfather {
+            public static PlayerControl godfather;
+            public static Color color = Palette.ImpostorRed;
+
+            public static void clearAndReload() {
+                godfather = null;
+            }
+        }
+
+        public static class Mafioso {
+            public static PlayerControl mafioso;
+            public static Color color = Palette.ImpostorRed;
+
+            public static void clearAndReload() {
+                mafioso = null;
+            }
+        }
+
+
+        public static class Janitor {
+            public static PlayerControl janitor;
+            public static Color color = Palette.ImpostorRed;
+
+            public static float cooldown = float.MaxValue;
+
+            private static Sprite buttonSprite;
+            public static Sprite getButtonSprite() {
+                if (buttonSprite) return buttonSprite;
+                buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.CleanButton.png", 100f);
+                return buttonSprite;
+            }
+
+            public static void clearAndReload() {
+                janitor = null;
+                cooldown = BonusRolesPlugin.janitorCooldown.GetValue();
+            }
+        }
+
+        public static class Sheriff {
+            public static PlayerControl sheriff;
+            public static Color color = new Color(255f / 255f, 204f / 255f, 0f / 255f, 1);
+
+            public static float cooldown = float.MaxValue;
+            public static bool jesterCanDieToSheriff = false;
+
+            public static PlayerControl currentTarget;
+
+            public static void clearAndReload() {
+                sheriff = null;
+                currentTarget = null;
+                cooldown = BonusRolesPlugin.sheriffCooldown.GetValue();
+                jesterCanDieToSheriff = BonusRolesPlugin.jesterCanDieToSheriff.GetValue();
+            }
+        }
+
+        public static class Lighter {
+            public static PlayerControl lighter;
+            public static Color color = new Color(250f / 255f, 204f / 255f, 37f / 255f, 1);
+            
+            public static float lighterVision = 2f;
+
+            public static void clearAndReload() {
+                lighter = null;
+                lighterVision = BonusRolesPlugin.lighterVision.GetValue();
+            }
+        }
+
+        public static class Detective {
+            public static PlayerControl detective;
+            public static Color color = new Color(2f / 255f, 61f / 255f, 156f / 255f, 1);
+
+            public static float footprintIntervall = 1f;
+            public static float footprintDuration = 1f;
+            public static bool anonymousFootprints = false;
+
+            public static float timer = 1f;
+
+            public static void clearAndReload() {
+                detective = null;
+                anonymousFootprints = BonusRolesPlugin.detectiveAnonymousFootprints.GetValue();
+                footprintIntervall = BonusRolesPlugin.detectiveFootprintIntervall.GetValue();
+                footprintDuration = BonusRolesPlugin.detectiveFootprintDuration.GetValue();
+                timer = footprintIntervall;
+            }
+        }
+    }
+
+    public static class TimeMaster {
+        public static PlayerControl timeMaster;
+        public static Color color = new Color(114f / 255f, 234f / 255f, 247f / 255f, 1);
+
+        public static bool reviveDuringRewind = false;
+        public static float rewindTime = 3f;
+        public static float cooldown = float.MaxValue;
+
+        public static bool isRewinding = false;
+
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.RewindButton.png", 100f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            timeMaster = null;
+            isRewinding = false;
+            reviveDuringRewind = BonusRolesPlugin.timeMasterReviveDuringRewind.GetValue();
+            rewindTime = BonusRolesPlugin.timeMasterRewindTime.GetValue();
+            cooldown = BonusRolesPlugin.timeMasterCooldown.GetValue();
+        }
+    }
+
+    public static class Medic {
+        public static PlayerControl medic;
+        public static PlayerControl shielded;
+        public static Color color = new Color(0f / 255f, 80f / 255f, 105f / 255f, 1);
+        public static bool usedShield;
+
+        public static float reportNameDuration = 10f;
+        public static float reportColorDuration = 20f;
+        public static int showShielded = 0;
+        public static bool showAttemptToShielded = false;
+
+        public static Color shieldedColor = new Color(0f / 255f, 221f / 255f, 255f / 255f, 1);
+        public static PlayerControl currentTarget;
+
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.ShieldButton.png", 100f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            medic = null;
+            shielded = null;
+            currentTarget = null;
+            usedShield = false;
+            shieldedColor = new Color(0f / 255f, 221f / 255f, 255f / 255f, 1);
+            reportNameDuration = BonusRolesPlugin.medicReportNameDuration.GetValue();
+            reportColorDuration = BonusRolesPlugin.medicReportColorDuration.GetValue();
+            showShielded = BonusRolesPlugin.medicShowShielded.GetValue();
+            showAttemptToShielded = BonusRolesPlugin.medicShowAttemptToShielded.GetValue();
+        }
+    }
+
+    public static class Shifter {
+        public static PlayerControl shifter;
+        public static Color color = new Color(90f / 255f, 90f / 255f, 90f / 255f, 1);
+    
+        public static float cooldown = float.MaxValue;
+
+        public static PlayerControl currentTarget;
+
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.ShiftButton.png", 100f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            shifter = null;
+            currentTarget = null;
+            cooldown = BonusRolesPlugin.shifterCooldown.GetValue();
+        }
+    }
+
+    public static class Swapper {
+        public static PlayerControl swapper;
+        public static Color color = new Color(240f / 255f, 128f / 255f, 72f / 255f, 1);
+        private static Sprite spriteCheck;
+
+        public static byte playerId1 = Byte.MaxValue;
+        public static byte playerId2 = Byte.MaxValue;
+
+        public static Sprite getCheckSprite() {
+            if (spriteCheck) return spriteCheck;
+            spriteCheck = Helpers.loadSpriteFromResources("BonusRoles.Resources.SwapperCheck.png", 150f);
+            return spriteCheck;
+        }
+
+        public static void clearAndReload() {
+            swapper = null;
+            playerId1 = Byte.MaxValue;
+            playerId2 = Byte.MaxValue;
+        }
+    }
+
+    public static class Lovers {
+        public static PlayerControl lover1;
+        public static PlayerControl lover2;
+        public static Color color = new Color(252f / 255f, 3f / 255f, 190f / 255f, 1);
+
+        public static bool bothDie = true;
+        // Lovers save if next to be exiled is a lover, because RPC of ending game comes before RPC of exiled
+        public static bool notAckedExiledIsLover = false;
+
+        public static bool existingAndAlive() {
+            return lover1 != null && lover2 != null && !lover1.Data.IsDead && !lover2.Data.IsDead && !notAckedExiledIsLover; // ADD NOT ACKED IS LOVER
+        }
+
+        public static bool existingWithImpLover() {
+            return lover1 != null && lover2 != null && (lover1.Data.IsImpostor || lover2.Data.IsImpostor);
+        }
+
+        public static void clearAndReload() {
+            lover1 = null;
+            lover2 = null;
+            notAckedExiledIsLover = false;
+            bothDie = BonusRolesPlugin.loversBothDie.GetValue();
+        }
+    }
+
+    public static class Seer {
+        public static PlayerControl seer;
+        public static Color color = new Color(60f / 255f, 181f / 255f, 100f / 255f, 1);
+        public static List<PlayerControl> revealedPlayers = new List<PlayerControl>();
+
+        public static float cooldown = float.MaxValue;
+        public static int kindOfInfo = 0;
+        public static int playersWithNotification = 0;
+
+        public static PlayerControl currentTarget;
+        
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.SeerButton.png", 100f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            seer = null;
+            revealedPlayers = new List<PlayerControl>();
+            cooldown = BonusRolesPlugin.seerCooldown.GetValue();
+            kindOfInfo = BonusRolesPlugin.seerKindOfInfo.GetValue();
+            playersWithNotification = BonusRolesPlugin.seerPlayersWithNotification.GetValue();
+        }
+    }
+
+    public static class Morphling {
+        public static PlayerControl morphling;
+        public static Color color = Palette.ImpostorRed;
+        private static Sprite sampleSprite;
+        private static Sprite morphSprite;
+    
+        public static float cooldown = float.MaxValue;
+
+        public static PlayerControl currentTarget;
+        public static PlayerControl sampledTarget;
+        public static PlayerControl morphTarget;
+        public static float morphTimer = 0f;
+
+
+        public static void clearAndReload() {
+            morphling = null;
+            currentTarget = null;
+            sampledTarget = null;
+            morphTarget = null;
+            morphTimer = 0f;
+            cooldown = BonusRolesPlugin.morphlingCooldown.GetValue();
+        }
+
+        public static Sprite getSampleSprite() {
+            if (sampleSprite) return sampleSprite;
+            sampleSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.SampleButton.png", 100f);
+            return sampleSprite;
+        }
+
+        public static Sprite getMorphSprite() {
+            if (morphSprite) return morphSprite;
+            morphSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.MorphButton.png", 100f);
+            return morphSprite;
+        }
+    }
+
+    public static class Camouflager {
+        public static PlayerControl camouflager;
+        public static Color color = Palette.ImpostorRed;
+    
+        public static float cooldown = float.MaxValue;
+        public static float camouflageTimer = 0f;
+
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.CamoButton.png", 100f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            camouflager = null;
+            camouflageTimer = 0f;
+            cooldown = BonusRolesPlugin.camouflagerCooldown.GetValue();
+        }
+    }
+
+    public static class Spy {
+        public static PlayerControl spy;
+        private static Sprite adminTableIcon;
+        public static Color color = new Color(252f / 255f, 90f / 255f, 30f / 255f, 1);
+
+        public static Sprite getAdminTableIconSprite() {
+            if (adminTableIcon) return adminTableIcon;
+            adminTableIcon = Helpers.loadSpriteFromResources("BonusRoles.Resources.AdminTableIcon.png", 350f);
+            return adminTableIcon;
+        }
+
+        public static void clearAndReload() {
+            spy = null;
+        }
+    }
+
+    public static class Child {
+        public static PlayerControl child;
+        public static Color color = Color.white;
+
+        public static void clearAndReload() {
+            child = null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/BonusRoles.csproj b/Source Code/BonusRoles.csproj
new file mode 100644 (file)
index 0000000..3df26ef
--- /dev/null
@@ -0,0 +1,51 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>netstandard2.1</TargetFramework>
+        <Version>1.0.0</Version>
+        <Mappings>NuclearPowered/Mappings:0.2.0</Mappings>
+
+        <Description>BonusRoles</Description>
+        <Authors>Eisbison</Authors>
+    </PropertyGroup>
+
+    <PropertyGroup Condition="'$(GamePlatform)' == 'Steam'">
+        <GameVersion>2021.3.5s</GameVersion>
+        <DefineConstants>$(DefineConstants);STEAM</DefineConstants>
+    </PropertyGroup>
+
+    <PropertyGroup Condition="'$(GamePlatform)' == 'Itch'">
+        <GameVersion>2021.3.5i</GameVersion>
+        <DefineConstants>$(DefineConstants);ITCH</DefineConstants>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <Deobfuscate Include="$(AmongUs)\BepInEx\plugins\Reactor-$(GameVersion).dll" />
+
+        <PackageReference Include="Reactor.OxygenFilter.MSBuild" Version="0.2.9" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <Reference Include="Essentials">
+        <HintPath>$(AmongUs)\BepInEx\plugins\Essentials-$(GameVersion).dll</HintPath>
+      </Reference>
+    </ItemGroup>
+
+    <ItemGroup>
+        <EmbeddedResource Include="Resources\RepairButton.png"/>
+        <EmbeddedResource Include="Resources\CleanButton.png"/>
+        <EmbeddedResource Include="Resources\RewindButton.png"/>
+        <EmbeddedResource Include="Resources\ShieldButton.png"/>
+        <EmbeddedResource Include="Resources\ShiftButton.png"/>
+        <EmbeddedResource Include="Resources\SeerButton.png"/>
+        <EmbeddedResource Include="Resources\MorphButton.png"/>
+        <EmbeddedResource Include="Resources\SampleButton.png"/>
+        <EmbeddedResource Include="Resources\CamoButton.png"/>
+        <EmbeddedResource Include="Resources\Footprint.png"/>
+        <EmbeddedResource Include="Resources\AdminTableIcon.png"/>
+        <EmbeddedResource Include="Resources\SwapperCheck.png"/>
+    </ItemGroup>
+
+    <Target Name="Copy" AfterTargets="Reobfuscate">
+        <Copy SourceFiles="$(OutputPath)reobfuscated/$(AssemblyName)-$(GameVersion).dll" DestinationFolder="$(AmongUs)/BepInEx/plugins/" Condition="'$(Configuration)' == 'Debug'" />
+    </Target>
+</Project>
\ No newline at end of file
diff --git a/Source Code/Buttons.cs b/Source Code/Buttons.cs
new file mode 100644 (file)
index 0000000..2fe81fd
--- /dev/null
@@ -0,0 +1,271 @@
+using HarmonyLib;
+using Hazel;
+using System;
+using System.IO;
+using System.Net.Http;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+using Reactor.Extensions;
+using System.Collections.Generic;
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))]
+    static class HudManagerStartPatch
+    {
+        private static CustomButton engineerRepairButton;
+        private static CustomButton janitorCleanButton;
+        private static CustomButton sheriffKillButton;
+        private static CustomButton timeMasterRewindTimeButton;
+        private static CustomButton medicShieldButton;
+        private static CustomButton shifterShiftButton;
+        private static CustomButton seerRevealButton;
+        private static CustomButton morphlingButton;
+        private static CustomButton camouflagerButton;
+
+        public static void setCustomButtonCooldowns() {
+            engineerRepairButton.MaxTimer = 0f;
+            janitorCleanButton.MaxTimer = Janitor.cooldown;
+            sheriffKillButton.MaxTimer = Sheriff.cooldown;
+            timeMasterRewindTimeButton.MaxTimer = TimeMaster.cooldown;
+            medicShieldButton.MaxTimer = 0f;
+            shifterShiftButton.MaxTimer = Shifter.cooldown;
+            seerRevealButton.MaxTimer = Seer.cooldown;
+            morphlingButton.MaxTimer = Morphling.cooldown;
+            camouflagerButton.MaxTimer = Camouflager.cooldown;
+        }
+
+        public static void Postfix(HudManager __instance)
+        {
+            // Engineer Repair
+            engineerRepairButton = new CustomButton(
+                () => {
+                    engineerRepairButton.Timer = 0f;
+
+                    MessageWriter usedRepairWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.EngineerUsedRepair, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(usedRepairWriter);
+                    RPCProcedure.engineerUsedRepair();
+                    foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks) {
+                        if (task.TaskType == TaskTypes.FixLights) {
+                            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.EngineerFixLights, Hazel.SendOption.None, -1);
+                            AmongUsClient.Instance.FinishRpcImmediately(writer);
+                            RPCProcedure.engineerFixLights();
+                        } else if (task.TaskType == TaskTypes.RestoreOxy) {
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.LifeSupp, 0 | 64);
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.LifeSupp, 1 | 64);
+                        } else if (task.TaskType == TaskTypes.ResetReactor) {
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.Reactor, 16);
+                        } else if (task.TaskType == TaskTypes.ResetSeismic) {
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.Laboratory, 16);
+                        } else if (task.TaskType == TaskTypes.FixComms) {
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.Comms, 16 | 0);
+                            ShipStatus.Instance.RpcRepairSystem(SystemTypes.Comms, 16 | 1);
+                        }
+                    }
+                },
+                () => { return Engineer.engineer != null && Engineer.engineer == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => {
+                    bool sabotageActive = false;
+                    foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks)
+                        if (task.TaskType == TaskTypes.FixLights || task.TaskType == TaskTypes.RestoreOxy || task.TaskType == TaskTypes.ResetReactor || task.TaskType == TaskTypes.ResetSeismic || task.TaskType == TaskTypes.FixComms)
+                            sabotageActive = true;
+                    return sabotageActive && !Engineer.usedRepair && PlayerControl.LocalPlayer.CanMove;
+                },
+                false,
+                Engineer.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            // Janitor Clean
+            janitorCleanButton = new CustomButton(
+                () => {
+                    foreach (Collider2D collider2D in Physics2D.OverlapCircleAll(PlayerControl.LocalPlayer.GetTruePosition(), PlayerControl.LocalPlayer.MaxReportDistance, Constants.PlayersOnlyMask)) {
+                        if (collider2D.tag == "DeadBody")
+                        {
+                            DeadBody component = collider2D.GetComponent<DeadBody>();
+                            if (component && !component.Reported)
+                            {
+                                Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+                                Vector2 truePosition2 = component.TruePosition;
+                                if (Vector2.Distance(truePosition2, truePosition) <= PlayerControl.LocalPlayer.MaxReportDistance && PlayerControl.LocalPlayer.CanMove && !PhysicsHelpers.AnythingBetween(truePosition, truePosition2, Constants.ShipAndObjectsMask, false))
+                                {
+                                    GameData.PlayerInfo playerInfo = GameData.Instance.GetPlayerById(component.ParentId);
+                                    
+                                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.JanitorClean, Hazel.SendOption.None, -1);
+                                    writer.Write(playerInfo.PlayerId);
+                                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                                    RPCProcedure.janitorClean(playerInfo.PlayerId);
+                                    janitorCleanButton.Timer = janitorCleanButton.MaxTimer;
+
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                },
+                () => { return Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return __instance.ReportButton.renderer.color == Palette.EnabledColor  && PlayerControl.LocalPlayer.CanMove; },
+                true,
+                Janitor.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            // Sheriff Kill
+            sheriffKillButton = new CustomButton(
+                () => {
+                    if (Medic.shielded != null && Medic.shielded == Sheriff.currentTarget) {
+                        MessageWriter attemptWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.None, -1);
+                        AmongUsClient.Instance.FinishRpcImmediately(attemptWriter);
+                        RPCProcedure.shieldedMurderAttempt();
+                        
+                        return;    
+                    }
+
+                    byte targetId = 0;
+                    if (Sheriff.currentTarget.Data.IsImpostor || (Sheriff.jesterCanDieToSheriff && Jester.jester != null && Jester.jester == Sheriff.currentTarget))
+                        targetId = Sheriff.currentTarget.PlayerId;
+                    else
+                        targetId = PlayerControl.LocalPlayer.PlayerId;
+
+                    MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SheriffKill, Hazel.SendOption.None, -1);
+                    killWriter.Write(targetId);
+                    AmongUsClient.Instance.FinishRpcImmediately(killWriter);
+                    RPCProcedure.sheriffKill(targetId);
+
+                    sheriffKillButton.Timer = sheriffKillButton.MaxTimer; 
+                    Sheriff.currentTarget = null;
+                },
+                () => { return Sheriff.sheriff != null && Sheriff.sheriff == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return Sheriff.currentTarget && PlayerControl.LocalPlayer.CanMove; },
+                true,
+                __instance.KillButton.renderer.sprite,
+                Vector3.zero,
+                __instance
+            );
+
+            // Time Master Rewind Time
+            timeMasterRewindTimeButton = new CustomButton(
+                () => {
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.TimeMasterRewindTime, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.timeMasterRewindTime();
+                    timeMasterRewindTimeButton.Timer = timeMasterRewindTimeButton.MaxTimer;
+                },
+                () => { return TimeMaster.timeMaster != null && TimeMaster.timeMaster == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return PlayerControl.LocalPlayer.CanMove; },
+                true,
+                TimeMaster.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            // Medic Shield
+            medicShieldButton = new CustomButton(
+                () => {
+                    medicShieldButton.Timer = 0f;
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.MedicSetShielded, Hazel.SendOption.None, -1);
+                    writer.Write(Medic.currentTarget.PlayerId);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.medicSetShielded(Medic.currentTarget.PlayerId);
+                },
+                () => { return Medic.medic != null && Medic.medic == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return !Medic.usedShield && Medic.currentTarget && PlayerControl.LocalPlayer.CanMove; },
+                false,
+                Medic.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            
+            // Shifter shift
+            shifterShiftButton = new CustomButton(
+                () => {
+                    shifterShiftButton.Timer = shifterShiftButton.MaxTimer;
+
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShifterShift, Hazel.SendOption.None, -1);
+                    writer.Write(Shifter.currentTarget.PlayerId);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    
+                    RPCProcedure.shifterShift(Shifter.currentTarget.PlayerId);
+                },
+                () => { return Shifter.shifter != null && Shifter.shifter == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return Shifter.currentTarget && PlayerControl.LocalPlayer.CanMove; },
+                true,
+                Shifter.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            // Seer reveal
+            seerRevealButton = new CustomButton(
+                () => {
+                    seerRevealButton.Timer = seerRevealButton.MaxTimer;
+
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SeerReveal, Hazel.SendOption.None, -1);
+                    writer.Write(Seer.currentTarget.PlayerId);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    
+                    RPCProcedure.seerReveal(Seer.currentTarget.PlayerId);
+                },
+                () => { return Seer.seer != null && Seer.seer == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return Seer.currentTarget && PlayerControl.LocalPlayer.CanMove; },
+                false,
+                Seer.getButtonSprite(),
+                Vector3.zero,
+                __instance
+            );
+
+            // Morphling morph
+            morphlingButton = new CustomButton(
+                () => {
+                    if (Morphling.sampledTarget != null) {
+                        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.MorphlingMorph, Hazel.SendOption.None, -1);
+                        writer.Write(Morphling.sampledTarget.PlayerId);
+                        AmongUsClient.Instance.FinishRpcImmediately(writer);
+                        RPCProcedure.morphlingMorph(Morphling.sampledTarget.PlayerId);
+                        Morphling.sampledTarget = null;
+                        morphlingButton.HasEffect = true; // Trigger effect on this click
+                    } else if (Morphling.currentTarget != null) {
+                        Morphling.sampledTarget = Morphling.currentTarget;
+                        morphlingButton.Sprite = Morphling.getMorphSprite();
+                        morphlingButton.HasEffect = false; // Block effect on this click
+                    }
+                },
+                () => { return Morphling.morphling != null && Morphling.morphling == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return (Morphling.currentTarget || Morphling.sampledTarget) && PlayerControl.LocalPlayer.CanMove; },
+                true,
+                Morphling.getSampleSprite(),
+                new Vector3(0f, 1.3f, 0f),
+                __instance,
+                true,
+                10f,
+                () => {
+                    morphlingButton.Timer = morphlingButton.MaxTimer;
+                    morphlingButton.Sprite = Morphling.getSampleSprite();
+                }
+            );
+
+            // Camouflager camouflage
+            camouflagerButton = new CustomButton(
+                () => {
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CamouflagerCamouflage, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.camouflagerCamouflage();
+                },
+                () => { return Camouflager.camouflager != null && Camouflager.camouflager == PlayerControl.LocalPlayer && __instance.UseButton.isActiveAndEnabled && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return PlayerControl.LocalPlayer.CanMove; },
+                true,
+                Camouflager.getButtonSprite(),
+                new Vector3(0f, 1.3f, 0f),
+                __instance,
+                true,
+                10f,
+                () => { camouflagerButton.Timer = camouflagerButton.MaxTimer; }
+            );
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/CredentialsPatch.cs b/Source Code/CredentialsPatch.cs
new file mode 100644 (file)
index 0000000..c2a8c0a
--- /dev/null
@@ -0,0 +1,31 @@
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
+    public static class VersionShowerPatch
+    {
+        static void Postfix(VersionShower __instance) {
+            string spacer = new String('\n', 15);
+            if (__instance.text.Text.Contains(spacer))
+                __instance.text.Text += "\n[FFFFFFFF]- Loaded [FCCE03FF]TheOtherRoles[FFFFFFFF] v1.4\n  by [FCCE03FF]Eisbison";
+            else
+                __instance.text.Text += spacer + "[FFFFFFFF]- Loaded [FCCE03FF]TheOtherRoles[FFFFFFFF] v1.4\n  by [FCCE03FF]Eisbison";
+        }
+    }
+
+    [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))]
+    public static class PingTrackerPatch
+    {
+        static void Postfix(VersionShower __instance)
+        {
+            __instance.text.Text += "\n[FCCE03FF]TheOtherRoles[FFFFFFFF]";
+            __instance.text.Text += "\nModded by [FCCE03FF]Eisbison";
+        }
+    }
+}
diff --git a/Source Code/CustomButton.cs b/Source Code/CustomButton.cs
new file mode 100644 (file)
index 0000000..e89bf31
--- /dev/null
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Reactor.Extensions;
+using Reactor.Unstrip;
+using UnityEngine;
+
+public class CustomButton
+{
+    public static List<CustomButton> buttons = new List<CustomButton>();
+    public KillButtonManager killButtonManager;
+    public Vector3 PositionOffset;
+    public float MaxTimer = float.MaxValue;
+    public float Timer = 0f;
+    public bool ResetTimeAfterMeeting;
+    private Action OnClick;
+    private Func<bool> HasButton;
+    private Func<bool> CouldUse;
+    private Action OnEffectEnds;
+    public bool HasEffect;
+    private bool isEffectActive = false;
+    private float EffectDuration;
+    public Sprite Sprite;
+    private HudManager hudManager;
+
+    public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, bool ResetTimeAfterMeeting, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool HasEffect, float EffectDuration, Action OnEffectEnds)
+    {
+        this.hudManager = hudManager;
+        this.OnClick = OnClick;
+        this.HasButton = HasButton;
+        this.CouldUse = CouldUse;
+        this.PositionOffset = PositionOffset;
+        this.ResetTimeAfterMeeting = ResetTimeAfterMeeting;
+        this.HasEffect = HasEffect;
+        this.EffectDuration = EffectDuration;
+        this.OnEffectEnds = OnEffectEnds;
+        this.Sprite = Sprite;
+        Timer = 16.2f;
+        buttons.Add(this);
+        killButtonManager = UnityEngine.Object.Instantiate(hudManager.KillButton, hudManager.transform);
+        PassiveButton button = killButtonManager.GetComponent<PassiveButton>();
+        button.OnClick.RemoveAllListeners();
+        button.OnClick.AddListener((UnityEngine.Events.UnityAction)listener);
+
+        void listener()
+        {
+            if (this.Timer < 0f && HasButton() && CouldUse())
+            {
+                killButtonManager.renderer.color = new Color(1f, 1f, 1f, 0.3f);
+                this.OnClick();
+
+                if (this.HasEffect && !this.isEffectActive) {
+                    this.Timer = this.EffectDuration;
+                    killButtonManager.TimerText.Color = new Color(0F, 0.8F, 0F);
+                    this.isEffectActive = true;
+                }
+            }
+        }
+        setActive(false);
+    }
+
+    public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, bool ResetTimeAfterMeeting, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager)
+    : this(OnClick, HasButton, CouldUse, ResetTimeAfterMeeting, Sprite, PositionOffset, hudManager, false, 0f, () => {}) { }
+
+
+    public static void HudUpdate()
+    {
+        buttons.RemoveAll(item => item.killButtonManager == null);
+        if (MeetingHud.Instance || ExileController.Instance) return;
+    
+        for (int i = 0; i < buttons.Count; i++)
+        {
+            try
+            {
+                buttons[i].Update();
+            }
+            catch (NullReferenceException)
+            {
+                System.Console.WriteLine("[WARNING] NullReferenceException from HudUpdate().HasButton(), if theres only one warning its fine");
+            }
+        }
+    }
+
+    public static void MeetingEndedUpdate() {
+        buttons.RemoveAll(item => item.killButtonManager == null);
+        for (int i = 0; i < buttons.Count; i++)
+        {
+            try
+            {
+                if (buttons[i].ResetTimeAfterMeeting) {
+                    buttons[i].Timer = buttons[i].MaxTimer;
+                    buttons[i].Update();
+                }
+            }
+            catch (NullReferenceException)
+            {
+                System.Console.WriteLine("[WARNING] NullReferenceException from MeetingEndedUpdate().HasButton(), if theres only one warning its fine");
+            }
+        }
+    }
+
+    public static void ResetAllCooldowns() {
+        for (int i = 0; i < buttons.Count; i++)
+        {
+            try
+            {
+                buttons[i].Timer = buttons[i].MaxTimer;
+                buttons[i].Update();
+            }
+            catch (NullReferenceException)
+            {
+                System.Console.WriteLine("[WARNING] NullReferenceException from MeetingEndedUpdate().HasButton(), if theres only one warning its fine");
+            }
+        }
+    }
+
+    public void setActive(bool isActive) {
+        if (isActive) {
+            killButtonManager.gameObject.SetActive(true);
+            killButtonManager.renderer.enabled = true;
+        } else {
+            killButtonManager.gameObject.SetActive(false);
+            killButtonManager.renderer.enabled = false;
+        }
+    }
+
+    private void Update()
+    {
+        if (PlayerControl.LocalPlayer.Data == null || !HasButton()) {
+            setActive(false);
+            return;
+        }
+        setActive(true);
+
+        killButtonManager.renderer.sprite = Sprite;
+
+        if (killButtonManager.transform.position == hudManager.KillButton.transform.position)
+        {
+            Vector3 vector = killButtonManager.transform.localPosition;
+            vector += new Vector3(PositionOffset.x, PositionOffset.y);
+            killButtonManager.transform.localPosition = vector;
+        }
+
+
+        if (CouldUse()) {
+            killButtonManager.renderer.color = Palette.EnabledColor;
+            killButtonManager.renderer.material.SetFloat("_Desat", 0f);
+        } else {
+            killButtonManager.renderer.color = Palette.DisabledColor;
+            killButtonManager.renderer.material.SetFloat("_Desat", 1f);
+        }
+
+        if (Timer >= 0) {
+            Timer -= Time.deltaTime;
+        }
+        
+        if (Timer <= 0 && HasEffect && isEffectActive) {
+            isEffectActive = false;
+            killButtonManager.TimerText.Color = Palette.EnabledColor;
+            OnEffectEnds();
+        }
+    
+        killButtonManager.SetCoolDown(Timer, (HasEffect && isEffectActive) ? EffectDuration : MaxTimer);
+    }
+}
\ No newline at end of file
diff --git a/Source Code/EndGamePatch.cs b/Source Code/EndGamePatch.cs
new file mode 100644 (file)
index 0000000..499d028
--- /dev/null
@@ -0,0 +1,275 @@
+  
+using HarmonyLib;
+using static BonusRoles.BonusRoles;
+using static BonusRoles.GameHistory;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using System.Linq;
+using Hazel;
+using UnhollowerBaseLib;
+using System;
+using System.Text;
+
+namespace BonusRoles {
+    enum WinCondition {
+        Default,
+        LoversTeamWin,
+        LoversSoloWin,
+        JesterWin,
+        ChildWin
+    }
+
+    static class AdditionalTempData {
+        // Should be implemented using a proper GameOverReason in the future
+        public static WinCondition winCondition = WinCondition.Default;
+        public static bool localIsLover = false;
+
+
+        public static void clear() {
+            winCondition = WinCondition.Default;
+            localIsLover = false;
+
+        }
+    }
+
+    [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))]
+    public class OnGameEndPatch {
+        public static void Postfix(AmongUsClient __instance, GameOverReason OFLKLGMHBEL, bool JFFPAKGPNJA) {
+            AdditionalTempData.clear();
+
+            // Remove shifter from winners
+            if (Shifter.shifter != null) {
+                WinningPlayerData shifterWinner = null;
+                foreach (WinningPlayerData winner in  TempData.winners)
+                    if (winner.Name == Shifter.shifter.Data.PlayerName) shifterWinner = winner;
+                
+                if (shifterWinner != null) TempData.winners.Remove(shifterWinner);
+            }
+            // Remove Jester from winners (on Jester win he will be added again, see below)
+            if (Jester.jester != null) {
+                WinningPlayerData jesterWinner = null;
+                foreach (WinningPlayerData winner in  TempData.winners)
+                    if (winner.Name == Jester.jester.Data.PlayerName) jesterWinner = winner;
+                
+                if (jesterWinner != null) TempData.winners.Remove(jesterWinner);
+            }
+
+
+            // Child win condition (should be implemented using a proper GameOverReason in the future)
+            if (Child.child != null && Child.child.Data.IsImpostor) {
+                AdditionalTempData.winCondition = WinCondition.ChildWin;
+                TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+                WinningPlayerData wpd = new WinningPlayerData(Child.child.Data);
+                wpd.IsImpostor = false; 
+                TempData.winners.Add(wpd);
+            }
+
+            // Jester win condition (should be implemented using a proper GameOverReason in the future)
+            else if (Jester.jester != null && Jester.jester.Data.IsImpostor) {
+                AdditionalTempData.winCondition = WinCondition.JesterWin;
+                TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+                WinningPlayerData wpd = new WinningPlayerData(Jester.jester.Data);
+                wpd.IsImpostor = false; 
+                TempData.winners.Add(wpd);
+            }
+
+            // Lovers win conditions (should be implemented using a proper GameOverReason in the future)
+            else if (Lovers.existingAndAlive()) {
+                AdditionalTempData.localIsLover = (PlayerControl.LocalPlayer == Lovers.lover1 || PlayerControl.LocalPlayer == Lovers.lover2);
+                // Double win for lovers, crewmates also win
+                if (TempData.DidHumansWin(OFLKLGMHBEL)) {
+                    AdditionalTempData.winCondition = WinCondition.LoversTeamWin;
+                }
+                // Lovers solo win
+                else if (Lovers.existingWithImpLover()){
+                    AdditionalTempData.winCondition = WinCondition.LoversSoloWin;
+                    TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+                    TempData.winners.Add(new WinningPlayerData(Lovers.lover1.Data));
+                    TempData.winners.Add(new WinningPlayerData(Lovers.lover2.Data));
+                }
+            }
+
+            // Reset Bonus Roles Settings
+            clearAndReloadRoles();
+            clearGameHistory();
+        }
+    }
+
+    [HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
+    public class EndGameManagerSetUpPatch {
+        public static void Postfix(EndGameManager __instance) {
+            GameObject bonusText = UnityEngine.Object.Instantiate(__instance.WinText.gameObject);
+            bonusText.transform.position = new Vector3(__instance.WinText.transform.position.x, __instance.WinText.transform.position.y - 0.8f, __instance.WinText.transform.position.z);
+            bonusText.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
+            TextRenderer textRenderer = bonusText.GetComponent<TextRenderer>();
+            textRenderer.Text = "";
+
+            if (AdditionalTempData.winCondition == WinCondition.ChildWin) {
+                textRenderer.Text = "Child Wins";
+                textRenderer.Color = Child.color;
+            }
+            else if (AdditionalTempData.winCondition == WinCondition.JesterWin) {
+                textRenderer.Text = "Jester Wins";
+                textRenderer.Color = Jester.color;
+            }
+            else if (AdditionalTempData.winCondition == WinCondition.LoversTeamWin) {
+                if (AdditionalTempData.localIsLover) {
+                    __instance.WinText.Text = "Double Victory";
+                } 
+                textRenderer.Text = "Lovers And Crewmates Win";
+                textRenderer.Color = Lovers.color;
+                __instance.BackgroundBar.material.SetColor("_Color", Lovers.color);
+            } else if (AdditionalTempData.winCondition == WinCondition.LoversSoloWin) {
+                textRenderer.Text = "Lovers Win";
+                textRenderer.Color = Lovers.color;
+                __instance.BackgroundBar.material.SetColor("_Color", Lovers.color);
+            }
+            
+            AdditionalTempData.clear();
+        }
+    }
+
+    [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.CheckEndCriteria))] 
+    class CheckEndCriteriaPatch{
+        private static void ReviveEveryone() {
+            for (int i = 0; i < GameData.Instance.PlayerCount; i++)
+                GameData.Instance.AllPlayers[i].Object.Revive();
+            DeadBody[] array = UnityEngine.Object.FindObjectsOfType<DeadBody>();
+            for (int i = 0; i < array.Length; i++) UnityEngine.Object.Destroy(array[i].gameObject);
+        }
+
+        private static void EndGameForSabotage(ShipStatus __instance)
+        {
+            if (!DestroyableSingleton<TutorialManager>.InstanceExists)
+            {
+                __instance.enabled = false;
+                ShipStatus.RpcEndGame(GameOverReason.ImpostorBySabotage, false);
+                return;
+            }
+            DestroyableSingleton<HudManager>.Instance.ShowPopUp(DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.GameOverSabotage, new Il2CppReferenceArray<Il2CppSystem.Object>(0)));
+        }
+
+        public static bool Prefix(ShipStatus __instance) {
+            if (!GameData.Instance)
+            {
+                return false;
+            }
+            ISystemType systemType = __instance.Systems.ContainsKey(SystemTypes.LifeSupp) ? __instance.Systems[SystemTypes.LifeSupp] : null;
+            if (systemType != null)
+            {
+                LifeSuppSystemType lifeSuppSystemType = systemType.TryCast<LifeSuppSystemType>();
+                if (lifeSuppSystemType.Countdown < 0f)
+                {
+                    EndGameForSabotage(__instance);
+                    lifeSuppSystemType.Countdown = 10000f;
+                }
+            }
+            ISystemType systemType2 = __instance.Systems.ContainsKey(SystemTypes.Reactor) ? __instance.Systems[SystemTypes.Reactor] : null;
+            if (systemType2 == null) systemType2 = __instance.Systems.ContainsKey(SystemTypes.Laboratory) ? __instance.Systems[SystemTypes.Laboratory] : null;
+            if (systemType2 != null)
+            {
+                ReactorSystemType reactorSystemType = systemType2.TryCast<ReactorSystemType>();
+                if (reactorSystemType.Countdown < 0f)
+                {
+                    EndGameForSabotage(__instance);
+                    reactorSystemType.Countdown = 10000f;
+                }
+            }
+            int num = 0;
+            int num2 = 0;
+            int num3 = 0;
+            for (int i = 0; i < GameData.Instance.PlayerCount; i++)
+            {
+                GameData.PlayerInfo playerInfo = GameData.Instance.AllPlayers[i];
+                if (!playerInfo.Disconnected)
+                {
+                    if (playerInfo.IsImpostor)
+                    {
+                        num3++;
+                    }
+                    if (!playerInfo.IsDead)
+                    {
+                        if (playerInfo.IsImpostor)
+                        {
+                            num2++;
+                        }
+                        else
+                        {
+                            num++;
+                        }
+                    }
+                }
+            }
+            if (num2 <= 0 && (!DestroyableSingleton<TutorialManager>.InstanceExists || num3 > 0))
+            {
+                if (!DestroyableSingleton<TutorialManager>.InstanceExists)
+                {
+                    __instance.enabled = false;
+                    ShipStatus.RpcEndGame(GameOverReason.HumansByVote, false);
+                    return false;
+                }
+                DestroyableSingleton<HudManager>.Instance.ShowPopUp(DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.GameOverImpostorDead, new Il2CppReferenceArray<Il2CppSystem.Object>(0)));
+                ReviveEveryone();
+                return false;
+            }
+            else
+            {
+                if (num > num2)
+                {
+                    bool localCompletedAllTasks = true;
+                    foreach (PlayerTask t in PlayerControl.LocalPlayer.myTasks) {
+                        localCompletedAllTasks = localCompletedAllTasks && t.IsComplete;
+                    }
+
+                    if (!DestroyableSingleton<TutorialManager>.InstanceExists)
+                    {
+                        if (GameData.Instance.TotalTasks <= GameData.Instance.CompletedTasks)
+                        {
+                            __instance.enabled = false;
+                            ShipStatus.RpcEndGame(GameOverReason.HumansByTask, false);
+                            return false;
+                        }
+                    }
+                    else if (localCompletedAllTasks)
+                    {
+                        DestroyableSingleton<HudManager>.Instance.ShowPopUp(DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.GameOverTaskWin, new Il2CppReferenceArray<Il2CppSystem.Object>(0)));
+                        __instance.Begin();
+                    }
+                    if (num + num2 == 3 && Lovers.existingAndAlive()) { // 3 players with 2 lovers is always a lover win (either shared with crewmates or solo for lovers, marked as impostor win)
+                        __instance.enabled = false;
+                        ShipStatus.RpcEndGame(Lovers.existingWithImpLover() ? GameOverReason.ImpostorByKill : GameOverReason.HumansByVote, false); // should be implemented using a proper GameOverReason in the future
+                        return false;
+                    }
+                    return false;
+                }
+                if (num == num2 && Lovers.existingAndAlive() && Lovers.existingWithImpLover()) { // 3 vs 3 or 2 vs 2 is not win if both lovers are alive and one is an impostor
+                    return false;
+                }
+                if (!DestroyableSingleton<TutorialManager>.InstanceExists)
+                {
+                    __instance.enabled = false;
+                    GameOverReason endReason;
+                    switch (TempData.LastDeathReason)
+                    {
+                    case DeathReason.Exile:
+                        endReason = GameOverReason.ImpostorByVote;
+                        break;
+                    case DeathReason.Kill:
+                        endReason = GameOverReason.ImpostorByKill;
+                        break;
+                    default:
+                        endReason = GameOverReason.ImpostorByVote;
+                        break;
+                    }
+                    ShipStatus.RpcEndGame(endReason, false);
+                    return false;
+                }
+                DestroyableSingleton<HudManager>.Instance.ShowPopUp(DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.GameOverImpostorKills, new Il2CppReferenceArray<Il2CppSystem.Object>(0)));
+                
+                ReviveEveryone();
+                return false;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/Footprint.cs b/Source Code/Footprint.cs
new file mode 100644 (file)
index 0000000..7cc24c8
--- /dev/null
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Collections;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+
+namespace BonusRoles{
+    class Footprint {
+        private static List<Footprint> footprints = new List<Footprint>();
+        private static Sprite sprite;
+        private Color color;
+        private GameObject footprint;
+        private SpriteRenderer spriteRenderer;
+
+        public static Sprite getFootprintSprite() {
+            if (sprite) return sprite;
+            sprite = Helpers.loadSpriteFromResources("BonusRoles.Resources.Footprint.png", 600f);
+            return sprite;
+        }
+
+        public Footprint(float footprintDuration, bool anonymousFootprints, PlayerControl player) {
+            if (anonymousFootprints)
+                this.color = new Color(0.2f, 0.2f, 0.2f, 1f);
+            else
+                this.color = Palette.PlayerColors[(int) player.Data.ColorId];
+
+            footprint = new GameObject("Footprint");
+            Vector3 position = new Vector3(player.transform.position.x, player.transform.position.y, player.transform.position.z + 1f);
+            footprint.transform.position = position;
+            footprint.transform.localPosition = position;
+            footprint.transform.SetParent(player.transform.parent);
+
+            footprint.transform.Rotate(0.0f, 0.0f, UnityEngine.Random.Range(0.0f, 360.0f));
+
+
+            spriteRenderer = footprint.AddComponent<SpriteRenderer>();
+            spriteRenderer.sprite = getFootprintSprite();
+            spriteRenderer.color = color;
+
+            footprint.SetActive(true);
+            footprints.Add(this);
+
+            Reactor.Coroutines.Start(CoFadeOutAndDestroy(footprintDuration));
+        }
+
+        IEnumerator CoFadeOutAndDestroy(float duration)
+        {
+            for (float t = 0f; t < duration; t += Time.deltaTime)
+            {
+                // Check not null since if game ends, this access throws an error
+                if (spriteRenderer) spriteRenderer.color = new Color(color.r, color.g, color.b, Mathf.Clamp(1f - t/duration, 0f, 1f));
+
+                yield return null;
+            }
+            UnityEngine.Object.Destroy(footprint);
+            footprints.Remove(this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/GameHistory.cs b/Source Code/GameHistory.cs
new file mode 100644 (file)
index 0000000..20c7552
--- /dev/null
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Collections;
+using System;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+
+namespace BonusRoles{
+    public class DeadPlayer
+    {
+        public PlayerControl player;
+        public DateTime timeOfDeath;
+        public DeathReason deathReason;
+        public PlayerControl killerIfExisting;
+
+        public DeadPlayer(PlayerControl player, DateTime timeOfDeath, DeathReason deathReason, PlayerControl killerIfExisting) {
+            this.player = player;
+            this.timeOfDeath = timeOfDeath;
+            this.deathReason = deathReason;
+            this.killerIfExisting = killerIfExisting;
+        }
+    }
+
+    static class GameHistory {
+        public static List<Tuple<Vector3, DateTime>> localPlayerPositions = new List<Tuple<Vector3, DateTime>>();
+        public static List<DeadPlayer> deadPlayers = new List<DeadPlayer>();
+
+        public static void clearGameHistory() {
+            localPlayerPositions = new List<Tuple<Vector3, DateTime>>();
+            deadPlayers = new List<DeadPlayer>();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/GameOptionsPatch.cs b/Source Code/GameOptionsPatch.cs
new file mode 100644 (file)
index 0000000..5e592b4
--- /dev/null
@@ -0,0 +1,23 @@
+using HarmonyLib;
+using System;
+using System.IO;
+using System.Net.Http;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+using Reactor.Extensions;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace BonusRoles
+{
+    [HarmonyPriority(Priority.Low)] 
+    [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Update))]
+    class GameOptionsMenuUpdatePatch
+    {
+        public static void Postfix(GameOptionsMenu __instance)
+        {
+            __instance.GetComponentInParent<Scroller>().YBounds.max = -0.5F + __instance.Children.Length * 0.5F;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/Helpers.cs b/Source Code/Helpers.cs
new file mode 100644 (file)
index 0000000..3b35483
--- /dev/null
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Reactor.Extensions;
+using System.Collections;
+using Reactor.Unstrip;
+using UnhollowerBaseLib;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+
+namespace BonusRoles {
+    public static class Helpers {
+
+        public static Sprite LoadSpriteFromEmbeddedResources(string resource, float PixelPerUnit) {
+            try {
+                System.Reflection.Assembly myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
+                System.IO.Stream myStream = myAssembly.GetManifestResourceStream(resource);
+                byte[] image = new byte[myStream.Length];
+                myStream.Read(image, 0, (int) myStream.Length);
+                Texture2D myTexture = new Texture2D(2, 2, TextureFormat.ARGB32, true);
+                LoadImage(myTexture, image, true);
+                return Sprite.Create(myTexture, new Rect(0, 0, myTexture.width, myTexture.height), new Vector2(0.5f, 0.5f), PixelPerUnit);
+            } catch { }
+            return null;
+        }
+
+        internal delegate bool d_LoadImage(IntPtr tex, IntPtr data, bool markNonReadable);
+        internal static d_LoadImage iCall_LoadImage;
+        private static bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable) {
+            if (iCall_LoadImage == null)
+                iCall_LoadImage = IL2CPP.ResolveICall<d_LoadImage>("UnityEngine.ImageConversion::LoadImage");
+
+            var il2cppArray = (Il2CppStructArray<byte>) data;
+
+            return iCall_LoadImage.Invoke(tex.Pointer, il2cppArray.Pointer, markNonReadable);
+        }
+
+        public static int a  = 0;
+
+        public static Sprite loadSpriteFromResources(string path, float pixelsPerUnit) {
+            try {
+            Texture2D texture = GUIExtensions.CreateEmptyTexture();
+            Assembly assembly = Assembly.GetExecutingAssembly();
+            Stream stream = assembly.GetManifestResourceStream(path);
+            byte[] byteTexture = Reactor.Extensions.Extensions.ReadFully(stream);
+            ImageConversion.LoadImage(texture, byteTexture, false);
+            return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
+            } catch {
+                System.Console.WriteLine("Error loading sprite from path: " + path);
+            }
+            return null;
+        }
+
+        public static PlayerControl playerById(byte id)
+        {
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                if (player.PlayerId == id)
+                    return player;
+            return null;
+        }
+        
+        public static Dictionary<byte, PlayerControl> allPlayersById()
+        {
+            Dictionary<byte, PlayerControl> res = new Dictionary<byte, PlayerControl>();
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                res.Add(player.PlayerId, player);
+            return res;
+        }
+
+        public static void setSkinWithAnim(PlayerPhysics playerPhysics, uint skinId) {
+            SkinData nextSkin = DestroyableSingleton<HatManager>.Instance.AllSkins[(int)skinId];
+            AnimationClip clip = null;
+            var spriteAnim = playerPhysics.Skin.animator;
+            var anim = spriteAnim.m_animator;
+            var skinLayer = playerPhysics.Skin;
+
+            var currentPhysicsAnim = playerPhysics.Animator.GetCurrentAnimation();
+            if (currentPhysicsAnim == playerPhysics.RunAnim) clip = nextSkin.RunAnim;
+            else if (currentPhysicsAnim == playerPhysics.SpawnAnim) clip = nextSkin.SpawnAnim;
+            else if (currentPhysicsAnim == playerPhysics.EnterVentAnim) clip = nextSkin.EnterVentAnim;
+            else if (currentPhysicsAnim == playerPhysics.ExitVentAnim) clip = nextSkin.ExitVentAnim;
+            else if (currentPhysicsAnim == playerPhysics.IdleAnim) clip = nextSkin.IdleAnim;
+            else clip = nextSkin.IdleAnim;
+
+            float progress = playerPhysics.Animator.m_animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
+            skinLayer.skin = nextSkin;
+
+            spriteAnim.Play(clip, 1f);
+            anim.Play("a", 0, progress % 1);
+            anim.Update(0f);
+        }
+
+        public static IEnumerator CoFlashAndDisable(SpriteRenderer renderer, float duration, Color a, Color b) {
+            float singleDuration = duration / 2;
+            for (float t = 0f; t < singleDuration; t += Time.deltaTime) {
+                if (renderer != null)
+                    renderer.color = Color.Lerp(a, b, Mathf.Clamp(t / singleDuration, 0, 1));
+                yield return null;
+            }
+            for (float t = singleDuration; t > 0f; t -= Time.deltaTime) {
+                if (renderer != null)
+                    renderer.color = Color.Lerp(a, b, Mathf.Clamp(t / singleDuration, 0, 1));
+                yield return null;
+            }
+            
+            if (renderer != null) renderer.enabled = false;
+        }
+
+        public static IEnumerator Slide2D(Transform target, Vector2 source, Vector2 dest, float duration = 0.75f)
+        {
+            Vector3 temp = default(Vector3);
+            temp.z = target.localPosition.z;
+            for (float time = 0f; time < duration; time += Time.deltaTime)
+            {
+                float num = time / duration;
+                temp.x = Mathf.SmoothStep(source.x, dest.x, num);
+                temp.y = Mathf.SmoothStep(source.y, dest.y, num);
+                target.localPosition = temp;
+                yield return null;
+            }
+            temp.x = dest.x;
+            temp.y = dest.y;
+            target.localPosition = temp;
+            yield break;
+        }
+
+        public static string colorToHex(Color c) {
+            return string.Format("[{0:X2}{1:X2}{2:X2}{3:X2}]", (int)c.r, (int)c.g, (int)c.b, (int)c.a);
+        }
+    }
+
+    public class SeerInfo {
+        public Color color;
+        public string roleName;
+        public bool isGood;
+
+        public static SeerInfo getSeerInfoForPlayer(PlayerControl p) {
+            string r = "";
+            bool g = true;
+            Color c = Color.white;
+
+            if (Jester.jester != null && p == Jester.jester) {
+                r = "Jester";
+                c = Jester.color;
+                g = false;
+            }
+            else if (Mayor.mayor != null && p == Mayor.mayor) {
+                r = "Mayor";   
+                c = Mayor.color;
+            }
+            else if (Engineer.engineer != null && p == Engineer.engineer) {
+                r = "Engineer";   
+                c = Engineer.color;
+            }
+            else if (Sheriff.sheriff != null && p == Sheriff.sheriff) {
+                r = "Sheriff";   
+                c = Sheriff.color;
+            }
+            else if (Lighter.lighter != null && p == Lighter.lighter) {
+                r = "Lighter";   
+                c = Lighter.color;
+            }
+            else if (Godfather.godfather != null && p == Godfather.godfather) {
+                r = "Godfather";   
+                c = Godfather.color;
+                g = false;
+            }
+            else if (Mafioso.mafioso != null && p == Mafioso.mafioso) {
+                r = "Mafioso";   
+                c = Mafioso.color;
+                g = false;
+            }
+            else if (Janitor.janitor != null && p == Janitor.janitor) {
+                r = "Janitor";
+                c = Janitor.color;
+                g = false;
+            }
+            else if (Morphling.morphling != null && p == Morphling.morphling) {
+                r = "Morphling";
+                c = Morphling.color;
+                g = false;
+            }
+            else if (Camouflager.camouflager != null && p == Camouflager.camouflager) {
+                r = "Camouflager";
+                c = Camouflager.color;
+                g = false;
+            }
+            else if (Detective.detective != null && p == Detective.detective) {
+                r = "Detective";
+                c = Detective.color;
+            }
+            else if (TimeMaster.timeMaster != null && p == TimeMaster.timeMaster) {
+                r = "Time Master";
+                c = TimeMaster.color;
+            }
+            else if (Medic.medic != null && p == Medic.medic) {
+                r = "Medic";
+                c = Medic.color;
+            }
+            else if (Shifter.shifter != null && p == Shifter.shifter) {
+                r = "Shifter";
+                c = Shifter.color;
+                g = false;
+            }
+            else if (Swapper.swapper != null && p == Swapper.swapper) {
+                r = "Swapper";
+                c = Swapper.color;
+            }
+            else if (Lovers.lover1 != null && p == Lovers.lover1) {
+                r = Lovers.lover1.Data.IsImpostor ? "ImpLover" : "Lover";
+                c = Lovers.lover1.Data.IsImpostor ? Palette.ImpostorRed : Lovers.color;
+                g = !Lovers.lover1.Data.IsImpostor;
+            }
+            else if (Lovers.lover2 != null && p == Lovers.lover2) {
+                r = Lovers.lover2.Data.IsImpostor ? "ImpLover" : "Lover";
+                c = Lovers.lover2.Data.IsImpostor ? Palette.ImpostorRed : Lovers.color;
+                g = !Lovers.lover2.Data.IsImpostor;
+            }
+            else if (Seer.seer != null && p == Seer.seer) { 
+                r = "Seer";
+                c = Seer.color;
+            }
+            else if (Spy.spy != null && p == Spy.spy) { 
+                r = "Spy";
+                c = Spy.color;
+            }
+            else if (Child.child != null && p == Child.child) { 
+                r = "Child";
+                c = Child.color;
+            }
+            else if (p.Data.IsImpostor) { // Just Impostor
+                r = "Impostor";
+                c = Palette.ImpostorRed;
+                g = false;
+            }
+            else if (!p.Data.IsImpostor) { // Just Crewmate
+                r = "Crewmate";
+                c = Color.white;
+            }
+
+            SeerInfo result = new SeerInfo();
+            result.roleName = r;
+            result.color = c;
+            result.isGood = g;
+
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/IntroPatch.cs b/Source Code/IntroPatch.cs
new file mode 100644 (file)
index 0000000..653cae0
--- /dev/null
@@ -0,0 +1,172 @@
+using HarmonyLib;
+using System;
+using static BonusRoles.BonusRoles;
+using UnityEngine;
+
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(IntroCutscene.CoBegin__d), nameof(IntroCutscene.CoBegin__d.MoveNext))]
+    class IntroPatch
+    {
+        // Intro special teams
+        static void Prefix(IntroCutscene.CoBegin__d __instance)
+        {
+            if (PlayerControl.LocalPlayer == Jester.jester)
+            {
+                var jesterTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
+                jesterTeam.Add(PlayerControl.LocalPlayer);
+                __instance.yourTeam = jesterTeam;
+            } else if (PlayerControl.LocalPlayer == Shifter.shifter) {
+                var shifterTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
+                shifterTeam.Add(PlayerControl.LocalPlayer);
+                __instance.yourTeam = shifterTeam;
+            }
+        }
+
+        // Intro display role
+        static void Postfix(IntroCutscene.CoBegin__d __instance)
+        {
+            if (PlayerControl.LocalPlayer == Jester.jester)
+            {
+                __instance.__this.Title.Text = "Jester";
+                __instance.__this.Title.Color = Jester.color;
+                __instance.__this.ImpostorText.Text = "Get voted off of the ship to win";
+                __instance.__this.BackgroundBar.material.color = Jester.color;
+            }
+            else if (PlayerControl.LocalPlayer == Mayor.mayor)
+            {
+                __instance.__this.Title.Text = "Mayor";
+                __instance.__this.Title.Color = Mayor.color;
+                __instance.__this.ImpostorText.Text = "Your vote counts twice";
+                __instance.__this.BackgroundBar.material.color = Mayor.color;   
+            }
+            else if (PlayerControl.LocalPlayer == Engineer.engineer)
+            {
+                __instance.__this.Title.Text = "Engineer";
+                __instance.__this.Title.Color = Engineer.color;
+                __instance.__this.ImpostorText.Text = "Maintain important systems on the ship";
+                __instance.__this.BackgroundBar.material.color = Engineer.color;
+            }
+            else if (PlayerControl.LocalPlayer == Godfather.godfather)
+            {
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.Title.Text = "Godfather";
+                __instance.__this.Title.Color = Godfather.color;
+                __instance.__this.ImpostorText.Text = "Kill all crewmates";
+                __instance.__this.BackgroundBar.material.color = Godfather.color;
+            }
+            else if (PlayerControl.LocalPlayer == Mafioso.mafioso)
+            {
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.Title.Text = "Mafioso";
+                __instance.__this.Title.Color = Mafioso.color;
+                __instance.__this.ImpostorText.Text = "Work with the [FF1919FF]Mafia[FFFFFFFF] to kill the crewmates";
+                __instance.__this.BackgroundBar.material.color = Mafioso.color;
+            }
+            else if (PlayerControl.LocalPlayer == Janitor.janitor)
+            {
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.Title.Text = "Janitor";
+                __instance.__this.Title.Color = Janitor.color;
+                __instance.__this.ImpostorText.Text = "Work with the [FF1919FF]Mafia[FFFFFFFF] by hiding dead bodies";
+                __instance.__this.BackgroundBar.material.color = Janitor.color;
+            }
+            else if (PlayerControl.LocalPlayer == Morphling.morphling)
+            {
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.Title.Text = "Morphling";
+                __instance.__this.Title.Color = Morphling.color;
+                __instance.__this.ImpostorText.Text = "Change your look to not get caught";
+                __instance.__this.BackgroundBar.material.color = Morphling.color;
+            }
+            else if (PlayerControl.LocalPlayer == Camouflager.camouflager)
+            {
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.Title.Text = "Camouflager";
+                __instance.__this.Title.Color = Camouflager.color;
+                __instance.__this.ImpostorText.Text = "Camouflage and kill the crewmates";
+                __instance.__this.BackgroundBar.material.color = Camouflager.color;
+            }
+            else if (PlayerControl.LocalPlayer == Sheriff.sheriff)
+            {
+                __instance.__this.Title.Text = "Sheriff";
+                __instance.__this.Title.Color = Sheriff.color;
+                __instance.__this.ImpostorText.Text = "Shoot the [FF1919FF]Impostors";
+                __instance.__this.BackgroundBar.material.color = Sheriff.color;
+            }
+            else if (PlayerControl.LocalPlayer == Lighter.lighter)
+            {
+                __instance.__this.Title.Text = "Lighter";
+                __instance.__this.Title.Color = Lighter.color;
+                __instance.__this.ImpostorText.Text = "Your light never goes out";
+                __instance.__this.BackgroundBar.material.color = Lighter.color;
+            }
+            else if (PlayerControl.LocalPlayer == Detective.detective)
+            {
+                __instance.__this.Title.Text = "Detective";
+                __instance.__this.Title.Color = Detective.color;
+                __instance.__this.ImpostorText.Text = "Find the [FF1919FF]Impostors[FFFFFFFF] by examining footprints";
+                __instance.__this.BackgroundBar.material.color = Detective.color;
+            }
+            else if (PlayerControl.LocalPlayer == TimeMaster.timeMaster)
+            {
+                __instance.__this.Title.Text = "Time Master";
+                __instance.__this.Title.Color = TimeMaster.color;
+                __instance.__this.ImpostorText.Text = "Rewind time to find the [FF1919FF]Impostors";
+                __instance.__this.BackgroundBar.material.color = TimeMaster.color;
+            }
+            else if (PlayerControl.LocalPlayer == Medic.medic)
+            {
+                __instance.__this.Title.Text = "Medic";
+                __instance.__this.Title.Color = Medic.color;
+                __instance.__this.ImpostorText.Text = "Create a shield to protect a person";
+                __instance.__this.BackgroundBar.material.color = Medic.color;
+            }
+            else if (PlayerControl.LocalPlayer == Shifter.shifter)
+            {
+                __instance.__this.Title.Text = "Shifter";
+                __instance.__this.Title.Color = Shifter.color;
+                __instance.__this.ImpostorText.Text = "Shift your role before the game ends";
+                __instance.__this.BackgroundBar.material.color = Shifter.color;
+            }
+            else if (PlayerControl.LocalPlayer == Swapper.swapper)
+            {
+                __instance.__this.Title.Text = "Swapper";
+                __instance.__this.Title.Color = Swapper.color;
+                __instance.__this.ImpostorText.Text = "Swap votes to exile the [FF1919FF]Impostors";
+                __instance.__this.BackgroundBar.material.color = Swapper.color;
+            }
+            else if (PlayerControl.LocalPlayer == Lovers.lover1 || PlayerControl.LocalPlayer == Lovers.lover2)
+            {
+                PlayerControl otherLover = PlayerControl.LocalPlayer == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
+                __instance.__this.Title.Text = PlayerControl.LocalPlayer.Data.IsImpostor ? "[FF1919FF]Imp[FC03BEFF]Lover" : "Lover";
+                __instance.__this.Title.Color = PlayerControl.LocalPlayer.Data.IsImpostor ? Color.white : Lovers.color;
+                __instance.__this.ImpostorText.Text = "You are in [FC03BEFF]Love [FFFFFFFF] with [FC03BEFF]" + (otherLover?.Data?.PlayerName ?? "");
+                __instance.__this.ImpostorText.gameObject.SetActive(true);
+                __instance.__this.BackgroundBar.material.color = Lovers.color;
+            }
+            else if (PlayerControl.LocalPlayer == Seer.seer)
+            {
+                __instance.__this.Title.Text = "Seer";
+                __instance.__this.Title.Color = Seer.color;
+                __instance.__this.ImpostorText.Text = "Reveal the intentions of everyone on the ship";
+                __instance.__this.BackgroundBar.material.color = Seer.color;
+            }
+            else if (PlayerControl.LocalPlayer == Spy.spy)
+            {
+                __instance.__this.Title.Text = "Spy";
+                __instance.__this.Title.Color = Spy.color;
+                __instance.__this.ImpostorText.Text = "Spy on everyone to find the [FF1919FF]Impostors";
+                __instance.__this.BackgroundBar.material.color = Spy.color;
+            }
+            else if (PlayerControl.LocalPlayer == Child.child)
+            {
+                __instance.__this.Title.Text = "Child";
+                __instance.__this.Title.Color = Child.color;
+                __instance.__this.ImpostorText.Text = "No one will harm you";
+                __instance.__this.BackgroundBar.material.color = Child.color;
+            }
+        }
+    }
+}
diff --git a/Source Code/Main.cs b/Source Code/Main.cs
new file mode 100644 (file)
index 0000000..01e1710
--- /dev/null
@@ -0,0 +1,133 @@
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using Hazel;
+using Reactor;
+using System.Collections.Generic;
+using Essentials.Options;
+using UnityEngine;
+using System.Linq;
+using System.Net;
+using System.IO;
+using System;
+using System.Reflection;
+using UnhollowerBaseLib;
+
+namespace BonusRoles
+{
+    [BepInPlugin(Id)]
+    [BepInProcess("Among Us.exe")]
+    [BepInDependency(ReactorPlugin.Id)]
+    public class BonusRolesPlugin : BasePlugin
+    {
+        public const string Id = "me.eisbison.bonusroles";
+        public Harmony Harmony { get; } = new Harmony(Id);
+
+        // Role spawn chances
+        public static CustomNumberOption mafiaSpawnChance = CustomOption.AddNumber("Mafia Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption morphlingSpawnChance = CustomOption.AddNumber("Morphling Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption camouflagerSpawnChance = CustomOption.AddNumber("Camouflager Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption loversSpawnChance = CustomOption.AddNumber("Lovers Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption jesterSpawnChance = CustomOption.AddNumber("Jester Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption mayorSpawnChance = CustomOption.AddNumber("Mayor Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption engineerSpawnChance = CustomOption.AddNumber("Engineer Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption sheriffSpawnChance = CustomOption.AddNumber("Sheriff Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption lighterSpawnChance = CustomOption.AddNumber("Lighter Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption detectiveSpawnChance = CustomOption.AddNumber("Detective Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption timeMasterSpawnChance = CustomOption.AddNumber("Time Master Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption medicSpawnChance = CustomOption.AddNumber("Medic Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption shifterSpawnChance = CustomOption.AddNumber("Shifter Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption swapperSpawnChance = CustomOption.AddNumber("Swapper Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption seerSpawnChance = CustomOption.AddNumber("Seer Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption spySpawnChance = CustomOption.AddNumber("Spy Spawn Chance", 100, 0, 100, 10);
+        public static CustomNumberOption childSpawnChance = CustomOption.AddNumber("Child Spawn Chance", 100, 0, 100, 10);
+
+        // Role settings
+        public static CustomNumberOption janitorCooldown = CustomOption.AddNumber("Janitor Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomNumberOption morphlingCooldown = CustomOption.AddNumber("Morphling Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomNumberOption camouflagerCooldown = CustomOption.AddNumber("Camouflager Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomToggleOption loversBothDie = CustomOption.AddToggle("Both Lovers Die", true); 
+        public static CustomNumberOption sheriffCooldown = CustomOption.AddNumber("Sheriff Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomToggleOption jesterCanDieToSheriff = CustomOption.AddToggle("Jester Can Die To Sheriff", false); 
+        public static CustomNumberOption lighterVision = CustomOption.AddNumber("Lighter Vision", 2f, 0.25f, 5f, 0.25f);
+        public static CustomToggleOption detectiveAnonymousFootprints = CustomOption.AddToggle("Anonymous Footprints", false); 
+        public static CustomNumberOption detectiveFootprintIntervall = CustomOption.AddNumber("Footprint Intervall", 0.5f, 0.25f, 10f, 0.25f);
+        public static CustomNumberOption detectiveFootprintDuration = CustomOption.AddNumber("Footprint Duration", 5f, 0.25f, 10f, 0.25f);
+        public static CustomNumberOption timeMasterCooldown = CustomOption.AddNumber("Time Master Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomNumberOption timeMasterRewindTime = CustomOption.AddNumber("Rewind Time", 3f, 1f, 10f, 1f);
+        public static CustomToggleOption timeMasterReviveDuringRewind = CustomOption.AddToggle("Revive During Rewind", false);
+        public static CustomNumberOption medicReportNameDuration = CustomOption.AddNumber("Time Where Medic Reports Will Have Name", 10, 0, 60, 2.5f);
+        public static CustomNumberOption medicReportColorDuration = CustomOption.AddNumber("Time Where Medic Reports Will Have Color Type", 20, 0, 120, 2.5f);
+        public static CustomStringOption medicShowShielded = CustomOption.AddString("Show Shielded Player", new string[] {"Everyone", "Shielded + Medic", "Medic"});
+        public static CustomToggleOption medicShowAttemptToShielded = CustomOption.AddToggle("Shielded Player Sees Murder Attempt", false);
+        public static CustomNumberOption shifterCooldown = CustomOption.AddNumber("Shifter Cooldown", 30f, 10f, 60f, 2.5f);
+        public static CustomNumberOption seerCooldown = CustomOption.AddNumber("Seer Cooldown (No Reset After Meeting)", 15f, 30f, 180f, 15f);
+        public static CustomStringOption seerKindOfInfo = CustomOption.AddString("Info That Seer Reveals", new string[] {"Role", "Good/Bad"});
+        public static CustomStringOption seerPlayersWithNotification = CustomOption.AddString("Players That See When They Are Being Revealed", new string[] {"Everyone", "The Good", "The Bad", "Nobody"}); 
+
+        public static ConfigEntry<string> Ip { get; set; }
+        public static ConfigEntry<ushort> Port { get; set; }
+        public static ConfigEntry<string> Name { get; private set; }
+
+        public override void Load()
+        {
+            Ip = Config.Bind("Custom", "Ipv4 or Hostname", "127.0.0.1");
+            Port = Config.Bind("Custom", "Port", (ushort)22023);
+            Name = Config.Bind("Custom", "Custom", "Custom");
+
+            var defaultRegions = ServerManager.DefaultRegions.ToList();
+            var ip = Ip.Value;
+            if (Uri.CheckHostName(Ip.Value).ToString() == "Dns")
+            {
+                try
+                {
+                    foreach (IPAddress address in Dns.GetHostAddresses(Ip.Value))
+                    {
+                        if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+                        {
+                            ip = address.ToString(); break;
+                        }
+                    }
+                }
+                catch { }
+            }
+
+            var port = Port.Value;
+            defaultRegions.Insert(0, new StaticRegionInfo(
+                Name.Value, (StringNames)3000, ip, new[]
+                {
+                    new ServerInfo($"CustomServer", ip, Port.Value)
+                }).Duplicate()
+            ) ;
+            ServerManager.DefaultRegions = defaultRegions.ToArray();
+
+            CustomOption.ShamelessPlug = false;
+            Harmony.PatchAll();
+        }
+    }
+
+    // Patch server name
+    [HarmonyPatch(typeof(LanguageUnit),"HOGGPANBLGP")]
+    public class ServerNamePatch {
+        public static bool Prefix(StringNames LENBLADGELB, string NNLLKLDPBAI, Il2CppReferenceArray<Il2CppSystem.Object> DKBJCINDDCD, ref string __result)
+        {
+            if (LENBLADGELB == (StringNames)3000)
+            {
+                __result = BonusRolesPlugin.Name.Value;
+                return false;
+            }         
+            return true;
+        }
+    }
+
+    // Deactivate bans, since I always leave my local testing game and ban myself
+    [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.AmBanned), MethodType.Getter)]
+    public static class AmBannedPatch
+    {
+        public static void Postfix(out bool __result)
+        {
+            __result = false;
+        }
+    }
+}
diff --git a/Source Code/MeetingPatch.cs b/Source Code/MeetingPatch.cs
new file mode 100644 (file)
index 0000000..f03e676
--- /dev/null
@@ -0,0 +1,360 @@
+using HarmonyLib;
+using Hazel;
+using System.Collections.Generic;
+using System.Linq;
+using UnhollowerBaseLib;
+using static BonusRoles.BonusRoles;
+using System.Collections;
+using System;
+using System.Text;
+using UnityEngine;
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(MeetingHud), "CalculateVotes")]
+    class MeetingCalculateVotesPatch {
+        static bool Prefix(MeetingHud __instance, ref Il2CppStructArray<byte> __result)
+        {
+            Il2CppStructArray<byte> array = new Il2CppStructArray<byte>(11);
+            for (int i = 0; i < __instance.playerStates.Length; i++)
+            {
+                PlayerVoteArea playerVoteArea = __instance.playerStates[i];
+                if (playerVoteArea.didVote)
+                {
+                    int num = (int)(playerVoteArea.votedFor + 1);
+                    if (num >= 0 && num < array.Length)
+                    {
+                        Il2CppStructArray<byte> array2 = array;
+                        int num2 = num;
+                        // Mayor count vote twice
+                        if (Mayor.mayor != null && playerVoteArea.TargetPlayerId == (sbyte)Mayor.mayor.PlayerId)
+                            array2[num2] += 2;
+                        else
+                            array2[num2] += 1;
+                    }
+                }
+            }
+
+            // Swapper swap votes
+            PlayerVoteArea swapped1 = null;
+            PlayerVoteArea swapped2 = null;
+
+            foreach (PlayerVoteArea playerVoteArea in __instance.playerStates) {
+                if (playerVoteArea.TargetPlayerId == Swapper.playerId1) swapped1 = playerVoteArea;
+                if (playerVoteArea.TargetPlayerId == Swapper.playerId2) swapped2 = playerVoteArea;
+            }
+
+            if (swapped1 != null && swapped2 != null && swapped1.TargetPlayerId + 1 >= 0 && swapped1.TargetPlayerId + 1 < array.Length && swapped2.TargetPlayerId + 1 >= 0 && swapped2.TargetPlayerId + 1 < array.Length) {
+                byte tmp = array[swapped1.TargetPlayerId + 1];
+                array[swapped1.TargetPlayerId + 1] = array[swapped2.TargetPlayerId + 1];
+                array[swapped2.TargetPlayerId + 1] = tmp;
+            }
+
+            __result = array;
+            return false;
+        }
+    }
+
+    [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.PopulateResults))]
+    class MeetingPopulateVotesPatch {
+        static bool Prefix(MeetingHud __instance, Il2CppStructArray<byte> GNKPMAGJLNC)
+        {
+            // Swapper swap votes
+            PlayerVoteArea swapped1 = null;
+            PlayerVoteArea swapped2 = null;
+
+            foreach (PlayerVoteArea playerVoteArea in __instance.playerStates) {
+                if (playerVoteArea.TargetPlayerId == Swapper.playerId1) swapped1 = playerVoteArea;
+                if (playerVoteArea.TargetPlayerId == Swapper.playerId2) swapped2 = playerVoteArea;
+            }
+
+            bool doSwap = swapped1 != null && swapped2 != null;
+            if (doSwap) {
+                Reactor.Coroutines.Start(Helpers.Slide2D(swapped1.transform, swapped1.transform.localPosition, swapped2.transform.localPosition, 2f));
+                Reactor.Coroutines.Start(Helpers.Slide2D(swapped2.transform, swapped2.transform.localPosition, swapped1.transform.localPosition, 2f));
+            }
+
+            // Mayor display vote twice
+            __instance.TitleText.Text = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.MeetingVotingResults, new Il2CppReferenceArray<Il2CppSystem.Object>(0));
+            int num = doSwap ? 4 : 0; // Delay animaton if swapping
+            for (int i = 0; i < __instance.playerStates.Length; i++)
+            {
+                PlayerVoteArea playerVoteArea = __instance.playerStates[i];
+                playerVoteArea.ClearForResults();
+                int num2 = doSwap ? 4 : 0; // Delay animaton if swapping
+                bool mayorFirstVoteDisplayed = false;
+
+                for (int j = 0; j < __instance.playerStates.Length; j++)
+                {
+                    PlayerVoteArea playerVoteArea2 = __instance.playerStates[j];
+                    byte self = GNKPMAGJLNC[(int)playerVoteArea2.TargetPlayerId];
+
+                    if (!((self & 128) > 0))
+                    {
+                        GameData.PlayerInfo playerById = GameData.Instance.GetPlayerById((byte)playerVoteArea2.TargetPlayerId);
+                        int votedFor = (int)PlayerVoteArea.GetVotedFor(self);
+                        if (votedFor == (int)playerVoteArea.TargetPlayerId)
+                        {
+                            SpriteRenderer spriteRenderer = UnityEngine.Object.Instantiate<SpriteRenderer>(__instance.PlayerVotePrefab);
+                            if (PlayerControl.GameOptions.AnonymousVotes)
+                            {
+                                PlayerControl.SetPlayerMaterialColors(Palette.DisabledGrey, spriteRenderer);
+                            }
+                            else
+                            {
+                                PlayerControl.SetPlayerMaterialColors((int)playerById.ColorId, spriteRenderer);
+                            }
+                            spriteRenderer.transform.SetParent(playerVoteArea.transform);
+                            spriteRenderer.transform.localPosition = __instance.CounterOrigin + new Vector3(__instance.CounterOffsets.x * (float)num2, 0f, 0f);
+                            spriteRenderer.transform.localScale = Vector3.zero;
+                            spriteRenderer.transform.SetParent(playerVoteArea.transform.parent); // Reparent votes so they don't move with their playerVoteArea
+                            __instance.StartCoroutine(Effects.Bloop((float)num2 * 0.5f, spriteRenderer.transform, 1f, 0.5f));
+                            num2++;
+                        }
+                        else if (i == 0 && votedFor == -1)
+                        {
+                            SpriteRenderer spriteRenderer2 = UnityEngine.Object.Instantiate<SpriteRenderer>(__instance.PlayerVotePrefab);
+                            if (PlayerControl.GameOptions.AnonymousVotes)
+                            {
+                                PlayerControl.SetPlayerMaterialColors(Palette.DisabledGrey, spriteRenderer2);
+                            }
+                            else
+                            {
+                                PlayerControl.SetPlayerMaterialColors((int)playerById.ColorId, spriteRenderer2);
+                            }
+                            spriteRenderer2.transform.SetParent(__instance.SkippedVoting.transform);
+                            spriteRenderer2.transform.localPosition = __instance.CounterOrigin + new Vector3(__instance.CounterOffsets.x * (float)num, 0f, 0f);
+                            spriteRenderer2.transform.localScale = Vector3.zero;
+                            spriteRenderer2.transform.SetParent(playerVoteArea.transform.parent); // Reparent votes so they don't move with their playerVoteArea
+                            __instance.StartCoroutine(Effects.Bloop((float)num * 0.5f, spriteRenderer2.transform, 1f, 0.5f));
+                            num++;
+                        }
+
+                        // Major vote, redo this iteration to place a second vote
+                        if (Mayor.mayor != null && playerVoteArea2.TargetPlayerId == (sbyte)Mayor.mayor.PlayerId && !mayorFirstVoteDisplayed) {
+                            mayorFirstVoteDisplayed = true;
+                            j--;    
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+
+
+    [HarmonyPatch]
+    class MeetingHudPopulateButtonsPatch {
+        static bool[] selections;
+        static SpriteRenderer[] renderers;
+        
+        [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.VotingComplete))]
+        class MeetingHudVotingCompletedPatch {
+            static void Postfix(MeetingHud __instance, byte[] GNKPMAGJLNC, GameData.PlayerInfo IHDMFDEEDEL, bool DCHFIBODGIL)
+            {
+                // Reset swapper values
+                Swapper.playerId1 = Byte.MaxValue;
+                Swapper.playerId2 = Byte.MaxValue;
+
+                // Lovers save next to be exiled, because RPC of ending game comes before RPC of exiled
+                Lovers.notAckedExiledIsLover = ((Lovers.lover1 != null && Lovers.lover1.PlayerId == IHDMFDEEDEL.PlayerId) || (Lovers.lover2 != null && Lovers.lover2.PlayerId == IHDMFDEEDEL.PlayerId));
+            }
+        }
+
+
+        static void onClick(int i, MeetingHud __instance)
+        {
+            if (__instance.state == MeetingHud.VoteStates.Results) return;
+            if (__instance.playerStates[i].isDead) return;
+
+            int selectedCount = selections.Where(b => b).Count();
+            SpriteRenderer renderer = renderers[i];
+
+            if (selectedCount == 0) {
+                renderer.color = Color.green;
+                selections[i] = true;
+            } else if (selectedCount == 1) {
+                if (selections[i]) {
+                    renderer.color = Color.red;
+                    selections[i] = false;
+                } else {
+                    selections[i] = true;
+                    renderer.color = Color.green;   
+                    
+                    PlayerVoteArea firstPlayer = null;
+                    PlayerVoteArea secondPlayer = null;
+                    for (int A = 0; A < selections.Length; A++) {
+                        if (selections[A]) {
+                            if (firstPlayer != null) {
+                                secondPlayer = __instance.playerStates[A];
+                                break;
+                            } else {
+                                firstPlayer = __instance.playerStates[A];
+                            }
+                        }
+                    }
+
+                    if (firstPlayer != null && secondPlayer != null) {
+                        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SwapperSwap, Hazel.SendOption.None, -1);
+                        writer.Write((byte)firstPlayer.TargetPlayerId);
+                        writer.Write((byte)secondPlayer.TargetPlayerId);
+                        AmongUsClient.Instance.FinishRpcImmediately(writer);
+
+                        RPCProcedure.swapperSwap((byte)firstPlayer.TargetPlayerId, (byte)secondPlayer.TargetPlayerId);
+                    }
+                }
+            }
+        }
+
+        static void addSwapperSwapButtons(MeetingHud __instance) {
+            if (Swapper.swapper == null || PlayerControl.LocalPlayer != Swapper.swapper || Swapper.swapper.Data.IsDead) return; 
+
+            selections = new bool[__instance.playerStates.Length];
+            renderers = new SpriteRenderer[__instance.playerStates.Length];
+
+            for (int i = 0; i < __instance.playerStates.Length; i++)
+                   {
+                PlayerVoteArea playerVoteArea = __instance.playerStates[i];
+                GameObject template = playerVoteArea.Buttons.transform.Find("CancelButton").gameObject;
+                GameObject checkbox = UnityEngine.Object.Instantiate(template);
+                checkbox.transform.SetParent(playerVoteArea.transform);
+                checkbox.transform.position = template.transform.position;
+                checkbox.transform.localPosition = new Vector3(0f, 0.03f, template.transform.localPosition.z);
+                SpriteRenderer renderer = checkbox.GetComponent<SpriteRenderer>();
+                renderer.sprite = Swapper.getCheckSprite();
+                renderer.color = Color.red;
+
+                PassiveButton button = checkbox.GetComponent<PassiveButton>();
+                button.OnClick.RemoveAllListeners();
+                int copiedIndex = i;
+                button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => onClick(copiedIndex, __instance)));
+                
+
+
+                selections[i] = false;
+                renderers[i] = renderer;
+            }
+        }
+
+        [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.ServerStart))]
+        class MeetingServerStartPatch {
+            static void Postfix(MeetingHud __instance)
+            {
+                // Add swapper buttons
+                addSwapperSwapButtons(__instance);
+            }
+        }
+
+        [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Deserialize))]
+        class MeetingDeserializePatch {
+            static void Postfix(MeetingHud __instance, MessageReader HFPCBBHJIPJ, bool CHDIOBMNLGH)
+            {
+                // Add swapper buttons
+                if (CHDIOBMNLGH) {
+                    addSwapperSwapButtons(__instance);
+                }
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(UnityEngine.Object), nameof(UnityEngine.Object.Destroy), new Type[] { typeof(UnityEngine.Object) })]
+    class MeetingExiledEndPatch
+    {
+        static void Prefix(UnityEngine.Object obj)
+        {
+            if (ExileController.Instance != null && obj == ExileController.Instance.gameObject)
+            {
+                // Reset custom button timers where necessary
+                CustomButton.MeetingEndedUpdate();
+
+                // Jester win condition
+                if (Jester.jester != null && ExileController.Instance.exiled != null && ExileController.Instance.exiled.PlayerId == Jester.jester.PlayerId)
+                {
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.JesterWin, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+
+                    RPCProcedure.jesterWin();
+                }
+
+                // Child win condition
+                if (Child.child != null && ExileController.Instance.exiled != null && ExileController.Instance.exiled.PlayerId == Child.child.PlayerId) {
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ChildDied, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.childDied();
+                }
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), new Type[] { typeof(StringNames), typeof(Il2CppReferenceArray<Il2CppSystem.Object>) })]
+    class MeetingExiledTextPatch
+    {
+        static void Postfix(ref string __result, StringNames DKEHCKOHMOH, Il2CppReferenceArray<Il2CppSystem.Object> DKBJCINDDCD)
+        {
+            if (ExileController.Instance != null && ExileController.Instance.exiled != null)
+            {
+                // Exile role text for roles that are being assigned to crewmates
+                if (DKEHCKOHMOH == StringNames.ExileTextPN || DKEHCKOHMOH == StringNames.ExileTextSN)
+                {
+                    if( Jester.jester != null && ExileController.Instance.exiled.Object.PlayerId == Jester.jester.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Jester.";
+                    else if(Mayor.mayor != null && ExileController.Instance.exiled.Object.PlayerId == Mayor.mayor.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Mayor.";
+                    else if(Engineer.engineer != null && ExileController.Instance.exiled.Object.PlayerId == Engineer.engineer.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Engineer.";
+                    else if(Sheriff.sheriff != null && ExileController.Instance.exiled.Object.PlayerId == Sheriff.sheriff.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Sheriff.";
+                    else if(Lighter.lighter != null && ExileController.Instance.exiled.Object.PlayerId == Lighter.lighter.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Lighter.";
+                    else if(Detective.detective != null && ExileController.Instance.exiled.Object.PlayerId == Detective.detective.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Detective.";
+                    else if(TimeMaster.timeMaster != null && ExileController.Instance.exiled.Object.PlayerId == TimeMaster.timeMaster.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Time Master.";
+                    else if(Medic.medic != null && ExileController.Instance.exiled.Object.PlayerId == Medic.medic.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Medic.";
+                    else if(Shifter.shifter != null && ExileController.Instance.exiled.Object.PlayerId == Shifter.shifter.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Shifter.";
+                    else if(Swapper.swapper != null && ExileController.Instance.exiled.Object.PlayerId == Swapper.swapper.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Swapper.";
+                    else if(Lovers.lover1 != null && ExileController.Instance.exiled.Object.PlayerId == Lovers.lover1.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Lover.";
+                    else if(Lovers.lover2 != null && ExileController.Instance.exiled.Object.PlayerId == Lovers.lover2.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Lover.";
+                    else if(Seer.seer != null && ExileController.Instance.exiled.Object.PlayerId == Seer.seer.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Seer.";
+                    else if(Spy.spy != null && ExileController.Instance.exiled.Object.PlayerId == Spy.spy.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Spy.";
+                    else if(Child.child != null && ExileController.Instance.exiled.Object.PlayerId == Child.child.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Child.";
+                    else
+                        __result = ExileController.Instance.exiled.PlayerName + " was not The Impostor.";
+                }
+                // Exile role text for roles that are being assigned to impostors
+                if (DKEHCKOHMOH == StringNames.ExileTextPP || DKEHCKOHMOH == StringNames.ExileTextSP) {
+                    if(Godfather.godfather != null && ExileController.Instance.exiled.Object.PlayerId == Godfather.godfather.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Godfather.";
+                    else if(Mafioso.mafioso != null && ExileController.Instance.exiled.Object.PlayerId == Mafioso.mafioso.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Mafioso.";
+                    else if(Janitor.janitor != null && ExileController.Instance.exiled.Object.PlayerId == Janitor.janitor.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Janitor.";
+                    else if(Morphling.morphling != null && ExileController.Instance.exiled.Object.PlayerId == Morphling.morphling.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Morphling.";
+                    else if(Camouflager.camouflager != null && ExileController.Instance.exiled.Object.PlayerId == Camouflager.camouflager.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Camouflager.";
+                    else if(Lovers.lover1 != null && ExileController.Instance.exiled.Object.PlayerId == Lovers.lover1.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The ImpLover.";
+                    else if(Lovers.lover2 != null && ExileController.Instance.exiled.Object.PlayerId == Lovers.lover2.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The ImpLover.";
+                }
+
+                // Hide number of remaining impostors on Jester win
+                if (DKEHCKOHMOH == StringNames.ImpostorsRemainP || DKEHCKOHMOH == StringNames.ImpostorsRemainS)
+                {
+                    if (Jester.jester != null && ExileController.Instance.exiled.Object.PlayerId == Jester.jester.PlayerId)
+                        __result = "";
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/PlayerControlPatch.cs b/Source Code/PlayerControlPatch.cs
new file mode 100644 (file)
index 0000000..5382fcf
--- /dev/null
@@ -0,0 +1,289 @@
+using HarmonyLib;
+using Hazel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static BonusRoles.BonusRoles;
+using static BonusRoles.GameHistory;
+using UnityEngine;
+
+namespace BonusRoles {
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
+    public static class PlayerControlFixedUpdatePatch
+    {
+        public static void bendTimeUpdate() {
+            if (TimeMaster.isRewinding) {
+                if (localPlayerPositions.Count > 0) {
+                    // Set position
+                    var next = localPlayerPositions[0];
+                    PlayerControl.LocalPlayer.transform.position = next.Item1;
+                    localPlayerPositions.RemoveAt(0);
+                    if (localPlayerPositions.Count > 0) localPlayerPositions.RemoveAt(0); // Skip every second position to rewinde in half the time
+                
+                    // Try reviving LOCAL player 
+                    if (TimeMaster.reviveDuringRewind && PlayerControl.LocalPlayer.Data.IsDead) {
+                        DeadPlayer deadPlayer = deadPlayers.Where(x => x.player == PlayerControl.LocalPlayer).FirstOrDefault();
+                        if (deadPlayer != null && next.Item2 < deadPlayer.timeOfDeath) {
+                            MessageWriter write = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte) CustomRPC.TimeMasterRevive, SendOption.None, -1);
+                            write.Write(PlayerControl.LocalPlayer.PlayerId);
+                            AmongUsClient.Instance.FinishRpcImmediately(write);
+                            RPCProcedure.timeMasterRevive(PlayerControl.LocalPlayer.PlayerId);
+                        }
+                    }
+                } else {
+                    TimeMaster.isRewinding = false;
+                    PlayerControl.LocalPlayer.moveable = true;
+                    HudManager.Instance.FullScreen.enabled = false;
+                }
+            } else {
+                while (localPlayerPositions.Count >= Mathf.Round(TimeMaster.rewindTime / Time.fixedDeltaTime)) localPlayerPositions.RemoveAt(localPlayerPositions.Count - 1);
+                localPlayerPositions.Insert(0, new Tuple<Vector3, DateTime>(PlayerControl.LocalPlayer.transform.position, DateTime.UtcNow));
+            }
+        }
+
+        static PlayerControl setAnyRoleTarget() {
+            PlayerControl result = null;
+            float num = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)];
+            if (!ShipStatus.Instance) return result;
+
+            Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+            Il2CppSystem.Collections.Generic.List<GameData.PlayerInfo> allPlayers = GameData.Instance.AllPlayers;
+            for (int i = 0; i < allPlayers.Count; i++)
+            {
+                GameData.PlayerInfo playerInfo = allPlayers[i];
+                if (!playerInfo.Disconnected && playerInfo.PlayerId != PlayerControl.LocalPlayer.PlayerId && !playerInfo.IsDead)
+                {
+                    PlayerControl @object = playerInfo.Object;
+                    if (@object && !@object.inVent)
+                    {
+                        Vector2 vector = @object.GetTruePosition() - truePosition;
+                        float magnitude = vector.magnitude;
+                        if (magnitude <= num && !PhysicsHelpers.AnyNonTriggersBetween(truePosition, vector.normalized, magnitude, Constants.ShipAndObjectsMask))
+                        {
+                            result = @object;
+                            num = magnitude;
+                        }
+                    }
+                }
+            }
+            return result;
+        }
+
+        static void medicSetTarget() {
+            if (Medic.medic == null || Medic.medic != PlayerControl.LocalPlayer) return;
+            Medic.currentTarget = setAnyRoleTarget();
+        }
+
+        static void shifterSetTarget() {
+            if (Shifter.shifter == null || Shifter.shifter != PlayerControl.LocalPlayer) return;
+            Shifter.currentTarget = setAnyRoleTarget();
+        }
+
+        
+        static void morphlingSetTarget() {
+            if (Morphling.morphling == null || Morphling.morphling != PlayerControl.LocalPlayer) return;
+            Morphling.currentTarget = setAnyRoleTarget();
+        }
+        
+        static void sheriffSetTarget() {
+            if (Sheriff.sheriff == null || Sheriff.sheriff != PlayerControl.LocalPlayer) return;
+            Sheriff.currentTarget = setAnyRoleTarget();
+        }
+
+        static void seerSetTarget() {
+            if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer) return;
+            Seer.currentTarget = setAnyRoleTarget();
+            if (Seer.currentTarget != null && Seer.revealedPlayers.Any(p => p.Data.PlayerId == Seer.currentTarget.Data.PlayerId)) Seer.currentTarget = null; // Remove target if already revealed
+
+        }
+
+        static void detectiveSetFootPrints() {
+            if (Detective.detective == null || Detective.detective != PlayerControl.LocalPlayer) return;
+
+            Detective.timer -= Time.fixedDeltaTime;
+            if (Detective.timer <= 0f) {
+                Detective.timer = Detective.footprintIntervall;
+                foreach (PlayerControl player in PlayerControl.AllPlayerControls) {
+                    if (player != null && player != PlayerControl.LocalPlayer && !player.Data.IsDead) {
+                        new Footprint(Detective.footprintDuration, Detective.anonymousFootprints, player);
+                    }
+                }
+            }
+
+        }
+
+        public static void Postfix(PlayerControl __instance) {
+            if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
+            
+            if (PlayerControl.LocalPlayer == __instance) {
+                // Time Master
+                bendTimeUpdate();
+                // Morphling
+                morphlingSetTarget();
+                // Medic
+                medicSetTarget();
+                // Shifter
+                shifterSetTarget();
+                // Sheriff
+                sheriffSetTarget();
+                // Seer
+                seerSetTarget();
+                // Detective
+                detectiveSetFootPrints();
+            } 
+        }
+    }
+
+    [HarmonyPatch(typeof(KillButtonManager), nameof(KillButtonManager.PerformKill))]
+    class PerformKillPatch
+    {
+        public static bool Prefix(KillButtonManager __instance) {
+            // Block imposter shielded kill
+                   if (__instance.isActiveAndEnabled && __instance.CurrentTarget && !__instance.isCoolingDown && !PlayerControl.LocalPlayer.Data.IsDead && PlayerControl.LocalPlayer.CanMove) {
+                if (Medic.shielded != null && Medic.shielded == __instance.CurrentTarget) {
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.None, -1);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.shieldedMurderAttempt();
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.LocalPlayer.CmdReportDeadBody))]
+    class BodyReportPatch
+    {
+        static void Postfix(PlayerControl __instance, GameData.PlayerInfo PAIBDFDMIGK)
+        {
+            // Medic report
+            if (Medic.medic != null && Medic.medic == PlayerControl.LocalPlayer && __instance.PlayerId == Medic.medic.PlayerId)
+            {
+                DeadPlayer deadPlayer = deadPlayers?.Where(x => x.player?.PlayerId == PAIBDFDMIGK?.PlayerId)?.FirstOrDefault();
+
+                if (deadPlayer != null && deadPlayer.killerIfExisting != null) {
+                    float timeSinceDeath = ((float)(DateTime.UtcNow - deadPlayer.timeOfDeath).TotalMilliseconds);
+                    string msg = "";
+
+                    if (timeSinceDeath < Medic.reportNameDuration * 1000) {
+                        msg =  $"Body Report: The killer appears to be {deadPlayer.killerIfExisting.name}! (Killed {Math.Round(timeSinceDeath / 1000)}s ago)";
+                    } else if (timeSinceDeath < Medic.reportColorDuration * 1000) {
+                        var colors = new Dictionary<byte, string>() {
+                            {0, "darker"},
+                            {1, "darker"},
+                            {2, "darker"},
+                            {3, "lighter"},
+                            {4, "lighter"},
+                            {5, "lighter"},
+                            {6, "darker"},
+                            {7, "lighter"},
+                            {8, "darker"},
+                            {9, "darker"},
+                            {10, "lighter"},
+                            {11, "lighter"},
+                        };
+                        var typeOfColor = colors[deadPlayer.killerIfExisting.Data.ColorId] ?? "unknown";
+                        msg =  $"Body Report: The killer appears to be a {typeOfColor} color. (Killed {Math.Round(timeSinceDeath / 1000)}s ago)";
+                    } else {
+                        msg = $"Body Report: The corpse is too old to gain information from. (Killed {Math.Round(timeSinceDeath / 1000)}s ago)";
+                    }
+
+                    if (!string.IsNullOrWhiteSpace(msg))
+                    {   
+                        if (AmongUsClient.Instance.AmClient && DestroyableSingleton<HudManager>.Instance)
+                        {
+                            DestroyableSingleton<HudManager>.Instance.Chat.AddChat(PlayerControl.LocalPlayer, msg);
+                        }
+                        if (msg.IndexOf("who", StringComparison.OrdinalIgnoreCase) >= 0)
+                        {
+                            DestroyableSingleton<Assets.CoreScripts.Telemetry>.Instance.SendWho();
+                        }
+                    }
+                }
+            }  
+        }
+    }
+
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))]
+    public static class MurderPlayerPatch
+    {
+        public static void Prefix(PlayerControl __instance, PlayerControl PAIBDFDMIGK, out bool __state)
+        {
+            // __state denotes if the player performing the murder needs to be reset to crewmate
+            __state = false;
+
+            // Allow Sheriff to kill "as crewmate" by assigning the impostor role
+            if (Sheriff.sheriff != null && __instance == Sheriff.sheriff) {
+                __instance.Data.IsImpostor = true;
+                __state = true;
+            }
+            // Allow Shifter to suicide
+            else if (Shifter.shifter != null && __instance == Shifter.shifter) {
+                __instance.Data.IsImpostor = true;
+                __state = true;
+            }
+            // Allow Lover which is no impostor to suicide
+            else if ((Lovers.lover1 != null && __instance == Lovers.lover1) || (Lovers.lover2 != null && __instance == Lovers.lover2)) {
+                if (!__instance.Data.IsImpostor) {
+                    __instance.Data.IsImpostor = true;
+                    __state = true;
+                }
+            }
+        }
+
+        public static void Postfix(PlayerControl __instance, PlayerControl PAIBDFDMIGK, bool __state)
+        {
+            // Collect dead player info
+            DeadPlayer deadPlayer = new DeadPlayer(PAIBDFDMIGK, DateTime.UtcNow, DeathReason.Kill, __instance);
+            GameHistory.deadPlayers.Add(deadPlayer);
+
+            // Reset killer to crewmate if __state
+            if (__state) __instance.Data.IsImpostor = false;
+
+            // Lover suicide trigger on murder
+            if ((Lovers.lover1 != null && PAIBDFDMIGK == Lovers.lover1) || (Lovers.lover2 != null && PAIBDFDMIGK == Lovers.lover2)) {
+                PlayerControl otherLover = PAIBDFDMIGK == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
+                if (PlayerControl.LocalPlayer == PAIBDFDMIGK && otherLover != null && !otherLover.Data.IsDead && Lovers.bothDie) { // Only the dead lover sends the rpc
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LoverSuicide, Hazel.SendOption.None, -1);
+                    writer.Write(otherLover.PlayerId);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.loverSuicide(otherLover.PlayerId);
+                }
+            }
+
+            // Child died
+            if (Child.child != null && PAIBDFDMIGK == Child.child) {
+                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ChildDied, Hazel.SendOption.None, -1);
+                AmongUsClient.Instance.FinishRpcImmediately(writer);
+                RPCProcedure.childDied();
+            }
+        }
+    }
+
+
+
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Exiled))]
+    public static class ExilePlayerPatch
+    {
+        public static void Postfix(PlayerControl __instance)
+        {
+            // Collect dead player info
+            DeadPlayer deadPlayer = new DeadPlayer(__instance, DateTime.UtcNow, DeathReason.Exile, null);
+            GameHistory.deadPlayers.Add(deadPlayer);
+
+            // Lover suicide trigger on exile
+            if ((Lovers.lover1 != null && __instance == Lovers.lover1) || (Lovers.lover2 != null && __instance == Lovers.lover2)) {
+                PlayerControl otherLover = __instance == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
+                if (PlayerControl.LocalPlayer == __instance && otherLover != null && !otherLover.Data.IsDead && Lovers.bothDie) { // Only the dead lover sends the rpc
+                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LoverSuicide, Hazel.SendOption.None, -1);
+                    writer.Write(otherLover.PlayerId);
+                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                    RPCProcedure.loverSuicide(otherLover.PlayerId);
+                }
+            }
+
+        }
+    }
+}
diff --git a/Source Code/RPC.cs b/Source Code/RPC.cs
new file mode 100644 (file)
index 0000000..ae51dad
--- /dev/null
@@ -0,0 +1,462 @@
+using HarmonyLib;
+using Hazel;
+using static BonusRoles.BonusRoles;
+using static BonusRoles.HudManagerStartPatch;
+using static BonusRoles.GameHistory;
+using System.Collections.Generic;
+using System.Collections;
+using System.Linq;
+using UnityEngine;
+
+namespace BonusRoles
+{
+    enum RoleId {
+        Jester = 0,
+        Mayor = 1,
+        Engineer = 2,
+        Sheriff = 3,
+        Lighter = 4,
+        Godfather = 5,
+        Mafioso = 6,
+        Janitor = 7,
+        Detective = 8,
+        TimeMaster = 9,
+        Medic = 10,
+        Shifter = 11,
+        Swapper = 12,
+        Lover1 = 13,
+        Lover2 = 14,
+        Seer = 15,
+        Morphling = 16,
+        Camouflager = 17,
+        Spy = 18,
+        Child = 19,
+    }
+
+    enum CustomRPC
+    {
+        // Main Controls
+
+        ResetVaribles = 50,
+        ForceEnd = 51,
+        SetRole = 52,
+
+        // Role functionality
+
+        JesterWin = 80,
+        EngineerFixLights = 81,
+        EngineerUsedRepair = 82,
+        JanitorClean = 83,
+        SheriffKill = 84,
+        TimeMasterRewindTime = 85,
+        MedicSetShielded = 86,
+        ShieldedMurderAttempt = 87,
+        TimeMasterRevive = 88,
+        ShifterShift = 89,
+        SwapperSwap = 90,
+        SeerReveal = 91,
+        MorphlingMorph = 92,
+        CamouflagerCamouflage = 93,
+        ChildDied = 94,
+        LoverSuicide = 95,
+    }
+
+    public static class RPCProcedure {
+
+        // Main Controls
+
+        public static void resetVariables() {
+            clearAndReloadRoles();
+            clearGameHistory();
+            setCustomButtonCooldowns();
+        }
+
+        public static void forceEnd() {
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+            {
+                if (!player.Data.IsImpostor)
+                {
+                    player.RemoveInfected();
+                    player.Die(DeathReason.Exile);
+                    player.Data.IsDead = true;
+                }
+            }
+        }
+
+        public static void setRole(byte roleId, byte playerId) {
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                if (player.PlayerId == playerId) {
+                    switch((RoleId)roleId) {
+                    case RoleId.Jester:
+                        Jester.jester = player;
+                        break;
+                    case RoleId.Mayor:
+                        Mayor.mayor = player;
+                        break;
+                    case RoleId.Engineer:
+                        Engineer.engineer = player;
+                        break;
+                    case RoleId.Sheriff:
+                        Sheriff.sheriff = player;
+                        break;
+                    case RoleId.Lighter:
+                        Lighter.lighter = player;
+                        break;
+                    case RoleId.Godfather:
+                        Godfather.godfather = player;
+                        break;
+                    case RoleId.Mafioso:
+                        Mafioso.mafioso = player;
+                        break;
+                    case RoleId.Janitor:
+                        Janitor.janitor = player;
+                        break;
+                    case RoleId.Detective:
+                        Detective.detective = player;
+                        break;
+                    case RoleId.TimeMaster:
+                        TimeMaster.timeMaster = player;
+                        break;
+                    case RoleId.Medic:
+                        Medic.medic = player;
+                        break;
+                    case RoleId.Shifter:
+                        Shifter.shifter = player;
+                        break;
+                    case RoleId.Swapper:
+                        Swapper.swapper = player;
+                        break;
+                    case RoleId.Lover1:
+                        Lovers.lover1 = player;
+                        break;
+                    case RoleId.Lover2:
+                        Lovers.lover2 = player;
+                        break;
+                    case RoleId.Seer:
+                        Seer.seer = player;
+                        break;
+                    case RoleId.Morphling:
+                        Morphling.morphling = player;
+                        break;
+                    case RoleId.Camouflager:
+                        Camouflager.camouflager = player;
+                        break;
+                    case RoleId.Spy:
+                        Spy.spy = player;
+                        break;
+                    case RoleId.Child:
+                        Child.child = player;
+                        break;
+                    }
+                }
+        }
+
+        // Role functionality
+
+        public static void jesterWin() {
+            Jester.jester.Revive();
+            Jester.jester.Data.IsDead = false;
+            Jester.jester.Data.IsImpostor = true;
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+            {
+                if (player != Jester.jester)
+                {
+                    player.RemoveInfected();
+                    player.Die(DeathReason.Exile);
+                    player.Data.IsDead = true;
+                    player.Data.IsImpostor = false;
+                }
+            }
+        }
+
+        public static void engineerFixLights() {
+            SwitchSystem switchSystem = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast<SwitchSystem>();
+            switchSystem.ActualSwitches = switchSystem.ExpectedSwitches;
+        }
+
+        public static void engineerUsedRepair() {
+            Engineer.usedRepair = true;
+        }
+
+        public static void janitorClean(byte playerId) {
+            DeadBody[] array = Object.FindObjectsOfType<DeadBody>();
+            for (int i = 0; i < array.Length; i++) {
+                if (GameData.Instance.GetPlayerById(array[i].ParentId).PlayerId == playerId)
+                    Object.Destroy(array[i].gameObject);
+            }
+        }
+
+        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.isRewinding = true;
+            if (MapBehaviour.Instance)
+                MapBehaviour.Instance.Close();
+            if (Minigame.Instance)
+                Minigame.Instance.ForceClose();
+            PlayerControl.LocalPlayer.moveable = false;
+            HudManager.Instance.FullScreen.color = new Color(0f, 0.5f, 0.8f, 0.3f);
+            HudManager.Instance.FullScreen.enabled = true;
+        }
+
+        public static void medicSetShielded(byte shieldedId) {
+            Medic.usedShield = true;
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                if (player.PlayerId == shieldedId)
+                    Medic.shielded = player;
+        }
+
+        public static void shieldedMurderAttempt() {
+            if (Medic.shielded != null && Medic.shielded == PlayerControl.LocalPlayer && Medic.showAttemptToShielded && HudManager.Instance?.FullScreen != null) {
+                Color c = Palette.ImpostorRed;
+                HudManager.Instance.FullScreen.enabled = true;
+                Reactor.Coroutines.Start(Helpers.CoFlashAndDisable(
+                    HudManager.Instance.FullScreen,
+                    0.5f,
+                    new Color(c.r, c.g, c.b, 0f),
+                    new Color(c.r, c.g, c.b, 0.75f)
+                ));
+            }
+        }
+
+        public static void timeMasterRevive(byte playerId) {
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                if (player.PlayerId == playerId) {
+                    player.Revive();
+                    var body = UnityEngine.Object.FindObjectsOfType<DeadBody>().FirstOrDefault(b => b.ParentId == playerId);
+                    DeadPlayer deadPlayerEntry = deadPlayers.Where(x => x.player.PlayerId == playerId).FirstOrDefault();
+                    if (body != null) UnityEngine.Object.Destroy(body.gameObject);
+                    if (deadPlayerEntry != null) deadPlayers.Remove(deadPlayerEntry);
+                }
+        }
+
+        public static void shifterShift(byte targetId) {
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+            {
+                if (player.PlayerId == targetId && Shifter.shifter != null)
+                {
+                    // Suicide when impostor or impostor variants
+                    if (player.Data.IsImpostor) {
+                        Shifter.shifter.MurderPlayer(Shifter.shifter);
+                        return;
+                    }
+
+                    PlayerControl oldShifter = Shifter.shifter;
+                    // Switch tasks
+                    var shifterSabotageTasks = oldShifter.myTasks;
+                    oldShifter.myTasks = player.myTasks;
+                    player.myTasks = shifterSabotageTasks;
+
+                    // Switch shield
+                    if (Medic.shielded != null && Medic.shielded == player) {
+                        Medic.shielded.myRend.material.SetFloat("_Outline", 0f);
+                        Medic.shielded = oldShifter;
+                    } else if (Medic.shielded != null && Medic.shielded == oldShifter) {
+                        Medic.shielded.myRend.material.SetFloat("_Outline", 0f);
+                        Medic.shielded = player;
+                    }
+
+                    // Shift role
+                    if (Jester.jester != null && Jester.jester == player) {
+                        Jester.jester = oldShifter;
+                    } else if (Mayor.mayor != null && Mayor.mayor == player) {
+                        Mayor.mayor = oldShifter;
+                    } else if (Engineer.engineer != null && Engineer.engineer == player) {
+                        Engineer.engineer = oldShifter;
+                    } else if (Sheriff.sheriff != null && Sheriff.sheriff == player) {
+                        Sheriff.sheriff = oldShifter;
+                    } else if (Lighter.lighter != null && Lighter.lighter == player) {
+                        Lighter.lighter = oldShifter;
+                    } else if (Detective.detective != null && Detective.detective == player) {
+                        Detective.detective = oldShifter;
+                    } else if (TimeMaster.timeMaster != null && TimeMaster.timeMaster == player) {
+                        TimeMaster.timeMaster = oldShifter;
+                    } else if (Medic.medic != null && Medic.medic == player) {
+                        Medic.medic = oldShifter;
+                    } else if (Swapper.swapper != null && Swapper.swapper == player) {
+                        Swapper.swapper = oldShifter;
+                    } else if (Lovers.lover1 != null && Lovers.lover1 == player) {
+                        Lovers.lover1 = oldShifter;
+                    } else if (Lovers.lover2 != null && Lovers.lover2 == player) {
+                        Lovers.lover2 = oldShifter;
+                    } else if (Seer.seer != null && Seer.seer == player) {
+                        Seer.seer = oldShifter;
+                    } else if (Spy.spy != null && Spy.spy == player) {
+                        Spy.spy = oldShifter;
+                    } else if (Child.child != null && Child.child == player) {
+                        Child.child = oldShifter;
+                    } else { // Crewmate
+                    }
+                    
+                    Shifter.shifter = player;
+
+                    // Set cooldowns to max for both players
+                    if (PlayerControl.LocalPlayer == Shifter.shifter || PlayerControl.LocalPlayer == oldShifter)
+                        CustomButton.ResetAllCooldowns();
+                }
+            }
+        }
+
+        public static void swapperSwap(byte playerId1, byte playerId2) {
+            if (MeetingHud.Instance) {
+                Swapper.playerId1 = playerId1;
+                Swapper.playerId2 = playerId2;
+            }
+        }
+
+        public static void seerReveal(byte playerId) {
+            if (Seer.seer == null) return;
+
+            PlayerControl player = Helpers.playerById(playerId);
+            if (player != null && !Seer.revealedPlayers.Any(p => p.Data.PlayerId == playerId)) {
+                Seer.revealedPlayers.Add(player);
+
+                if (PlayerControl.LocalPlayer == player && HudManager.Instance?.FullScreen != null) {
+                    SeerInfo si = SeerInfo.getSeerInfoForPlayer(player);
+                    bool showNotification = false;
+                    if (Seer.playersWithNotification == 0 ) showNotification = true;
+                    else if (Seer.playersWithNotification == 1 && si.isGood) showNotification = true;
+                    else if (Seer.playersWithNotification == 2 && !si.isGood) showNotification = true;
+                    else if (Seer.playersWithNotification == 3) showNotification = false;
+
+                    if (showNotification) {
+                        HudManager.Instance.FullScreen.enabled = true;
+                        Reactor.Coroutines.Start(Helpers.CoFlashAndDisable(
+                            HudManager.Instance.FullScreen,
+                            0.5f,
+                            new Color(42f / 255f, 187f / 255f, 245f / 255f, 0f),
+                            new Color(42f / 255f, 187f / 255f, 245f / 255f, 0.75f)
+                        ));
+                    }
+                }
+            }
+        }
+
+        public static void morphlingMorph(byte playerId) {  
+            PlayerControl target = Helpers.playerById(playerId);
+            if (Morphling.morphling == null || target == null) return;
+
+            Morphling.morphTimer = 10f;
+            Morphling.morphTarget = target;
+        }
+
+        public static void camouflagerCamouflage() {
+            if (Camouflager.camouflager == null) return;
+
+            Camouflager.camouflageTimer = 10f;
+        }
+
+        public static void childDied() {
+            Child.child.Revive();
+            Child.child.Data.IsDead = false;
+            Child.child.Data.IsImpostor = true;
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+            {
+                if (player != null && player != Child.child)
+                {
+                    player.RemoveInfected();
+                    player.Die(DeathReason.Exile);
+                    player.Data.IsDead = true;
+                    player.Data.IsImpostor = false;
+                }
+            }
+        }
+
+        public static void loverSuicide(byte remainingLoverId) {
+            if (Lovers.lover1 != null && !Lovers.lover1.Data.IsDead && Lovers.lover1.PlayerId == remainingLoverId) {
+                Lovers.lover1.MurderPlayer(Lovers.lover1);
+            } else if (Lovers.lover2 != null && !Lovers.lover2.Data.IsDead && Lovers.lover2.PlayerId == remainingLoverId) {
+                Lovers.lover2.MurderPlayer(Lovers.lover2);
+            }
+        }     
+    }
+
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))]
+    class RPCHandlerPatch
+    {
+        static void Postfix(byte ACCJCEHMKLN, MessageReader HFPCBBHJIPJ)
+        {
+            byte packetId = ACCJCEHMKLN;
+            MessageReader reader = HFPCBBHJIPJ;
+            switch (packetId) {
+
+                // Main Controls
+
+                case (byte)CustomRPC.ResetVaribles:
+                    RPCProcedure.resetVariables();
+                    break;
+                case (byte)CustomRPC.ForceEnd:
+                    RPCProcedure.forceEnd();
+                    break;
+                case (byte)CustomRPC.SetRole:
+                    byte roleId = HFPCBBHJIPJ.ReadByte();
+                    byte playerId = HFPCBBHJIPJ.ReadByte();
+                    RPCProcedure.setRole(roleId, playerId);
+                    break;
+
+
+                // Role functionality
+
+                case (byte)CustomRPC.JesterWin:
+                    RPCProcedure.jesterWin();
+                    break;
+                case (byte)CustomRPC.EngineerFixLights:
+                    RPCProcedure.engineerFixLights();
+                    break;
+                case (byte)CustomRPC.EngineerUsedRepair:
+                    RPCProcedure.engineerUsedRepair();
+                    break;
+                case (byte)CustomRPC.JanitorClean:
+                    RPCProcedure.janitorClean(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.SheriffKill:
+                    RPCProcedure.sheriffKill(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.TimeMasterRewindTime:
+                    RPCProcedure.timeMasterRewindTime();
+                    break;
+                case (byte)CustomRPC.MedicSetShielded:
+                    RPCProcedure.medicSetShielded(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.ShieldedMurderAttempt:
+                    RPCProcedure.shieldedMurderAttempt();
+                    break;
+                case (byte)CustomRPC.TimeMasterRevive:
+                    RPCProcedure.timeMasterRevive(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.ShifterShift:
+                    RPCProcedure.shifterShift(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.SwapperSwap:
+                    byte playerId1 = HFPCBBHJIPJ.ReadByte();
+                    byte playerId2 = HFPCBBHJIPJ.ReadByte();
+                    RPCProcedure.swapperSwap(playerId1, playerId2);
+                    break;
+                case (byte)CustomRPC.SeerReveal:
+                    RPCProcedure.seerReveal(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.MorphlingMorph:
+                    RPCProcedure.morphlingMorph(HFPCBBHJIPJ.ReadByte());
+                    break;
+                case (byte)CustomRPC.CamouflagerCamouflage:
+                    RPCProcedure.camouflagerCamouflage();
+                    break;
+                case (byte)CustomRPC.ChildDied:
+                    RPCProcedure.childDied();
+                    break;
+                case (byte)CustomRPC.LoverSuicide:
+                    RPCProcedure.loverSuicide(HFPCBBHJIPJ.ReadByte());
+                    break;
+            }
+        }
+    }
+}
diff --git a/Source Code/Resources/AdminTableIcon.png b/Source Code/Resources/AdminTableIcon.png
new file mode 100644 (file)
index 0000000..a1b2ee4
Binary files /dev/null and b/Source Code/Resources/AdminTableIcon.png differ
diff --git a/Source Code/Resources/CamoButton.png b/Source Code/Resources/CamoButton.png
new file mode 100644 (file)
index 0000000..9933097
Binary files /dev/null and b/Source Code/Resources/CamoButton.png differ
diff --git a/Source Code/Resources/CleanButton.png b/Source Code/Resources/CleanButton.png
new file mode 100644 (file)
index 0000000..83a817d
Binary files /dev/null and b/Source Code/Resources/CleanButton.png differ
diff --git a/Source Code/Resources/Footprint.png b/Source Code/Resources/Footprint.png
new file mode 100644 (file)
index 0000000..2bcd1ac
Binary files /dev/null and b/Source Code/Resources/Footprint.png differ
diff --git a/Source Code/Resources/MorphButton.png b/Source Code/Resources/MorphButton.png
new file mode 100644 (file)
index 0000000..14b8488
Binary files /dev/null and b/Source Code/Resources/MorphButton.png differ
diff --git a/Source Code/Resources/RepairButton.png b/Source Code/Resources/RepairButton.png
new file mode 100644 (file)
index 0000000..6e92051
Binary files /dev/null and b/Source Code/Resources/RepairButton.png differ
diff --git a/Source Code/Resources/RewindButton.png b/Source Code/Resources/RewindButton.png
new file mode 100644 (file)
index 0000000..adabbbd
Binary files /dev/null and b/Source Code/Resources/RewindButton.png differ
diff --git a/Source Code/Resources/SampleButton.png b/Source Code/Resources/SampleButton.png
new file mode 100644 (file)
index 0000000..11e531e
Binary files /dev/null and b/Source Code/Resources/SampleButton.png differ
diff --git a/Source Code/Resources/SeerButton.png b/Source Code/Resources/SeerButton.png
new file mode 100644 (file)
index 0000000..993dc05
Binary files /dev/null and b/Source Code/Resources/SeerButton.png differ
diff --git a/Source Code/Resources/ShieldButton.png b/Source Code/Resources/ShieldButton.png
new file mode 100644 (file)
index 0000000..c044fe7
Binary files /dev/null and b/Source Code/Resources/ShieldButton.png differ
diff --git a/Source Code/Resources/ShiftButton.png b/Source Code/Resources/ShiftButton.png
new file mode 100644 (file)
index 0000000..c45047f
Binary files /dev/null and b/Source Code/Resources/ShiftButton.png differ
diff --git a/Source Code/Resources/SwapperCheck.png b/Source Code/Resources/SwapperCheck.png
new file mode 100644 (file)
index 0000000..2feabac
Binary files /dev/null and b/Source Code/Resources/SwapperCheck.png differ
diff --git a/Source Code/RoleAssignmentPatch.cs b/Source Code/RoleAssignmentPatch.cs
new file mode 100644 (file)
index 0000000..77df16d
--- /dev/null
@@ -0,0 +1,91 @@
+using HarmonyLib;
+using Hazel;
+using System.Collections.Generic;
+using System.Linq;
+using UnhollowerBaseLib;
+using System;
+using static BonusRoles.BonusRoles;
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSetInfected))]
+    class SetInfectedPatch
+    {
+        private static void setRoleToRandomPlayer(byte roleId, List<PlayerControl> playerList) {
+            var index = rnd.Next(0, playerList.Count);
+            byte playerId = playerList[index].PlayerId;
+            playerList.RemoveAt(index);
+
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRole, Hazel.SendOption.None, -1);
+            writer.Write(roleId);
+            writer.Write(playerId);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            RPCProcedure.setRole(roleId, playerId);
+        }
+
+        public static void Postfix(Il2CppReferenceArray<GameData.PlayerInfo> FMAOEJEHPAO)
+        {
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ResetVaribles, Hazel.SendOption.None, -1);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            RPCProcedure.resetVariables();
+
+            List<PlayerControl> crewmates = PlayerControl.AllPlayerControls.ToArray().ToList();
+            crewmates.RemoveAll(x => x.Data.IsImpostor);
+            List<PlayerControl> impostors = PlayerControl.AllPlayerControls.ToArray().ToList();
+            impostors.RemoveAll(x => !x.Data.IsImpostor);
+
+            // Special roles impostors can be converted to
+            if (impostors.Count >= 3 && (rnd.Next(1, 101) <= BonusRolesPlugin.mafiaSpawnChance.GetValue())) {
+                setRoleToRandomPlayer((byte)RoleId.Godfather, impostors);
+                setRoleToRandomPlayer((byte)RoleId.Janitor, impostors);
+                setRoleToRandomPlayer((byte)RoleId.Mafioso, impostors);
+            }
+
+            if (impostors.Count >= 1 && (rnd.Next(1, 101) <= BonusRolesPlugin.morphlingSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Morphling, impostors);
+
+            if (impostors.Count >= 1 && (rnd.Next(1, 101) <= BonusRolesPlugin.camouflagerSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Camouflager, impostors);
+            
+            // Special roles that involve crewmates and impostors
+            if (rnd.Next(1, 101) <= BonusRolesPlugin.loversSpawnChance.GetValue())
+            {
+                if (impostors.Count > 0 && crewmates.Count > 0 && rnd.Next(1, 101) <= 33) {
+                    setRoleToRandomPlayer((byte)RoleId.Lover1, impostors); 
+                    setRoleToRandomPlayer((byte)RoleId.Lover2, crewmates); 
+                } else if (crewmates.Count >= 2) {
+                    setRoleToRandomPlayer((byte)RoleId.Lover1, crewmates); 
+                    setRoleToRandomPlayer((byte)RoleId.Lover2, crewmates); 
+                }
+            }
+
+            // Special roles crewmates can be converted to
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.jesterSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Jester, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.mayorSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Mayor, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.engineerSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Engineer, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.sheriffSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Sheriff, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.lighterSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Lighter, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.detectiveSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Detective, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.timeMasterSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.TimeMaster, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.medicSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Medic, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.shifterSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Shifter, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.swapperSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Swapper, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.seerSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Seer, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.spySpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Spy, crewmates);
+            if (crewmates.Count > 0 && (rnd.Next(1, 101) <= BonusRolesPlugin.childSpawnChance.GetValue()))
+                setRoleToRandomPlayer((byte)RoleId.Child, crewmates);
+        }
+    }
+}
diff --git a/Source Code/ShipStatusPatch.cs b/Source Code/ShipStatusPatch.cs
new file mode 100644 (file)
index 0000000..6636cc2
--- /dev/null
@@ -0,0 +1,15 @@
+  
+using HarmonyLib;
+using static BonusRoles.BonusRoles;
+
+namespace BonusRoles {
+
+    [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.CalculateLightRadius))]
+    public class ShipStatusPatch {
+        public static void Postfix(ref float __result, ShipStatus __instance, [HarmonyArgument(0)] GameData.PlayerInfo PlayerData) {
+            if (Lighter.lighter != null && Lighter.lighter.PlayerId == PlayerData.PlayerId) {
+                __result = __instance.MaxLightRadius * Lighter.lighterVision;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/UpdatePatch.cs b/Source Code/UpdatePatch.cs
new file mode 100644 (file)
index 0000000..23f8274
--- /dev/null
@@ -0,0 +1,334 @@
+using HarmonyLib;
+using System;
+using System.IO;
+using System.Net.Http;
+using UnityEngine;
+using static BonusRoles.BonusRoles;
+using Reactor.Extensions;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BonusRoles
+{
+    [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
+    class HudManagerUpdatePatch
+    {
+        static void resetNameTagsAndColors() {
+            Dictionary<byte, PlayerControl> playersById = Helpers.allPlayersById();
+
+            foreach (PlayerControl player in PlayerControl.AllPlayerControls) {
+                player.nameText.Text = player.Data.PlayerName;
+                if (PlayerControl.LocalPlayer.Data.IsImpostor && player.Data.IsImpostor) {
+                    player.nameText.Color = Palette.ImpostorRed;
+                } else {
+                    player.nameText.Color = Color.white;
+                }
+            }
+            if (MeetingHud.Instance != null)
+                foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates) {
+                    PlayerControl playerControl = playersById.ContainsKey((byte)player.TargetPlayerId) ? playersById[(byte)player.TargetPlayerId] : null;
+                    if (playerControl != null) {
+                        player.NameText.Text = playerControl.Data.PlayerName;
+                        if (PlayerControl.LocalPlayer.Data.IsImpostor && playerControl.Data.IsImpostor) {
+                            player.NameText.Color = Palette.ImpostorRed;
+                        } else {
+                            player.NameText.Color = Color.white;
+                        }
+                    }
+                }
+
+            if (PlayerControl.LocalPlayer.Data.IsImpostor) {
+                List<PlayerControl> impostors = PlayerControl.AllPlayerControls.ToArray().ToList();
+                impostors.RemoveAll(x => !x.Data.IsImpostor);
+                foreach (PlayerControl player in impostors)
+                    player.nameText.Color = Palette.ImpostorRed;
+                if (MeetingHud.Instance != null)
+                    foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates) {
+                        PlayerControl playerControl = Helpers.playerById((byte)player.TargetPlayerId);
+                        if (playerControl != null && playerControl.Data.IsImpostor)
+                            player.NameText.Color =  Palette.ImpostorRed;
+                    }
+            }
+            
+        }
+
+        static void setPlayerNameColor(PlayerControl p, Color color) {
+            p.nameText.Color = color;
+            if (MeetingHud.Instance != null)
+                foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates)
+                    if (player.NameText != null && p.PlayerId == player.TargetPlayerId)
+                        player.NameText.Color = color;
+        }
+
+        static void setNameColors() {
+            if (Jester.jester != null && Jester.jester == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Jester.jester, Jester.color);
+            else if (Mayor.mayor != null && Mayor.mayor == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Mayor.mayor, Mayor.color);
+            else if (Engineer.engineer != null && Engineer.engineer == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Engineer.engineer, Engineer.color);
+            else if (Sheriff.sheriff != null && Sheriff.sheriff == PlayerControl.LocalPlayer) 
+                setPlayerNameColor(Sheriff.sheriff, Sheriff.color);
+            else if (Lighter.lighter != null && Lighter.lighter == PlayerControl.LocalPlayer) 
+                setPlayerNameColor(Lighter.lighter, Lighter.color);
+            else if (Detective.detective != null && Detective.detective == PlayerControl.LocalPlayer) 
+                setPlayerNameColor(Detective.detective, Detective.color);
+            else if (TimeMaster.timeMaster != null && TimeMaster.timeMaster == PlayerControl.LocalPlayer)
+                setPlayerNameColor(TimeMaster.timeMaster, TimeMaster.color);
+            else if (Medic.medic != null && Medic.medic == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Medic.medic, Medic.color);
+            else if (Shifter.shifter != null && Shifter.shifter == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Shifter.shifter, Shifter.color);
+            else if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Swapper.swapper, Swapper.color);
+            else if (Lovers.lover1 != null && Lovers.lover2 != null && (Lovers.lover1 == PlayerControl.LocalPlayer || Lovers.lover2 == PlayerControl.LocalPlayer)) {             
+                setPlayerNameColor(Lovers.lover1, Lovers.color);
+                setPlayerNameColor(Lovers.lover2, Lovers.color);
+            }
+            else if (Seer.seer != null && Seer.seer == PlayerControl.LocalPlayer)
+                setPlayerNameColor(Seer.seer, Seer.color);  
+            else if (Spy.spy != null && Spy.spy == PlayerControl.LocalPlayer) 
+                setPlayerNameColor(Spy.spy, Spy.color);
+
+            // Crewmate roles with no changes: Child
+            // Impostor roles with no changes: Morphling, Camouflager, Godfather, Janitor and Mafioso
+        }
+
+        static void setMafiaNameTags() {
+            if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.IsImpostor) {
+                foreach (PlayerControl player in PlayerControl.AllPlayerControls)
+                    if (Godfather.godfather != null && Godfather.godfather == player)
+                            player.nameText.Text = player.Data.PlayerName + " (G)";
+                    else if (Mafioso.mafioso != null && Mafioso.mafioso == player)
+                            player.nameText.Text = player.Data.PlayerName + " (M)";
+                    else if (Janitor.janitor != null && Janitor.janitor == player)
+                            player.nameText.Text = player.Data.PlayerName + " (J)";
+                if (MeetingHud.Instance != null)
+                    foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates)
+                        if (Godfather.godfather != null && Godfather.godfather.PlayerId == player.TargetPlayerId)
+                            player.NameText.Text = Godfather.godfather.Data.PlayerName + " (G)";
+                        else if (Mafioso.mafioso != null && Mafioso.mafioso.PlayerId == player.TargetPlayerId)
+                            player.NameText.Text = Mafioso.mafioso.Data.PlayerName + " (M)";
+                        else if (Janitor.janitor != null && Janitor.janitor.PlayerId == player.TargetPlayerId)
+                            player.NameText.Text = Janitor.janitor.Data.PlayerName + " (J)";
+            }
+        }
+
+        static void updateShielded() {
+            if (Medic.shielded == null) return;
+
+            if(Medic.showShielded == 0) // Everyone
+            {
+                Medic.shielded.myRend.material.SetFloat("_Outline",  1f);
+                Medic.shielded.myRend.material.SetColor("_OutlineColor", Medic.shieldedColor);
+            }
+            else if (Medic.showShielded == 1 && PlayerControl.LocalPlayer == Medic.shielded) // Shielded + Medic
+            {
+                Medic.shielded.myRend.material.SetFloat("_Outline", 1f);
+                Medic.shielded.myRend.material.SetColor("_OutlineColor", Medic.shieldedColor);
+            }
+            else if(PlayerControl.LocalPlayer == Medic.medic) // Medic
+            {
+                Medic.shielded.myRend.material.SetFloat("_Outline",  1f);
+                Medic.shielded.myRend.material.SetColor("_OutlineColor", Medic.shieldedColor);
+            }
+
+            // Break shield
+            if (Medic.shielded.Data.IsDead || Medic.medic == null || Medic.medic.Data.IsDead) {
+                Medic.shielded.myRend.material.SetFloat("_Outline", 0f);
+                Medic.shielded = null;
+            }
+        }
+
+        static void jesterClearTasks() {
+            if (Jester.jester == null) return;
+
+            var toRemove = new List<PlayerTask>();
+            foreach (PlayerTask task in Jester.jester.myTasks)
+                if (task.TaskType != TaskTypes.FixComms && task.TaskType != TaskTypes.FixLights && task.TaskType != TaskTypes.ResetReactor && task.TaskType != TaskTypes.ResetSeismic && task.TaskType != TaskTypes.RestoreOxy)
+                    toRemove.Add(task);
+            foreach (PlayerTask task in toRemove)
+                Jester.jester.RemoveTask(task);
+        }
+
+        static void mafiosoDeactivateKillButtonIfNecessary(HudManager __instance) {
+            if (Mafioso.mafioso == null || Mafioso.mafioso != PlayerControl.LocalPlayer) return;
+
+            if (!PlayerControl.LocalPlayer.Data.IsDead && __instance.UseButton.isActiveAndEnabled && Godfather.godfather != null && Godfather.godfather.Data.IsDead) {
+                __instance.KillButton.gameObject.SetActive(true);
+                __instance.KillButton.renderer.enabled = true;
+                __instance.KillButton.isActive = true;
+                __instance.KillButton.enabled = true;
+            } else {
+                __instance.KillButton.gameObject.SetActive(false);
+                __instance.KillButton.renderer.enabled = false;
+                __instance.KillButton.isActive = false;
+                __instance.KillButton.enabled = false;
+            }
+        }
+
+        static void janitorDeactivateKillButton(HudManager __instance) {
+            if (Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer) {
+                __instance.KillButton.gameObject.SetActive(false);
+                __instance.KillButton.renderer.enabled = false;
+                __instance.KillButton.isActive = false;
+                __instance.KillButton.enabled = false;
+            }
+        }
+
+        static void shifterClearTasks() {
+            if (Shifter.shifter == null) return;
+
+            var toRemove = new List<PlayerTask>();
+            foreach (PlayerTask task in Shifter.shifter.myTasks)
+                if (task.TaskType != TaskTypes.FixComms && task.TaskType != TaskTypes.FixLights && task.TaskType != TaskTypes.ResetReactor && task.TaskType != TaskTypes.ResetSeismic && task.TaskType != TaskTypes.RestoreOxy)
+                    toRemove.Add(task);
+            foreach (PlayerTask task in toRemove)
+                Shifter.shifter.RemoveTask(task);
+        }
+
+        static void seerUpdate() {
+            if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer) return;
+
+            // Update revealed players
+            foreach (PlayerControl p in Seer.revealedPlayers) {
+                // Update color and name regarding settings and given info
+                string result = p.Data.PlayerName;
+                SeerInfo si = SeerInfo.getSeerInfoForPlayer(p);
+                if (Seer.kindOfInfo == 0)
+                    result = p.Data.PlayerName + " (" + si.roleName + ")";
+                else if (Seer.kindOfInfo == 1) {
+                    si.color = si.isGood ? new Color(250f / 255f, 217f / 255f, 52f / 255f, 1) : new Color (51f / 255f, 61f / 255f, 54f / 255f, 1); 
+                }
+
+                // Set color and name
+                p.nameText.Color = si.color;
+                p.nameText.Text = result;
+                if (MeetingHud.Instance != null) {
+                    foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates) {
+                        if (p.PlayerId == player.TargetPlayerId) {
+                            player.NameText.Text = result;
+                            player.NameText.Color = si.color;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        static void camouflageAndMorphActions() {
+            float oldCamouflageTimer = Camouflager.camouflageTimer;
+            float oldMorphTimer = Morphling.morphTimer;
+
+            Camouflager.camouflageTimer -= Time.deltaTime;
+            Morphling.morphTimer -= Time.deltaTime;
+
+            // Morphling player size not done here
+
+            // Set morphling morphed look
+            if (Morphling.morphTimer > 0f && Camouflager.camouflageTimer <= 0f) {
+                if (Morphling.morphling != null && Morphling.morphTarget != null) {
+                    if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer)
+                        Morphling.morphling.nameText.Text = Morphling.morphTarget.Data.PlayerName;
+                    Morphling.morphling.myRend.material.SetColor("_BackColor", Palette.ShadowColors[Morphling.morphTarget.Data.ColorId]);
+                    Morphling.morphling.myRend.material.SetColor("_BodyColor", Palette.PlayerColors[Morphling.morphTarget.Data.ColorId]);
+                    Morphling.morphling.HatRenderer.SetHat(Morphling.morphTarget.Data.HatId, Morphling.morphTarget.Data.ColorId);
+                    Morphling.morphling.nameText.transform.localPosition = new Vector3(0f, (Morphling.morphTarget.Data.HatId == 0U) ? 0.7f : 1.05f, -0.5f);
+
+                    if (Morphling.morphling.MyPhysics.Skin.skin.ProdId != DestroyableSingleton<HatManager>.Instance.AllSkins[(int)Morphling.morphTarget.Data.SkinId].ProdId) {
+                        Helpers.setSkinWithAnim(Morphling.morphling.MyPhysics, Morphling.morphTarget.Data.SkinId);
+                    }
+                    if (Morphling.morphling.CurrentPet == null || Morphling.morphling.CurrentPet.ProdId != DestroyableSingleton<HatManager>.Instance.AllPets[(int)Morphling.morphTarget.Data.PetId].ProdId) {
+                        if (Morphling.morphling.CurrentPet) UnityEngine.Object.Destroy(Morphling.morphling.CurrentPet.gameObject);
+                        Morphling.morphling.CurrentPet = UnityEngine.Object.Instantiate<PetBehaviour>(DestroyableSingleton<HatManager>.Instance.AllPets[(int)Morphling.morphTarget.Data.PetId]);
+                        Morphling.morphling.CurrentPet.transform.position = Morphling.morphling.transform.position;
+                        Morphling.morphling.CurrentPet.Source = Morphling.morphling;
+                        Morphling.morphling.CurrentPet.Visible = Morphling.morphling.Visible;
+                        PlayerControl.SetPlayerMaterialColors(Morphling.morphTarget.Data.ColorId, Morphling.morphling.CurrentPet.rend);
+                    } else if (Morphling.morphling.CurrentPet) {
+                        PlayerControl.SetPlayerMaterialColors(Morphling.morphTarget.Data.ColorId, Morphling.morphling.CurrentPet.rend);
+                    }
+                }
+            }
+
+            // Set camouflaged look (overrides morphling morphed look if existent)
+            if (Camouflager.camouflageTimer > 0f) {
+                foreach (PlayerControl p in PlayerControl.AllPlayerControls) {
+                    if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer)
+                        p.nameText.Text = "";
+                    p.myRend.material.SetColor("_BackColor", Color.grey);
+                    p.myRend.material.SetColor("_BodyColor", Color.grey);
+                    p.HatRenderer.SetHat(0, 0);
+                    Helpers.setSkinWithAnim(p.MyPhysics, 0);
+                    if (p.CurrentPet) UnityEngine.Object.Destroy(p.CurrentPet.gameObject);
+                }
+            } 
+            
+            // Everyone but morphling reset
+            if (oldCamouflageTimer > 0f && Camouflager.camouflageTimer <= 0f) {
+                foreach (PlayerControl p in PlayerControl.AllPlayerControls) {
+                    if (Morphling.morphling == null || Morphling.morphling != p) {
+                        if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer)
+                            p.SetName(p.Data.PlayerName);
+                        p.SetHat(p.Data.HatId, (int)p.Data.ColorId);
+                        Helpers.setSkinWithAnim(p.MyPhysics, p.Data.SkinId);
+                        p.SetPet(p.Data.PetId);
+                        p.CurrentPet.Visible = p.Visible;
+                        p.SetColor(p.Data.ColorId);
+                    }
+                }
+            }
+
+            // Morphling reset
+            if ((oldMorphTimer > 0f || oldCamouflageTimer > 0f) && Camouflager.camouflageTimer <= 0f && Morphling.morphTimer <= 0f && Morphling.morphling != null) {
+                Morphling.morphTarget = null;
+                if (Seer.seer == null || Seer.seer != PlayerControl.LocalPlayer)
+                    Morphling.morphling.SetName(Morphling.morphling.Data.PlayerName);
+                Morphling.morphling.SetHat(Morphling.morphling.Data.HatId, (int)Morphling.morphling.Data.ColorId);
+                Helpers.setSkinWithAnim(Morphling.morphling.MyPhysics, Morphling.morphling.Data.SkinId);
+                Morphling.morphling.SetPet(Morphling.morphling.Data.PetId);
+                Morphling.morphling.CurrentPet.Visible = Morphling.morphling.Visible;
+                Morphling.morphling.SetColor(Morphling.morphling.Data.ColorId);
+            }
+        }
+
+        public static void updatePlayerSizes() {
+            foreach (PlayerControl p in PlayerControl.AllPlayerControls) {
+                if (p == null) continue;
+
+                if (Child.child != null && Child.child == p)
+                    p.transform.localScale = new Vector3(0.35f, 0.35f, 1f);
+                else if (Morphling.morphling != null && Morphling.morphling == p && Morphling.morphTarget != null && Morphling.morphTarget == Child.child && Morphling.morphTimer > 0f)
+                    p.transform.localScale = new Vector3(0.35f, 0.35f, 1f);
+                else
+                    p.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
+            }
+        }
+
+        static void Postfix(HudManager __instance)
+        {
+            if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
+
+            CustomButton.HudUpdate();
+            resetNameTagsAndColors();
+            setNameColors();
+            updateShielded();
+
+            // Mafia
+            setMafiaNameTags();
+            // Jester
+            jesterClearTasks();
+            // Mafioso
+            mafiosoDeactivateKillButtonIfNecessary(__instance);
+            // Janitor
+            janitorDeactivateKillButton(__instance);
+            // Shifter
+            shifterClearTasks();
+            // Seer update
+            seerUpdate();
+
+            camouflageAndMorphActions();
+            updatePlayerSizes();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source Code/UsablesPatch.cs b/Source Code/UsablesPatch.cs
new file mode 100644 (file)
index 0000000..8dea16b
--- /dev/null
@@ -0,0 +1,244 @@
+using HarmonyLib;
+using System;
+using System.IO;
+using System.Net.Http;
+using UnityEngine;
+using System.Linq;
+using static BonusRoles.BonusRoles;
+using static BonusRoles.GameHistory;
+using Reactor.Extensions;
+using System.Collections.Generic;
+
+namespace BonusRoles
+{
+
+    [HarmonyPatch(typeof(Vent), "CanUse")]
+    public static class VentPatch
+    {
+        public static bool Prefix(Vent __instance, ref float __result, [HarmonyArgument(0)] GameData.PlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse)
+        {
+            float num = float.MaxValue;
+            PlayerControl @object = pc.Object;
+
+
+            bool roleCouldUse = false;
+            if (Engineer.engineer != null && Engineer.engineer == @object)
+                roleCouldUse = true;
+            else if (pc.IsImpostor) {
+                if (Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer)
+                    roleCouldUse = false;
+                else if (Mafioso.mafioso != null && Mafioso.mafioso == PlayerControl.LocalPlayer && Godfather.godfather != null && !Godfather.godfather.Data.IsDead)
+                    roleCouldUse = false;
+                else
+                    roleCouldUse = true;
+            }
+
+            couldUse = ((@object.inVent || roleCouldUse) && !pc.IsDead && (@object.CanMove || @object.inVent));
+            canUse = couldUse;
+            if (canUse)
+            {
+                Vector2 truePosition = @object.GetTruePosition();
+                Vector3 position = __instance.transform.position;
+                num = Vector2.Distance(truePosition, position);
+                canUse &= (num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(truePosition, position, Constants.ShipOnlyMask, false));
+            }
+            __result = num;
+            return false;
+        }
+    }
+
+    [HarmonyPatch(typeof(UseButtonManager), nameof(UseButtonManager.SetTarget))]
+    class UseButtonSetTargetPatch {
+        static void Postfix(UseButtonManager __instance) {
+            if (__instance.Field_3 != null) return;
+
+            // Mafia sabotage button render patch
+            bool blockSabotageJanitor = (Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer);
+            bool blockSabotageMafioso = (Mafioso.mafioso != null && Mafioso.mafioso == PlayerControl.LocalPlayer && Godfather.godfather != null && !Godfather.godfather.Data.IsDead);
+            if (blockSabotageJanitor || blockSabotageMafioso) {
+                // __instance.UseButton.sprite = __instance.UseImage;
+                __instance.UseButton.color = new Color(1f, 1f, 1f, 0.3f);
+            }
+
+        }
+    }
+
+    [HarmonyPatch(typeof(UseButtonManager), nameof(UseButtonManager.DoClick))]
+    class UseButtonDoClickPatch {
+        static bool Prefix(UseButtonManager __instance) { 
+            if (__instance.Field_3 != null) return true;
+
+            // Mafia sabotage button click patch
+            bool blockSabotageJanitor = (Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer);
+            bool blockSabotageMafioso = (Mafioso.mafioso != null && Mafioso.mafioso == PlayerControl.LocalPlayer && Godfather.godfather != null && !Godfather.godfather.Data.IsDead);
+            if (blockSabotageJanitor || blockSabotageMafioso) return false;
+
+            return true;
+        }
+    }
+
+    [HarmonyPatch(typeof(EmergencyMinigame), nameof(EmergencyMinigame.Update))]
+    class EmergencyMinigameUpdatePatch {
+        static void Postfix(EmergencyMinigame __instance) {
+            // Swapper deactivate emergency button
+            if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer) {
+                __instance.StatusText.Text = "The Swapper can't start an emergency meeting";
+                __instance.NumberText.Text = string.Empty;
+                __instance.ClosedLid.gameObject.SetActive(true);
+                __instance.OpenLid.gameObject.SetActive(false);
+                __instance.ButtonActive = false;
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(TuneRadioMinigame), nameof(TuneRadioMinigame.Begin))]
+    class CommsMinigameBeginPatch {
+        static void Postfix(TuneRadioMinigame __instance) {
+            // Block Swapper from fixing comms. Still looking for a better way to do this, but deleting the task doesn't seem like a viable option since then the camera, admin table, ... work while comms are out
+            if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer) {
+                __instance.Close();
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(SwitchMinigame), nameof(SwitchMinigame.Begin))]
+    class LightsMinigameBeginPatch {
+        static void Postfix(SwitchMinigame __instance) {
+            // Block Swapper from fixing lights. One could also just delete the PlayerTask, but I wanted to do it the same way as with coms for now.
+            if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer) {
+                __instance.Close();
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(VitalsMinigame), nameof(VitalsMinigame.Update))]
+    class VitalsMinigamePatch {
+        static void Postfix(VitalsMinigame __instance) {
+            // Spy show time since death
+            if (Spy.spy == null || Spy.spy != PlayerControl.LocalPlayer) return;
+            for (int k = 0; k < __instance.vitals.Length; k++)
+            {
+                VitalsPanel vitalsPanel = __instance.vitals[k];
+                GameData.PlayerInfo playerInfo = GameData.Instance.AllPlayers[k];
+
+                if (vitalsPanel.IsDead) {
+                    DeadPlayer deadPlayer = deadPlayers?.Where(x => x.player?.PlayerId == playerInfo?.PlayerId)?.FirstOrDefault();
+                    if (deadPlayer != null && deadPlayer.timeOfDeath != null) {
+                        float timeSinceDeath = ((float)(DateTime.UtcNow - deadPlayer.timeOfDeath).TotalMilliseconds);
+                        vitalsPanel.Text.Text = Math.Round(timeSinceDeath / 1000) + "s";
+                    }
+                }
+               }
+        }
+    }    
+
+    [HarmonyPatch]
+    class AdminPanelPatch {
+        static Dictionary<SystemTypes, List<Color>> players = new Dictionary<SystemTypes, List<Color>>();
+
+        [HarmonyPatch(typeof(MapCountOverlay), nameof(MapCountOverlay.Update))]
+        class MapCountOverlayUpdatePatch {
+            static bool Prefix(MapCountOverlay __instance) {
+                // Spy save colors for each room on the admin panel
+                if (Spy.spy == null || Spy.spy != PlayerControl.LocalPlayer) return true;
+
+                __instance.timer += Time.deltaTime;
+                if (__instance.timer < 0.1f)
+                {
+                    return false;
+                }
+                __instance.timer = 0f;
+                players = new Dictionary<SystemTypes, List<Color>>();
+                bool commsActive = false;
+                    foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks)
+                        if (task.TaskType == TaskTypes.FixComms) commsActive = true;       
+
+
+                if (!__instance.isSab && commsActive)
+                {
+                    __instance.isSab = true;
+                    __instance.BackgroundColor.SetColor(Palette.DisabledGrey);
+                    __instance.SabotageText.gameObject.SetActive(true);
+                    return false;
+                }
+                if (__instance.isSab && !commsActive)
+                {
+                    __instance.isSab = false;
+                    __instance.BackgroundColor.SetColor(Color.green);
+                    __instance.SabotageText.gameObject.SetActive(false);
+                }
+
+                for (int i = 0; i < __instance.CountAreas.Length; i++)
+                {
+                    CounterArea counterArea = __instance.CountAreas[i];
+                    List<Color> roomColors = new List<Color>();
+                    players.Add(counterArea.RoomType, roomColors);
+
+                    if (!commsActive)
+                    {
+                        PlainShipRoom plainShipRoom = ShipStatus.Instance.FastRooms[counterArea.RoomType];
+
+                        if (plainShipRoom != null && plainShipRoom.roomArea)
+                        {
+                            int num = plainShipRoom.roomArea.OverlapCollider(__instance.filter, __instance.buffer);
+                            int num2 = num;
+                            for (int j = 0; j < num; j++)
+                            {
+                                Collider2D collider2D = __instance.buffer[j];
+                                if (!(collider2D.tag == "DeadBody"))
+                                {
+                                    PlayerControl component = collider2D.GetComponent<PlayerControl>();
+                                    if (!component || component.Data == null || component.Data.Disconnected || component.Data.IsDead)
+                                    {
+                                        num2--;
+                                    } else {
+                                        roomColors.Add(Palette.PlayerColors[component.Data.ColorId]);
+                                    }
+                                } else {
+                                    DeadBody component = collider2D.GetComponent<DeadBody>();
+                                    if (component) {
+                                        GameData.PlayerInfo playerInfo = GameData.Instance.GetPlayerById(component.ParentId);
+                                        if (playerInfo != null)
+                                            roomColors.Add(Palette.PlayerColors[playerInfo.ColorId]);
+                                    }
+                                }
+                            }
+                            counterArea.UpdateCount(num2);
+                        }
+                        else
+                        {
+                            Debug.LogWarning("Couldn't find counter for:" + counterArea.RoomType);
+                        }
+                    }
+                    else
+                    {
+                        counterArea.UpdateCount(0);
+                    }
+                }
+                return false;
+            }
+        }
+
+        [HarmonyPatch(typeof(CounterArea), nameof(CounterArea.UpdateCount))]
+        class CounterAreaUpdateCountPatch {
+            static void Postfix(CounterArea __instance) {
+                // Spy display saved colors on the admin panel
+                if (Spy.spy == null || Spy.spy != PlayerControl.LocalPlayer) return;
+
+                if (players.ContainsKey(__instance.RoomType)) {
+                    List<Color> colors = players[__instance.RoomType];
+
+                    for (int i = 0; i < __instance.myIcons.Count; i++) {
+                        PoolableBehavior icon = __instance.myIcons[i];
+                        SpriteRenderer renderer = icon.GetComponent<SpriteRenderer>();
+
+                        if (renderer != null && colors.Count > i && Spy.getAdminTableIconSprite() != null) {
+                            renderer.sprite = Spy.getAdminTableIconSprite();
+                            renderer.color = colors[i];
+                        } 
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file