]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Update v4.5.0
authorgendelo3 <gendelo3@gmail.com>
Fri, 15 Dec 2023 18:52:32 +0000 (19:52 +0100)
committergendelo3 <gendelo3@gmail.com>
Fri, 15 Dec 2023 18:52:32 +0000 (19:52 +0100)
52 files changed:
README.md
TheOtherRoles/Buttons.cs
TheOtherRoles/CustomGameModes/PropHunt.cs
TheOtherRoles/CustomOptionHolder.cs
TheOtherRoles/Helpers.cs
TheOtherRoles/Main.cs
TheOtherRoles/MapOptions.cs
TheOtherRoles/Modules/ChatCommands.cs
TheOtherRoles/Modules/CustomColors.cs
TheOtherRoles/Modules/CustomHats.cs [deleted file]
TheOtherRoles/Modules/CustomHats/CustomHat.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/CustomHatHashes.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/CustomHatManager.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Extensions/HatDataExtensions.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/HatExtension.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/HatsLoader.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Patches/CosmeticsCachePatches.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Patches/HatManagerPatches.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Patches/HatParentPatches.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Patches/HatsTabPatches.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/Patches/PlayerPhysicsPatches.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomHats/SkinsConfigFile.cs [new file with mode: 0644]
TheOtherRoles/Modules/CustomOptions.cs
TheOtherRoles/Modules/ModUpdater.cs
TheOtherRoles/Objects/Bloodytrail.cs
TheOtherRoles/Objects/Footprint.cs
TheOtherRoles/Objects/JackInTheBox.cs
TheOtherRoles/Objects/Portal.cs
TheOtherRoles/Patches/AirShipSetAntiTpPosition.cs [deleted file]
TheOtherRoles/Patches/ClientOptionsPatch.cs
TheOtherRoles/Patches/ConstantsPatch.cs
TheOtherRoles/Patches/CredentialsPatch.cs
TheOtherRoles/Patches/EndGamePatch.cs
TheOtherRoles/Patches/ExileControllerPatch.cs
TheOtherRoles/Patches/GameStartManagerPatch.cs
TheOtherRoles/Patches/MainMenuPatch.cs
TheOtherRoles/Patches/MapBehaviourPatch.cs
TheOtherRoles/Patches/MeetingPatch.cs
TheOtherRoles/Patches/PlayerControlPatch.cs
TheOtherRoles/Patches/ShipStatusPatch.cs
TheOtherRoles/Patches/TransportationToolPatches.cs [new file with mode: 0644]
TheOtherRoles/Patches/UpdatePatch.cs
TheOtherRoles/Patches/UsablesPatch.cs
TheOtherRoles/RPC.cs
TheOtherRoles/Resources/FungleVentSealed.png [new file with mode: 0644]
TheOtherRoles/Resources/Txt/Props.txt
TheOtherRoles/Resources/Vent.png [new file with mode: 0644]
TheOtherRoles/SubmergedCompatibility.cs
TheOtherRoles/TheOtherRoles.cs
TheOtherRoles/TheOtherRoles.csproj
TheOtherRoles/Utilities/EventUtility.cs
TheOtherRoles/packages.lock.json

index a14e50b9b647d8f1a1700404e10bd3597dfa80a2..279bfd0a18e93a717e28d57e0bbf14838287f139 100644 (file)
--- a/README.md
+++ b/README.md
@@ -37,22 +37,24 @@ Even more roles are coming soon. :)
 |  | [Trapper](#trapper) |  |  |
 |  | [Nice Guesser](#guesser) |  |  |
 
-The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players.
+The [Role Assignment](#role-assignment) section explains how the roles are being distributed among the players.
 
 # Releases
 | Among Us - Version| Mod Version | Link |
 |----------|-------------|-----------------|
+| 2023.11.28s| v4.5.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.5.0/TheOtherRoles.zip)
 | 2023.07.12s| v4.4.2| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.4.2/TheOtherRoles.zip)
-| 2023.07.12s| v4.4.1| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.4.1/TheOtherRoles.zip)
-| 2023.07.12s| v4.4.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.4.0/TheOtherRoles.zip)
-| 2023.07.12s| v4.3.4| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.4/TheOtherRoles.zip)
-| 2023.07.12s| v4.3.3| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.3/TheOtherRoles.zip)
-| 2023.03.28s| v4.3.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.2/TheOtherRoles.zip)
+
 <details>
   <summary>Click to show older versions</summary>
   
 | Among Us - Version| Mod Version | Link |
 |----------|-------------|-----------------|
+| 2023.07.12s| v4.4.1| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.4.1/TheOtherRoles.zip)
+| 2023.07.12s| v4.4.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.4.0/TheOtherRoles.zip)
+| 2023.07.12s| v4.3.4| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.4/TheOtherRoles.zip)
+| 2023.07.12s| v4.3.3| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.3/TheOtherRoles.zip)
+| 2023.03.28s| v4.3.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.2/TheOtherRoles.zip)
 | 2023.03.28s| v4.3.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.1/TheOtherRoles.zip)
 | 2023.02.28s| v4.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.0/TheOtherRoles.zip)
 | 2022.12.14s| v4.2.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.2.1/TheOtherRoles.zip)
@@ -128,6 +130,18 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 <details>
   <summary>Click to show the Changelog</summary>
 
+**Version 4.5.0**
+- Updated to Among Us version 2023.11.28 (with the new map, Fungle)
+- Added a new feature: Show Vents on Map (toggle in your local options)
+- Added a new kill distance: Very Short (thanks twix)
+- Added a feature to change the gamemode in the settings: `/gm <gamemode>` (host only)
+- Fixed a bug where Pet visibility was sometimes wron
+- Fixed the HatLoader - testing hats should work again (thanks EnoPM)
+- Changed the HatLoader - loading the hats does not require a restart of the mod anymore
+- Changed the ModUpdater (thanks EnoPM)
+- Changed the Vampire: Kills will be delayed a bit longer if the target is using a ladder, platform or zipline
+- Note: Compatibility with Submerged and LevelImposter is not tested yet.
+
 **Version 4.4.2**
 - Added 10 new colors and adapted some existing ones (thanks to Avlona & Listoric for sorting our colors!)
 - Added a message of the day feature in the main menu. Look out for interesting news and lame jokes!
@@ -791,6 +805,26 @@ docker run -d -p 22023:22023/udp --env IMPOSTOR_AntiCheatEnabled=false --env IMP
 
 
 # Credits & Resources
+Team:
+Mallöris    K3ndo    Bavari    Gendelo
+
+Former Team Members:
+Eisbison (GOAT)    Thunderstorm584    EndOfFile
+
+Additional Devs:
+EnoPM    twix    NesTT
+
+Github Contributors:
+Alex2911    amsyarasyiq    MaximeGillot
+Psynomit    probablyadnf    JustASysAdmin
+
+[Discord](https://discord.gg/77RkMJHWsM]Discord) Moderators:</b>
+Draco Cordraconis    Streamblox (formerly)
+Thanks to all our discord helpers!
+
+Thanks to miniduikboot & GD for hosting modded servers (and so much more)
+
+
 [OxygenFilter](https://github.com/NuclearPowered/Reactor.OxygenFilter) - For all the versions between v2.3.0 and v2.6.1, we were using the OxygenFilter for automatic deobfuscation\
 [Reactor](https://github.com/NuclearPowered/Reactor) - The framework used for all versions before v2.0.0\
 [BepInEx](https://github.com/BepInEx) - Used to hook to game functions\
@@ -2030,6 +2064,9 @@ can only use them, if the previous player did not use them before)
 -----------------------
 
 # Gamemodes
+Gamemodes can be switched when creating a lobby or inside the lobby by using a command in the chat:
+`/gm <gamemode>`. Use the following gamemodes: `guess` or `gm`,`prophunt` or `ph`, `hidenseek` or `hns`. If `/gm` is used without argument or the argument can't be parsed, the lobby will switch to classic mode.
+
 
 ## Guesser Modifier
 The **Guesser-Gamemode** is an extension to the Classic-Gamemode and gives you a multitude of new options for Guessers.\
index f733166bd48b8a85fd2614735034478300240622..c9e3b8544e8777e81fc64a737234d31cd9aa9697 100644 (file)
@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using TheOtherRoles.Players;
 using TheOtherRoles.Utilities;
 using TheOtherRoles.CustomGameModes;
+using TheOtherRoles.Patches;
 
 namespace TheOtherRoles
 {
@@ -549,7 +550,7 @@ namespace TheOtherRoles
                     }
                 },
                 () => { return Morphling.morphling != null && Morphling.morphling == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead; },
-                () => { return (Morphling.currentTarget || Morphling.sampledTarget) && CachedPlayer.LocalPlayer.PlayerControl.CanMove; },
+                () => { return (Morphling.currentTarget || Morphling.sampledTarget) && CachedPlayer.LocalPlayer.PlayerControl.CanMove && !Helpers.MushroomSabotageActive(); },
                 () => { 
                     morphlingButton.Timer = morphlingButton.MaxTimer;
                     morphlingButton.Sprite = Morphling.getSampleSprite();
@@ -695,11 +696,11 @@ namespace TheOtherRoles
 
                    Hacker.chargesVitals--;
                },
-               () => { return Hacker.hacker != null && Hacker.hacker == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && GameOptionsManager.Instance.currentNormalGameOptions.MapId != 0 && GameOptionsManager.Instance.currentNormalGameOptions.MapId != 3; },
+               () => { return Hacker.hacker != null && Hacker.hacker == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && GameOptionsManager.Instance.currentGameOptions.MapId != 0 && GameOptionsManager.Instance.currentNormalGameOptions.MapId != 3; },
                () => {
                    if (hackerVitalsChargesText != null) hackerVitalsChargesText.text = $"{Hacker.chargesVitals} / {Hacker.toolsNumber}";
-                   hackerVitalsButton.actionButton.graphic.sprite = GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? Hacker.getLogSprite() : Hacker.getVitalsSprite();
-                   hackerVitalsButton.actionButton.OverrideText(GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? "DOORLOG" : "VITALS");
+                   hackerVitalsButton.actionButton.graphic.sprite = Helpers.isMira() ? Hacker.getLogSprite() : Hacker.getVitalsSprite();
+                   hackerVitalsButton.actionButton.OverrideText(Helpers.isMira() ? "DOORLOG" : "VITALS");
                    return Hacker.chargesVitals > 0;
                },
                () => {
@@ -713,16 +714,16 @@ namespace TheOtherRoles
                KeyCode.Q,
                true,
                0f,
-               () => { 
+               () => {
                    hackerVitalsButton.Timer = hackerVitalsButton.MaxTimer;
-                   if(!hackerAdminTableButton.isEffectActive) CachedPlayer.LocalPlayer.PlayerControl.moveable = true;
+                   if (!hackerAdminTableButton.isEffectActive) CachedPlayer.LocalPlayer.PlayerControl.moveable = true;
                    if (Minigame.Instance) {
-                       if (GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1) Hacker.doorLog.ForceClose();
+                       if (Helpers.isMira()) Hacker.doorLog.ForceClose();
                        else Hacker.vitals.ForceClose();
                    }
                },
                false,
-              GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? "DOORLOG" : "VITALS"
+              Helpers.isMira() ? "DOORLOG" : "VITALS"
            );
 
             // Hacker Vitals Charges
@@ -809,12 +810,14 @@ namespace TheOtherRoles
                                 }
                                 if (p == 1f) {
                                     // Perform kill if possible and reset bitten (regardless whether the kill was successful or not)
-                                    Helpers.checkMurderAttemptAndKill(Vampire.vampire, Vampire.bitten, showAnimation: false);
-                                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
-                                    writer.Write(byte.MaxValue);
-                                    writer.Write(byte.MaxValue);
-                                    AmongUsClient.Instance.FinishRpcImmediately(writer);
-                                    RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
+                                    var res = Helpers.checkMurderAttemptAndKill(Vampire.vampire, Vampire.bitten, showAnimation: false);
+                                    if (res == MurderAttemptResult.PerformKill) {
+                                        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
+                                        writer.Write(byte.MaxValue);
+                                        writer.Write(byte.MaxValue);
+                                        AmongUsClient.Instance.FinishRpcImmediately(writer);
+                                        RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
+                                    }
                                 }
                             })));
                             SoundEffectsManager.play("vampireBite");
@@ -1114,7 +1117,8 @@ namespace TheOtherRoles
                     RPCProcedure.lightsOut();
                     SoundEffectsManager.play("lighterLight");
                 },
-                () => { return Trickster.trickster != null && Trickster.trickster == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && JackInTheBox.hasJackInTheBoxLimitReached() && JackInTheBox.boxesConvertedToVents; },
+                () => { return Trickster.trickster != null && Trickster.trickster == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead
+                                                           && JackInTheBox.hasJackInTheBoxLimitReached() && JackInTheBox.boxesConvertedToVents; },
                 () => { return CachedPlayer.LocalPlayer.PlayerControl.CanMove && JackInTheBox.hasJackInTheBoxLimitReached() && JackInTheBox.boxesConvertedToVents; },
                 () => { 
                     lightsOutButton.Timer = lightsOutButton.MaxTimer;
@@ -1241,7 +1245,7 @@ namespace TheOtherRoles
                         RPCProcedure.sealVent(SecurityGuard.ventTarget.Id);
                         SecurityGuard.ventTarget = null;
                         
-                    } else if (GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1 && !SubmergedCompatibility.IsSubmerged) { // Place camera if there's no vent and it's not MiraHQ or Submerged
+                    } else if (!Helpers.isMira() && !Helpers.isFungle() && !SubmergedCompatibility.IsSubmerged) { // Place camera if there's no vent and it's not MiraHQ or Submerged
                         var pos = CachedPlayer.LocalPlayer.transform.position;
                         byte[] buff = new byte[sizeof(float) * 2];
                         Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0*sizeof(float), sizeof(float));
@@ -1257,12 +1261,12 @@ namespace TheOtherRoles
                 },
                 () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && SecurityGuard.remainingScrews >= Mathf.Min(SecurityGuard.ventPrice, SecurityGuard.camPrice); },
                 () => {
-                    securityGuardButton.actionButton.graphic.sprite = (SecurityGuard.ventTarget == null && GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1 && !SubmergedCompatibility.IsSubmerged) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite(); 
+                    securityGuardButton.actionButton.graphic.sprite = (SecurityGuard.ventTarget == null && !Helpers.isMira() && !Helpers.isFungle() && !SubmergedCompatibility.IsSubmerged) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite(); 
                     if (securityGuardButtonScrewsText != null) securityGuardButtonScrewsText.text = $"{SecurityGuard.remainingScrews}/{SecurityGuard.totalScrews}";
 
                     if (SecurityGuard.ventTarget != null)
                         return SecurityGuard.remainingScrews >= SecurityGuard.ventPrice && CachedPlayer.LocalPlayer.PlayerControl.CanMove;
-                    return GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1 && !SubmergedCompatibility.IsSubmerged && SecurityGuard.remainingScrews >= SecurityGuard.camPrice && CachedPlayer.LocalPlayer.PlayerControl.CanMove;
+                    return !Helpers.isMira() && !Helpers.isFungle() && !SubmergedCompatibility.IsSubmerged && SecurityGuard.remainingScrews >= SecurityGuard.camPrice && CachedPlayer.LocalPlayer.PlayerControl.CanMove;
                 },
                 () => { securityGuardButton.Timer = securityGuardButton.MaxTimer; },
                 SecurityGuard.getPlaceCameraButtonSprite(),
@@ -1280,12 +1284,12 @@ namespace TheOtherRoles
 
             securityGuardCamButton = new CustomButton(
                 () => {
-                    if (GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1) {
+                    if (!Helpers.isMira()) {
                         if (SecurityGuard.minigame == null) {
                             byte mapId = GameOptionsManager.Instance.currentNormalGameOptions.MapId;
-                            var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("Surv_Panel") || x.name.Contains("Cam"));
-                            if (mapId == 0 || mapId == 3) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("SurvConsole"));
-                            else if (mapId == 4) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("task_cams"));
+                            var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("Surv_Panel") || x.name.Contains("Cam") || x.name.Contains("BinocularsSecurityConsole"));
+                            if (Helpers.isSkeld() || mapId == 3) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("SurvConsole"));
+                            else if (Helpers.isAirship()) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("task_cams"));
                             if (e == null || Camera.main == null) return;
                             SecurityGuard.minigame = UnityEngine.Object.Instantiate(e.MinigamePrefab, Camera.main.transform, false);
                         }
@@ -1307,12 +1311,14 @@ namespace TheOtherRoles
                     if (SecurityGuard.cantMove) CachedPlayer.LocalPlayer.PlayerControl.moveable = false;
                     CachedPlayer.LocalPlayer.NetTransform.Halt(); // Stop current movement 
                 },
-                () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && SecurityGuard.remainingScrews < Mathf.Min(SecurityGuard.ventPrice, SecurityGuard.camPrice)
-                               && !SubmergedCompatibility.IsSubmerged; },
+                () => {
+                    return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead && SecurityGuard.remainingScrews < Mathf.Min(SecurityGuard.ventPrice, SecurityGuard.camPrice)
+                               && !SubmergedCompatibility.IsSubmerged;
+                },
                 () => {
                     if (securityGuardChargesText != null) securityGuardChargesText.text = $"{SecurityGuard.charges} / {SecurityGuard.maxCharges}";
-                    securityGuardCamButton.actionButton.graphic.sprite = GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? SecurityGuard.getLogSprite() : SecurityGuard.getCamSprite();
-                    securityGuardCamButton.actionButton.OverrideText(GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? "DOORLOG" : "SECURITY");
+                    securityGuardCamButton.actionButton.graphic.sprite = Helpers.isMira() ? SecurityGuard.getLogSprite() : SecurityGuard.getCamSprite();
+                    securityGuardCamButton.actionButton.OverrideText(Helpers.isMira() ? "DOORLOG" : "SECURITY");
                     return CachedPlayer.LocalPlayer.PlayerControl.CanMove && SecurityGuard.charges > 0;
                 },
                 () => {
@@ -1334,7 +1340,7 @@ namespace TheOtherRoles
                     CachedPlayer.LocalPlayer.PlayerControl.moveable = true;
                 },
                 false,
-                GameOptionsManager.Instance.currentNormalGameOptions.MapId == 1 ? "DOORLOG" : "SECURITY"
+                Helpers.isMira() ? "DOORLOG" : "SECURITY"
             );
 
             // Security Guard cam button charges
@@ -1683,7 +1689,7 @@ namespace TheOtherRoles
                 () => { return Ninja.ninja != null && Ninja.ninja == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead; },
                 () => {  // CouldUse
                     ninjaButton.Sprite = Ninja.ninjaMarked != null ? Ninja.getKillButtonSprite() : Ninja.getMarkButtonSprite(); 
-                    return (Ninja.currentTarget != null || Ninja.ninjaMarked != null) && CachedPlayer.LocalPlayer.PlayerControl.CanMove;
+                    return (Ninja.currentTarget != null || Ninja.ninjaMarked != null && !TransportationToolPatches.isUsingTransportation(Ninja.ninjaMarked)) && CachedPlayer.LocalPlayer.PlayerControl.CanMove;
                 },
                 () => {  // on meeting ends
                     ninjaButton.Timer = ninjaButton.MaxTimer;
@@ -2047,7 +2053,6 @@ namespace TheOtherRoles
                     // Prop stuff
                     var player = PlayerControl.LocalPlayer;
                     GameObject disguiseTarget = PropHunt.currentTarget;
-                    // PropHunt.FindClosestDisguiseObject(player.gameObject, 1, true);
                     if (disguiseTarget != null) {
                         player.transform.localScale = disguiseTarget.transform.lossyScale;
                         MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.SetProp, Hazel.SendOption.Reliable, -1);
@@ -2063,6 +2068,7 @@ namespace TheOtherRoles
                 () => { return PropHunt.isPropHuntGM && !PlayerControl.LocalPlayer.Data.Role.IsImpostor && !PlayerControl.LocalPlayer.Data.IsDead; },
                 () => {
                     propSpriteRenderer.sprite = PropHunt.currentTarget?.GetComponent<SpriteRenderer>()?.sprite;
+                    if (propSpriteRenderer.sprite == null) propSpriteRenderer.sprite = PropHunt.currentTarget?.transform.GetComponentInChildren<SpriteRenderer>()?.sprite;
                     if (propSpriteRenderer.sprite != null)
                         propSpriteHolder.transform.localScale *= 1 / propSpriteRenderer.bounds.size.magnitude;
                     return PropHunt.currentTarget != null && PropHunt.currentTarget?.GetComponent<SpriteRenderer>()?.sprite != null;
index bdbd39d47514f470bcdc5e60874a6a36cf2c6434..caa7754a7eaa57fa11c84348108d7bbb9b401392 100644 (file)
@@ -122,6 +122,21 @@ namespace TheOtherRoles.CustomGameModes {
             return Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.IntroAnimation.intro_{index + 1000}.png", 150f, cache: false);
         }
 
+        public static void updateWhitelistedObjects() {
+            TheOtherRolesPlugin.Logger.LogMessage($"updating whitelisted objects!");
+            string allNames = Helpers.readTextFromResources("TheOtherRoles.Resources.Txt.props.txt");
+            TheOtherRolesPlugin.Logger.LogMessage($"raed from res");
+            bool debug = false;
+            if (debug) {
+                allNames = Helpers.readTextFromFile(System.IO.Directory.GetCurrentDirectory() + "\\props.txt"); 
+            }
+            TheOtherRolesPlugin.Logger.LogMessage($"after debug");
+            whitelistedObjects = allNames.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList();
+            TheOtherRolesPlugin.Logger.LogMessage($"after split");
+            
+            TheOtherRolesPlugin.Logger.LogMessage($"Last element: {whitelistedObjects.Last()}");
+        }
+
 
         public static void propTargetAndTimerDisplayUpdate() {
             
@@ -344,9 +359,8 @@ namespace TheOtherRoles.CustomGameModes {
             try {
                 Collider2D bestCollider = null;
                 float bestDist = 9999;
-                if (whitelistedObjects == null || whitelistedObjects.Count == 0) {
-                    string allNames = Helpers.readTextFromResources("TheOtherRoles.Resources.Txt.Props.txt");
-                    whitelistedObjects = allNames.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList();
+                if (whitelistedObjects == null || whitelistedObjects.Count == 0 || verbose) {
+                    updateWhitelistedObjects();
                 }
                 foreach (Collider2D collider in Physics2D.OverlapCircleAll(origin.transform.position, radius)) {
                     if (verbose) {
@@ -444,7 +458,7 @@ namespace TheOtherRoles.CustomGameModes {
 
                 __instance.gameObject.AddComponent<SpriteRenderer>();
                 __instance.GetComponent<CircleCollider2D>().radius = 0.00001f;
-            }
+        }
 
 
         // Runs periodically, resets animation data for players
index 2d78be4a9c9aa2aaaf919865a2b8578a1e6df8fa..876f718c14e1d1ffd2ae7e935cbd227c0bfa4878 100644 (file)
@@ -315,6 +315,7 @@ namespace TheOtherRoles {
         public static CustomOption dynamicMapEnableMira;
         public static CustomOption dynamicMapEnablePolus;
         public static CustomOption dynamicMapEnableAirShip;
+        public static CustomOption dynamicMapEnableFungle;
         public static CustomOption dynamicMapEnableSubmerged;
         public static CustomOption dynamicMapSeparateSettings;
 
@@ -631,7 +632,7 @@ namespace TheOtherRoles {
             securityGuardCamPrice = CustomOption.Create(283, Types.Crewmate, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate);
             securityGuardVentPrice = CustomOption.Create(284, Types.Crewmate, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate);
             securityGuardCamDuration = CustomOption.Create(285, Types.Crewmate, "Security Guard Duration", 10f, 2.5f, 60f, 2.5f, securityGuardSpawnRate);
-            securityGuardCamMaxCharges = CustomOption.Create(286, Types.Crewmate, "Gadged Max Charges", 5f, 1f, 30f, 1f, securityGuardSpawnRate);
+            securityGuardCamMaxCharges = CustomOption.Create(286, Types.Crewmate, "Gadget Max Charges", 5f, 1f, 30f, 1f, securityGuardSpawnRate);
             securityGuardCamRechargeTasksNumber = CustomOption.Create(287, Types.Crewmate, "Number Of Tasks Needed For Recharging", 3f, 1f, 10f, 1f, securityGuardSpawnRate);
             securityGuardNoMove = CustomOption.Create(288, Types.Crewmate, "Cant Move During Cam Duration", true, securityGuardSpawnRate);
 
@@ -718,7 +719,7 @@ namespace TheOtherRoles {
             guesserGamemodeCantGuessSnitchIfTaksDone = CustomOption.Create(2010, Types.Guesser, "Guesser Can't Guess Snitch When Tasks Completed", true, null);
 
             // Hide N Seek Gamemode (3000 - 3999)
-            hideNSeekMap = CustomOption.Create(3020, Types.HideNSeekMain, cs(Color.yellow, "Map"), new string[] { "The Skeld", "Mira", "Polus", "Airship", "Submerged", "LI Map"}, null, true, onChange: () => { int map = hideNSeekMap.selection; if (map >= 3) map++; GameOptionsManager.Instance.currentNormalGameOptions.MapId = (byte)map; });
+            hideNSeekMap = CustomOption.Create(3020, Types.HideNSeekMain, cs(Color.yellow, "Map"), new string[] { "The Skeld", "Mira", "Polus", "Airship", "Fungle", "Submerged", "LI Map"}, null, true, onChange: () => { int map = hideNSeekMap.selection; if (map >= 3) map++; GameOptionsManager.Instance.currentNormalGameOptions.MapId = (byte)map; });
             hideNSeekHunterCount = CustomOption.Create(3000, Types.HideNSeekMain, cs(Color.yellow, "Number Of Hunters"), 1f, 1f, 3f, 1f);
             hideNSeekKillCooldown = CustomOption.Create(3021, Types.HideNSeekMain, cs(Color.yellow, "Kill Cooldown"), 10f, 2.5f, 60f, 2.5f);
             hideNSeekHunterVision = CustomOption.Create(3001, Types.HideNSeekMain, cs(Color.yellow, "Hunter Vision"), 0.5f, 0.25f, 2f, 0.25f);
@@ -749,7 +750,7 @@ namespace TheOtherRoles {
             huntedShieldNumber = CustomOption.Create(3026, Types.HideNSeekRoles, cs(Color.gray, "Hunted Shield Number"), 3f, 1f, 15f, 1f);
 
             // Prop Hunt General Options
-            propHuntMap = CustomOption.Create(4020, Types.PropHunt, cs(Color.yellow, "Map"), new string[] { "The Skeld", "Mira", "Polus", "Airship", "Submerged", "LI Map"}, null, true, onChange: ()=> { int map = propHuntMap.selection; if (map >= 3) map++; GameOptionsManager.Instance.currentNormalGameOptions.MapId = (byte)map;});
+            propHuntMap = CustomOption.Create(4020, Types.PropHunt, cs(Color.yellow, "Map"), new string[] { "The Skeld", "Mira", "Polus", "Airship", "Fungle", "Submerged", "LI Map"}, null, true, onChange: ()=> { int map = propHuntMap.selection; if (map >= 3) map++; GameOptionsManager.Instance.currentNormalGameOptions.MapId = (byte)map;});
             propHuntTimer = CustomOption.Create(4021, Types.PropHunt, cs(Color.yellow, "Timer In Min"), 5f, 1f, 30f, 0.5f);
             propHuntUnstuckCooldown = CustomOption.Create(4011, Types.PropHunt, cs(Color.yellow, "Unstuck Cooldown"), 30f, 2.5f, 60f, 2.5f);
             propHuntUnstuckDuration = CustomOption.Create(4012, Types.PropHunt, cs(Color.yellow, "Unstuck Duration"), 2f, 1f, 60f, 1f);
@@ -795,6 +796,7 @@ namespace TheOtherRoles {
             dynamicMapEnableMira = CustomOption.Create(502, Types.General, "Mira", rates, dynamicMap, false);
             dynamicMapEnablePolus = CustomOption.Create(503, Types.General, "Polus", rates, dynamicMap, false);
             dynamicMapEnableAirShip = CustomOption.Create(504, Types.General, "Airship", rates, dynamicMap, false);
+            dynamicMapEnableFungle = CustomOption.Create(506, Types.General, "Fungle", rates, dynamicMap, false);
             dynamicMapEnableSubmerged = CustomOption.Create(505, Types.General, "Submerged", rates, dynamicMap, false);
             dynamicMapSeparateSettings = CustomOption.Create(509, Types.General, "Use Random Map Setting Presets", false, dynamicMap, false);
 
index f333bf3dbfe899fff0e2259b412cdfcaa98b8bad..d9dc79cb98af9123f88071d91a3ef0c93148dfa8 100644 (file)
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Reflection;
 using UnityEngine;
@@ -11,11 +12,10 @@ using Hazel;
 using TheOtherRoles.Players;
 using TheOtherRoles.Utilities;
 using System.Threading.Tasks;
-using System.Net;
 using TheOtherRoles.CustomGameModes;
 using Reactor.Utilities.Extensions;
 using AmongUs.GameOptions;
-using Innersloth.Assets;
+using TheOtherRoles.Patches;
 
 namespace TheOtherRoles {
 
@@ -23,6 +23,7 @@ namespace TheOtherRoles {
         PerformKill,
         SuppressKill,
         BlankKill,
+        DelayVampireKill
     }
 
     public enum CustomGamemodes {
@@ -85,7 +86,7 @@ namespace TheOtherRoles {
         }
 
         public static AudioClip loadAudioClipFromResources(string path, string clipName = "UNNAMED_TOR_AUDIO_CLIP") {
-            // must be "raw (headerless) 2-channel signed 32 bit pcm (le)" (can e.g. use Audacity® to export)
+            // must be "raw (headerless) 2-channel signed 32 bit pcm (le)" (can e.g. use Audacity® to export)
             try {
                 Assembly assembly = Assembly.GetExecutingAssembly();
                 Stream stream = assembly.GetManifestResourceStream(path);
@@ -121,6 +122,12 @@ namespace TheOtherRoles {
             return textStreamReader.ReadToEnd();
         }
 
+        public static string readTextFromFile(string path) {
+            Stream stream = File.OpenRead(path);
+            StreamReader textStreamReader = new StreamReader(stream);
+            return textStreamReader.ReadToEnd();
+        }
+
         public static PlayerControl playerById(byte id)
         {
             foreach (PlayerControl player in CachedPlayer.AllPlayers)
@@ -238,6 +245,38 @@ namespace TheOtherRoles {
                 player.Data.Tasks.Clear();
         }
 
+        public static void MurderPlayer(this PlayerControl player, PlayerControl target)
+        {
+            player.MurderPlayer(target, MurderResultFlags.Succeeded);
+        }
+
+        public static void RpcRepairSystem(this ShipStatus shipStatus, SystemTypes systemType, byte amount)
+        {
+            shipStatus.RpcUpdateSystem(systemType, amount);
+        }
+
+        public static bool isMira() {
+            return GameOptionsManager.Instance.CurrentGameOptions.MapId == 1;
+        }
+
+        public static bool isAirship() {
+            return GameOptionsManager.Instance.CurrentGameOptions.MapId == 4;
+        }
+        public static bool isSkeld() {
+            return GameOptionsManager.Instance.CurrentGameOptions.MapId == 0;
+        }
+        public static bool isPolus() {
+            return GameOptionsManager.Instance.CurrentGameOptions.MapId == 2;
+        }
+
+        public static bool isFungle() {           
+            return GameOptionsManager.Instance.CurrentGameOptions.MapId == 5;
+        }
+
+        public static bool MushroomSabotageActive() {
+            return CachedPlayer.LocalPlayer.PlayerControl.myTasks.ToArray().Any((x) => x.TaskType == TaskTypes.MushroomMixupSabotage);
+        }
+
         public static void setSemiTransparent(this PoolablePlayer player, bool value, float alpha=0.25f) {
             alpha = value ? alpha : 1f;
             foreach (SpriteRenderer r in player.gameObject.GetComponentsInChildren<SpriteRenderer>())
@@ -281,7 +320,7 @@ namespace TheOtherRoles {
         }
 
         public static bool hidePlayerName(PlayerControl source, PlayerControl target) {
-            if (Camouflager.camouflageTimer > 0f) return true; // No names are visible
+            if (Camouflager.camouflageTimer > 0f || Helpers.MushroomSabotageActive()) return true; // No names are visible
             if (Patches.SurveillanceMinigamePatch.nightVisionIsActive) return true;
             else if (Ninja.isInvisble && Ninja.ninja == target) return true;
             else if (!TORMapOptions.hidePlayerNames) return false; // All names are visible
@@ -295,7 +334,13 @@ namespace TheOtherRoles {
         }
 
         public static void setDefaultLook(this PlayerControl target, bool enforceNightVisionUpdate = true) {
-            target.setLook(target.Data.PlayerName, target.Data.DefaultOutfit.ColorId, target.Data.DefaultOutfit.HatId, target.Data.DefaultOutfit.VisorId, target.Data.DefaultOutfit.SkinId, target.Data.DefaultOutfit.PetId, enforceNightVisionUpdate);
+            if (Helpers.MushroomSabotageActive()) {
+                var instance = ShipStatus.Instance.CastFast<FungleShipStatus>().specialSabotage;
+                MushroomMixupSabotageSystem.CondensedOutfit condensedOutfit = instance.currentMixups[target.PlayerId];
+                GameData.PlayerOutfit playerOutfit = instance.ConvertToPlayerOutfit(condensedOutfit);
+                target.MixUpOutfit(playerOutfit);
+            } else
+                target.setLook(target.Data.PlayerName, target.Data.DefaultOutfit.ColorId, target.Data.DefaultOutfit.HatId, target.Data.DefaultOutfit.VisorId, target.Data.DefaultOutfit.SkinId, target.Data.DefaultOutfit.PetId, enforceNightVisionUpdate);
         }
 
         public static void setLook(this PlayerControl target, String playerName, int colorId, string hatId, string visorId, string skinId, string petId, bool enforceNightVisionUpdate = true) {
@@ -447,22 +492,40 @@ namespace TheOtherRoles {
 
                 return MurderAttemptResult.SuppressKill;
             }
-
+            if (TransportationToolPatches.isUsingTransportation(target) && !blockRewind && killer == Vampire.vampire) {
+                return MurderAttemptResult.DelayVampireKill;
+            } else if (TransportationToolPatches.isUsingTransportation(target))
+                return MurderAttemptResult.SuppressKill;
             return MurderAttemptResult.PerformKill;
         }
 
+        public static void MurderPlayer(PlayerControl killer, PlayerControl target, bool showAnimation) {
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
+            writer.Write(killer.PlayerId);
+            writer.Write(target.PlayerId);
+            writer.Write(showAnimation ? Byte.MaxValue : 0);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            RPCProcedure.uncheckedMurderPlayer(killer.PlayerId, target.PlayerId, showAnimation ? Byte.MaxValue : (byte)0);
+        }
+
         public static MurderAttemptResult checkMurderAttemptAndKill(PlayerControl killer, PlayerControl target, bool isMeetingStart = false, bool showAnimation = true, bool ignoreBlank = false, bool ignoreIfKillerIsDead = false)  {
             // The local player checks for the validity of the kill and performs it afterwards (different to vanilla, where the host performs all the checks)
             // The kill attempt will be shared using a custom RPC, hence combining modded and unmodded versions is impossible
             MurderAttemptResult murder = checkMuderAttempt(killer, target, isMeetingStart, ignoreBlank, ignoreIfKillerIsDead);
 
             if (murder == MurderAttemptResult.PerformKill) {
-                MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
-                writer.Write(killer.PlayerId);
-                writer.Write(target.PlayerId);
-                writer.Write(showAnimation ? Byte.MaxValue : 0);
-                AmongUsClient.Instance.FinishRpcImmediately(writer);
-                RPCProcedure.uncheckedMurderPlayer(killer.PlayerId, target.PlayerId, showAnimation ? Byte.MaxValue : (byte)0);
+                MurderPlayer(killer, target, showAnimation);
+            } else if (murder == MurderAttemptResult.DelayVampireKill) {
+                HudManager.Instance.StartCoroutine(Effects.Lerp(10f, new Action<float>((p) => { 
+                    if (!TransportationToolPatches.isUsingTransportation(target) && Vampire.bitten != null) {
+                        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.VampireSetBitten, Hazel.SendOption.Reliable, -1);
+                        writer.Write(byte.MaxValue);
+                        writer.Write(byte.MaxValue);
+                        AmongUsClient.Instance.FinishRpcImmediately(writer);
+                        RPCProcedure.vampireSetBitten(byte.MaxValue, byte.MaxValue);
+                        MurderPlayer(killer, target, showAnimation);
+                    }
+                })));
             }
             return murder;            
         }
@@ -530,10 +593,24 @@ namespace TheOtherRoles {
             ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); // This will move button positions to the correct position.
         }
 
+        private static long GetBuiltInTicks()
+        {
+            var assembly = Assembly.GetExecutingAssembly();
+            var builtin = assembly.GetType("Builtin");
+            if (builtin == null) return 0;
+            var field = builtin.GetField("CompileTime");
+            if (field == null) return 0;
+            var value = field.GetValue(null);
+            if (value == null) return 0;
+            return (long)value;
+        }
+
         public static async Task checkBeta() {
             if (TheOtherRolesPlugin.betaDays > 0) {
                 TheOtherRolesPlugin.Logger.LogMessage($"Beta check");
-                var compileTime = new DateTime(Builtin.CompileTime, DateTimeKind.Utc);  // This may show as an error, but it is not, compilation will work!
+                var ticks = GetBuiltInTicks();
+                var compileTime = new DateTime(ticks, DateTimeKind.Utc);  // This may show as an error, but it is not, compilation will work!
+                TheOtherRolesPlugin.Logger.LogMessage($"Compiled at {compileTime.ToString(CultureInfo.InvariantCulture)}");
                 DateTime? now;
                 // Get time from the internet, so no-one can cheat it (so easily).
                 try {
index 11a1b2f9ea9a5ee902dda62f40444391d41ac001..b6e7178db2bb3ddc2a1d8ab71af1ba87cf8ccbcf 100644 (file)
@@ -16,11 +16,12 @@ using UnityEngine;
 using TheOtherRoles.Modules;
 using TheOtherRoles.Players;
 using TheOtherRoles.Utilities;
-using Reactor;
 using Il2CppSystem.Security.Cryptography;
 using Il2CppSystem.Text;
 using Reactor.Networking.Attributes;
 using AmongUs.Data;
+using TheOtherRoles.Modules.CustomHats;
+using static TheOtherRoles.Modules.ModUpdater;
 
 namespace TheOtherRoles
 {
@@ -28,10 +29,11 @@ namespace TheOtherRoles
     [BepInDependency(SubmergedCompatibility.SUBMERGED_GUID, BepInDependency.DependencyFlags.SoftDependency)]
     [BepInProcess("Among Us.exe")]
     [ReactorModFlags(Reactor.Networking.ModFlags.RequireOnAllClients)]
+    
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string VersionString = "4.4.2";
+        public const string VersionString = "4.5.0";
         public static uint betaDays = 0;  // amount of days for the build to be usable (0 for infinite!)
 
         public static Version Version = Version.Parse(VersionString);
@@ -51,6 +53,7 @@ namespace TheOtherRoles
         public static ConfigEntry<bool> ShowLighterDarker { get; set; }
         public static ConfigEntry<bool> EnableSoundEffects { get; set; }
         public static ConfigEntry<bool> EnableHorseMode { get; set; }
+        public static ConfigEntry<bool> ShowVentsOnMap { get; set; }
         public static ConfigEntry<string> Ip { get; set; }
         public static ConfigEntry<ushort> Port { get; set; }
         public static ConfigEntry<string> ShowPopUpVersion { get; set; }
@@ -104,6 +107,7 @@ namespace TheOtherRoles
             EnableSoundEffects = Config.Bind("Custom", "Enable Sound Effects", true);
             EnableHorseMode = Config.Bind("Custom", "Enable Horse Mode", false);
             ShowPopUpVersion = Config.Bind("Custom", "Show PopUp", "0");
+            ShowVentsOnMap = Config.Bind("Custom", "Show vent positions on minimap", false);
             
             Ip = Config.Bind("Custom", "Custom Server IP", "127.0.0.1");
             Port = Config.Bind("Custom", "Custom Server Port", (ushort)22023);
@@ -116,23 +120,22 @@ namespace TheOtherRoles
             
             CustomOptionHolder.Load();
             CustomColors.Load();
+            CustomHatManager.LoadHats();
             if (BepInExUpdater.UpdateRequired)
             {
                 AddComponent<BepInExUpdater>();
                 return;
             }
 
+            AddComponent<ModUpdater>();
+
             EventUtility.Load();
             SubmergedCompatibility.Initialize();
-            AddComponent<ModUpdateBehaviour>();
-            Modules.MainMenuPatch.addSceneChangeCallbacks();
+            MainMenuPatch.addSceneChangeCallbacks();
             _ = RoleInfo.loadReadme();
+            AddToKillDistanceSetting.addKillDistance();
             TheOtherRolesPlugin.Logger.LogInfo("Loading TOR completed!");
         }
-        public static Sprite GetModStamp() {
-            if (ModStamp) return ModStamp;
-            return ModStamp = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.ModStamp.png", 150f);
-        }
     }
 
     // Deactivate bans, since I always leave my local testing game and ban myself
index 79994947c429132b34c48824832b669b67cbf445..83cce2d659d976e8565c724cf354e5a2d0a69c13 100644 (file)
@@ -18,6 +18,7 @@ namespace TheOtherRoles{
         public static bool enableSoundEffects = true;
         public static bool enableHorseMode = false;
         public static bool shieldFirstKill = false;
+        public static bool ShowVentsOnMap = true;
         public static CustomGamemodes gameMode = CustomGamemodes.Classic;
 
         // Updating values
@@ -52,6 +53,8 @@ namespace TheOtherRoles{
             showLighterDarker = TheOtherRolesPlugin.ShowLighterDarker.Value;
             enableSoundEffects = TheOtherRolesPlugin.EnableSoundEffects.Value;
             enableHorseMode = TheOtherRolesPlugin.EnableHorseMode.Value;
+            ShowVentsOnMap = TheOtherRolesPlugin.ShowVentsOnMap.Value;
+            
             //Patches.ShouldAlwaysHorseAround.isHorseMode = TheOtherRolesPlugin.EnableHorseMode.Value;
         }
     }
index d33f863cfad131a1a40e8a941257cb6702aae12f..4b0c2b6c2c719958f0104157fd5165f33140b12c 100644 (file)
@@ -3,6 +3,7 @@ using HarmonyLib;
 using System.Linq;
 using TheOtherRoles.Players;
 using TheOtherRoles.Utilities;
+using Hazel;
 
 namespace TheOtherRoles.Modules {
     [HarmonyPatch]
@@ -35,6 +36,28 @@ namespace TheOtherRoles.Modules {
                                 handled = true;
                             }
                         }
+                    } else if (text.ToLower().StartsWith("/gm")) {
+                        string gm = text.Substring(4).ToLower();
+                        CustomGamemodes gameMode = CustomGamemodes.Classic;
+                        if (gm.StartsWith("prop") || gm.StartsWith("ph")) {
+                            gameMode = CustomGamemodes.PropHunt;
+                        } else if (gm.StartsWith("guess") || gm.StartsWith("gm")) {
+                            gameMode = CustomGamemodes.Guesser;
+                        } else if (gm.StartsWith("hide") || gm.StartsWith("hn")) {
+                            gameMode = CustomGamemodes.HideNSeek;
+                        }
+                        // else its classic!
+
+                        if (AmongUsClient.Instance.AmHost) {
+                            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.ShareGamemode, Hazel.SendOption.Reliable, -1);
+                            writer.Write((byte)TORMapOptions.gameMode);
+                            AmongUsClient.Instance.FinishRpcImmediately(writer);
+                            RPCProcedure.shareGamemode((byte)gameMode);
+                            RPCProcedure.shareGamemode((byte)TORMapOptions.gameMode);
+                        } else {
+                            __instance.AddChat(CachedPlayer.LocalPlayer.PlayerControl, "Nice try, but you have to be the host to use this feature");
+                        }
+                        handled = true;
                     }
                 }
                 
index 9653b01e6bb4760ce2a5ccd739d22cf4f2e1894d..495af2413b144e2dc1989b4e3bf5f650455d6c78 100644 (file)
@@ -143,14 +143,10 @@ namespace TheOtherRoles.Modules {
                     
             /** Add Colors **/
             int id = 50000;
-            foreach (var c in Palette.PlayerColors) {
-                TheOtherRolesPlugin.Logger.LogMessage(c);
-            }
             foreach (CustomColor cc in colors) {
                 longlist.Add((StringNames)id);
                 CustomColors.ColorStrings[id++] = cc.longname;
                 colorlist.Add(cc.color);
-                TheOtherRolesPlugin.Logger.LogMessage(cc.color);
                 shadowlist.Add(cc.shadow);
                 if (cc.isLighterColor)
                     lighterColors.Add(colorlist.Count - 1);
diff --git a/TheOtherRoles/Modules/CustomHats.cs b/TheOtherRoles/Modules/CustomHats.cs
deleted file mode 100644 (file)
index a7ff4de..0000000
+++ /dev/null
@@ -1,830 +0,0 @@
-using HarmonyLib;
-using UnityEngine;
-using System.IO;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-using System.Security.Cryptography;
-using Newtonsoft.Json.Linq;
-using TheOtherRoles.Players;
-using TheOtherRoles.Utilities;
-using AmongUs.Data;
-using AmongUs.Data.Legacy;
-using System.Reflection;
-using static Rewired.Controller;
-using static TheOtherRoles.Modules.CustomHatLoader;
-using Innersloth.Assets;
-using UnityEngine.AddressableAssets;
-using PowerTools;
-using System.Drawing;
-
-namespace TheOtherRoles.Modules {
-    [HarmonyPatch]
-    public class CustomHats { 
-        private static bool LOADED = false;
-        private static bool RUNNING = false;
-        public static Material hatShader;
-
-        public static Dictionary<string, HatExtension> CustomHatRegistry = new Dictionary<string, HatExtension>();
-        public static Dictionary<string, HatViewData> CustomHatViewDatas = new Dictionary<string, HatViewData>();
-        public static HatExtension TestExt = null;
-
-        public class HatExtension {
-            public string author { get; set;}
-            public string package { get; set;}
-            public string condition { get; set;}
-            public Sprite FlipImage { get; set;}
-            public Sprite BackFlipImage { get; set;}
-        }
-
-        public class CustomHat { 
-            public string author { get; set;}
-            public string package { get; set;}
-            public string condition { get; set;}
-            public string name { get; set;}
-            public string resource { get; set;}
-            public string flipresource { get; set;}
-            public string backflipresource { get; set;}
-            public string backresource { get; set;}
-            public string climbresource { get; set;}
-            public bool bounce { get; set;}
-            public bool adaptive { get; set;}
-            public bool behind { get; set;}
-        }
-
-        private static List<CustomHat> createCustomHatDetails(string[] hats, bool fromDisk = false) {
-            Dictionary<string, CustomHat> fronts = new Dictionary<string, CustomHat>();
-            Dictionary<string, string> backs = new Dictionary<string, string>();
-            Dictionary<string, string> flips = new Dictionary<string, string>();
-            Dictionary<string, string> backflips = new Dictionary<string, string>();
-            Dictionary<string, string> climbs = new Dictionary<string, string>();
-
-            for (int i = 0; i < hats.Length; i++) {
-                string s = fromDisk ? hats[i].Substring(hats[i].LastIndexOf("\\") + 1).Split('.')[0] : hats[i].Split('.')[3];
-                string[] p = s.Split('_');
-
-                HashSet<string> options = new HashSet<string>();
-                for (int j = 1; j < p.Length; j++) 
-                    options.Add(p[j]);
-
-                if (options.Contains("back") && options.Contains("flip"))
-                    backflips.Add(p[0], hats[i]);
-                else if (options.Contains("climb")) 
-                    climbs.Add(p[0], hats[i]);
-                else if (options.Contains("back"))
-                    backs.Add(p[0], hats[i]);
-                else if (options.Contains("flip"))
-                    flips.Add(p[0], hats[i]);
-                else {
-                    CustomHat custom = new CustomHat { resource = hats[i] };
-                    custom.name = p[0].Replace('-', ' ');
-                    custom.bounce = options.Contains("bounce");
-                    custom.adaptive = options.Contains("adaptive");
-                    custom.behind = options.Contains("behind");
-                    
-                    fronts.Add(p[0], custom);
-                }
-            }
-
-            List<CustomHat> customhats = new List<CustomHat>();
-
-            foreach (string k in fronts.Keys) {
-                CustomHat hat = fronts[k];
-                string br, cr, fr, bfr;
-                backs.TryGetValue(k, out br);
-                climbs.TryGetValue(k, out cr);
-                flips.TryGetValue(k, out fr);
-                backflips.TryGetValue(k, out bfr);
-                if (br != null)
-                    hat.backresource = br;
-                if (cr != null)
-                    hat.climbresource = cr;
-                if (fr != null)
-                    hat.flipresource = fr;
-                if (bfr != null)
-                    hat.backflipresource = bfr;
-                if (hat.backresource != null)
-                    hat.behind = true;
-
-                customhats.Add(hat);
-            }
-
-            return customhats;
-        }
-
-        private static Sprite CreateHatSprite(string path, bool fromDisk = false) {
-            Texture2D texture = fromDisk ? Helpers.loadTextureFromDisk(path) : Helpers.loadTextureFromResources(path);
-            if (texture == null)
-                return null;
-            Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.53f, 0.575f), texture.width * 0.375f);
-            if (sprite == null)
-                return null;
-            texture.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset;
-            sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset;
-
-            return sprite;
-
-        }
-
-        private static HatData CreateHatBehaviour(CustomHat ch, bool fromDisk = false, bool testOnly = false) {
-            if (hatShader == null) {
-                Material tmpShader = FastDestroyableSingleton<HatManager>.Instance.PlayerMaterial;
-                hatShader = tmpShader;
-            }
-            var viewdata = ScriptableObject.CreateInstance<HatViewData>();
-            HatData hat = ScriptableObject.CreateInstance<HatData>();
-
-            viewdata.MainImage = CreateHatSprite(ch.resource, fromDisk);
-            viewdata.FloorImage = viewdata.MainImage;
-            if (ch.backresource != null) {
-                viewdata.BackImage = CreateHatSprite(ch.backresource, fromDisk);
-                ch.behind = true; // Required to view backresource
-            }
-            if (ch.climbresource != null) {
-                viewdata.ClimbImage = CreateHatSprite(ch.climbresource, fromDisk);
-                viewdata.LeftClimbImage = viewdata.ClimbImage;
-            }
-            hat.name = ch.name;
-            hat.displayOrder = 99;
-            hat.ProductId = "hat_" + ch.name.Replace(' ', '_');
-            hat.InFront = !ch.behind;
-            hat.NoBounce = !ch.bounce;
-            hat.ChipOffset = new Vector2(0f, 0.2f);
-            hat.Free = true;
-
-            if (ch.adaptive && hatShader != null)
-                viewdata.AltShader = hatShader;
-
-            HatExtension extend = new HatExtension();
-            extend.author = ch.author != null ? ch.author : "Unknown";
-            extend.package = ch.package != null ? ch.package : "Misc.";
-            extend.condition = ch.condition != null ? ch.condition : "none";
-
-            if (ch.flipresource != null)
-                extend.FlipImage = CreateHatSprite(ch.flipresource, fromDisk);
-            if (ch.backflipresource != null)
-                 extend.BackFlipImage = CreateHatSprite(ch.backflipresource, fromDisk);
-
-            if (testOnly) {
-                TestExt = extend;
-                TestExt.condition = hat.name;
-            } else {
-                CustomHatRegistry.Add(hat.name, extend);
-            }
-            CustomHatViewDatas.TryAdd(hat.name, viewdata);
-            var assetRef = new AssetReference(viewdata.Pointer);
-
-            hat.ViewDataRef = assetRef;
-            hat.CreateAddressableAsset();
-            return hat;
-        }
-
-        private static HatData CreateHatBehaviour(CustomHatLoader.CustomHatOnline chd, bool fromDisk = true) {
-            if (fromDisk) {
-                string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\";
-                chd.resource = filePath + chd.resource;
-                if (chd.backresource != null)
-                    chd.backresource = filePath + chd.backresource;
-                if (chd.climbresource != null)
-                    chd.climbresource = filePath + chd.climbresource;
-                if (chd.flipresource != null)
-                    chd.flipresource = filePath + chd.flipresource;
-                if (chd.backflipresource != null)
-                    chd.backflipresource = filePath + chd.backflipresource;
-            }
-
-            return CreateHatBehaviour(chd, fromDisk, false);
-        }
-
-        [HarmonyPatch(typeof(HatManager), nameof(HatManager.GetHatById))]
-        private static class HatManagerPatch {
-            private static List<HatData> allHatsList;
-            static void Prefix(HatManager __instance) {
-                if (RUNNING || LOADED) return;
-                RUNNING = true; // prevent simultanious execution
-                allHatsList = __instance.allHats.ToList();
-
-                try {
-                    while (CustomHatLoader.hatdetails.Count > 0) {
-                        bool fromDisk = !(CustomHatLoader.hatdetails[0].name.Contains("HorseHat_") && (EventUtility.canBeEnabled || EventUtility.isEnabled));
-                        allHatsList.Add(CreateHatBehaviour(CustomHatLoader.hatdetails[0], fromDisk));
-                        CustomHatLoader.hatdetails.RemoveAt(0);
-                    }
-                    __instance.allHats = allHatsList.ToArray();
-                    LOADED = true;  // this will only be set to true if loading is succesful.
-                } catch (System.Exception e) {
-                    if (!LOADED)
-                        TheOtherRolesPlugin.Logger.LogMessage("Unable to add Custom Hats\n" + e);
-                }
-            }
-            static void Postfix(HatManager __instance) {
-                RUNNING = false;
-            }
-        }
-
-        [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))]
-        private static class PlayerPhysicsHandleAnimationPatch {
-            private static void Postfix(PlayerPhysics __instance) {
-                if (!CustomHatViewDatas.ContainsKey(__instance.myPlayer.cosmetics.hat.Hat.name)) return;
-                HatViewData viewData = CustomHatViewDatas[__instance.myPlayer.cosmetics.hat.Hat.name];
-                AnimationClip currentAnimation = __instance.Animations.Animator.GetCurrentAnimation();
-                if (currentAnimation == __instance.Animations.group.ClimbUpAnim || currentAnimation == __instance.Animations.group.ClimbDownAnim) return;
-                HatParent hp = __instance.myPlayer.cosmetics.hat;
-                if (hp == null || hp.Hat == null) return;
-                HatExtension extend = hp.Hat.getHatExtension();
-                if (extend == null) return;
-                if (extend.FlipImage != null) {
-                    if (__instance.FlipX) {
-                        hp.FrontLayer.sprite = extend.FlipImage;
-                    } else {
-                        hp.FrontLayer.sprite = viewData.MainImage;
-                    }
-                }
-                if (extend.BackFlipImage != null) {
-                    if (__instance.FlipX) {
-                        hp.BackLayer.sprite = extend.BackFlipImage;
-                    } else {
-                        hp.BackLayer.sprite = viewData.BackImage;
-                    }
-                }
-            }
-        }
-
-        [HarmonyPatch]
-        private static class FreeplayHatTestingPatches
-        {
-            [HarmonyPriority(Priority.High)]
-            [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), typeof(int))]
-            private static class HatParentSetHatPatchColor {
-                static void Prefix(HatParent __instance) {
-                    if (DestroyableSingleton<TutorialManager>.InstanceExists) {
-                        try {
-                            string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\Test";
-                            if (!Directory.Exists(filePath)) Directory.CreateDirectory(filePath);
-                            DirectoryInfo d = new DirectoryInfo(filePath);
-                            string[] filePaths = d.GetFiles("*.png").Select(x => x.FullName).ToArray(); // Getting Text files
-                            List<CustomHat> hats = createCustomHatDetails(filePaths, true);
-                            if (hats.Count > 0) {
-                                __instance.Hat = CreateHatBehaviour(hats[0], true, true);
-                            }
-                        } catch (System.Exception e) {
-                            System.Console.WriteLine("Unable to create test hat\n" + e);
-                        }
-                    }
-                }     
-            }
-            
-            [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), typeof(HatData), typeof(int))]
-            private static class HatParentSetHatPatchExtra {
-                static bool Prefix(HatParent __instance, HatData hat, int color)
-                {
-                    if (!DestroyableSingleton<TutorialManager>.InstanceExists) return true;
-                    
-                    try 
-                    {
-                        __instance.Hat = hat;
-                        __instance.hatDataAsset = __instance.Hat.CreateAddressableAsset();
-
-                        string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\Test";
-                        if (!Directory.Exists(filePath)) return true;
-                        DirectoryInfo d = new DirectoryInfo(filePath);
-                        string[] filePaths = d.GetFiles("*.png").Select(x => x.FullName).ToArray(); // Getting Test files
-                        List<CustomHat> hats = createCustomHatDetails(filePaths, true);
-                        if (hats.Count > 0) 
-                        {
-                            __instance.Hat = CreateHatBehaviour(hats[0], true, true);
-                            __instance.Hat.CreateAddressableAsset();
-                        }
-                    } 
-                    catch (System.Exception e) 
-                    {
-                        System.Console.WriteLine("Unable to create test hat\n" + e);
-                        return true;
-                    }
-                    
-                    __instance.PopulateFromHatViewData();
-                    __instance.SetMaterialColor(color);
-                    return false;
-                }     
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), typeof(int))]
-        public class SetHatPatch {
-            public static bool Prefix(HatParent __instance, int color) {
-                if (!CustomHatViewDatas.ContainsKey(__instance.Hat.name)) return true;
-                __instance.hatDataAsset = null;
-                __instance.PopulateFromHatViewData();
-                __instance.SetMaterialColor(color);
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.UpdateMaterial))]
-        public class UpdateMaterialPatch {
-            public static bool Prefix(HatParent __instance) {
-                HatViewData asset;
-                try {
-                    HatViewData vanillaAsset = __instance.hatDataAsset.GetAsset();
-                    return true;
-                } catch { 
-                    try {
-                       asset = CustomHatViewDatas[__instance.Hat.name];
-                    } catch {
-                        return false;
-                    }
-                }
-                if (asset.AltShader) {
-                    __instance.FrontLayer.sharedMaterial = asset.AltShader;
-                    if (__instance.BackLayer) {
-                        __instance.BackLayer.sharedMaterial = asset.AltShader;
-                    }
-                } else {
-                    __instance.FrontLayer.sharedMaterial = DestroyableSingleton<HatManager>.Instance.DefaultShader;
-                    if (__instance.BackLayer) {
-                        __instance.BackLayer.sharedMaterial = DestroyableSingleton<HatManager>.Instance.DefaultShader;
-                    }
-                }
-                int colorId = __instance.matProperties.ColorId;
-                PlayerMaterial.SetColors(colorId, __instance.FrontLayer);
-                if (__instance.BackLayer) {
-                    PlayerMaterial.SetColors(colorId, __instance.BackLayer);
-                }
-                __instance.FrontLayer.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer);
-                if (__instance.BackLayer) {
-                    __instance.BackLayer.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer);
-                }
-                PlayerMaterial.MaskType maskType = __instance.matProperties.MaskType;
-                if (maskType == PlayerMaterial.MaskType.ScrollingUI) {
-                    if (__instance.FrontLayer) {
-                        __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
-                    }
-                    if (__instance.BackLayer) {
-                        __instance.BackLayer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
-                        return false;
-                    }
-                } else if (maskType == PlayerMaterial.MaskType.Exile) {
-                    if (__instance.FrontLayer) {
-                        __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask;
-                    }
-                    if (__instance.BackLayer) {
-                        __instance.BackLayer.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask;
-                        return false;
-                    }
-                } else {
-                    if (__instance.FrontLayer) {
-                        __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.None;
-                    }
-                    if (__instance.BackLayer) {
-                        __instance.BackLayer.maskInteraction = SpriteMaskInteraction.None;
-                    }
-                }
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.LateUpdate))]
-        public static class HatParentLateUpdatePatch {
-            public static bool Prefix(HatParent __instance) {
-                if (__instance.Parent) {
-                    HatViewData hatViewData;
-                    try {
-                        hatViewData = __instance.hatDataAsset.GetAsset();
-                        return true;
-                    } catch {
-                        try {
-                            hatViewData = CustomHatViewDatas[__instance.Hat.name];
-                        } catch {
-                            return false;
-                        }
-                    }
-                    if (__instance.Hat && hatViewData != null) {
-                        if (__instance.FrontLayer.sprite != hatViewData.ClimbImage && __instance.FrontLayer.sprite != hatViewData.FloorImage) {
-                            if ((__instance.Hat.InFront || hatViewData.BackImage) && hatViewData.LeftMainImage) {
-                                __instance.FrontLayer.sprite = (__instance.Parent.flipX ? hatViewData.LeftMainImage : hatViewData.MainImage);
-                            }
-                            if (hatViewData.BackImage && hatViewData.LeftBackImage) {
-                                __instance.BackLayer.sprite = (__instance.Parent.flipX ? hatViewData.LeftBackImage : hatViewData.BackImage);
-                                return false;
-                            }
-                            if (!hatViewData.BackImage && !__instance.Hat.InFront && hatViewData.LeftMainImage) {
-                                __instance.BackLayer.sprite = (__instance.Parent.flipX ? hatViewData.LeftMainImage : hatViewData.MainImage);
-                                return false;
-                            }
-                        } else if (__instance.FrontLayer.sprite == hatViewData.ClimbImage || __instance.FrontLayer.sprite == hatViewData.LeftClimbImage) {
-                            SpriteAnimNodeSync spriteAnimNodeSync = __instance.SpriteSyncNode ?? __instance.GetComponent<SpriteAnimNodeSync>();
-                            if (spriteAnimNodeSync) {
-                                spriteAnimNodeSync.NodeId = 0;
-                            }
-                        }
-                    }
-                }
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetFloorAnim))]
-        public class HatParentSetFloorAnimPatch {
-            public static bool Prefix(HatParent __instance) {
-                try {
-                    HatViewData vanillaAsset = __instance.hatDataAsset.GetAsset();
-                    return true;
-                } catch { }
-                HatViewData hatViewData = CustomHatViewDatas[__instance.Hat.name];
-                __instance.BackLayer.enabled = false;
-                __instance.FrontLayer.enabled = true;
-                __instance.FrontLayer.sprite = hatViewData.FloorImage;
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetIdleAnim))]
-        public class HatParentSetIdleAnimPatch {
-            public static bool Prefix(HatParent __instance, int colorId) {
-                if (!__instance.Hat) return false;
-                if (!CustomHatViewDatas.ContainsKey(__instance.Hat.name))
-                    return true; 
-                HatViewData hatViewData = CustomHatViewDatas[__instance.Hat.name];
-                __instance.hatDataAsset = null;
-                __instance.PopulateFromHatViewData();
-                __instance.SetMaterialColor(colorId);
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetClimbAnim))]
-        public class HatParentSetClimbAnimPatch {
-            public static bool Prefix(HatParent __instance) {
-                try {
-                    HatViewData vanillaAsset = __instance.hatDataAsset.GetAsset();
-                    return true;
-                } catch { }
-
-                HatViewData hatViewData = CustomHatViewDatas[__instance.Hat.name];
-                if (!__instance.options.ShowForClimb) {
-                    return false;
-                }
-                __instance.BackLayer.enabled = false;
-                __instance.FrontLayer.enabled = true;
-                __instance.FrontLayer.sprite = hatViewData.ClimbImage;
-                return false;
-            }
-        }
-
-
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.PopulateFromHatViewData))]
-        public class PopulateFromHatViewDataPatch {
-            public static bool Prefix(HatParent __instance) {
-                try {
-                    HatViewData vanillaAsset = __instance.hatDataAsset.GetAsset();
-                    return true;
-                } catch { 
-                    if (__instance.Hat && !CustomHatViewDatas.ContainsKey(__instance.Hat.name))
-                        return true;
-                    }
-                
-
-                HatViewData asset = CustomHatViewDatas[__instance.Hat.name];
-
-                if (!asset) {
-                    return true;
-                }
-                __instance.UpdateMaterial();
-
-                SpriteAnimNodeSync spriteAnimNodeSync = __instance.SpriteSyncNode ?? __instance.GetComponent<SpriteAnimNodeSync>();
-                if (spriteAnimNodeSync) {
-                    spriteAnimNodeSync.NodeId = (__instance.Hat.NoBounce ? 1 : 0);
-                }
-                if (__instance.Hat.InFront) {
-                    __instance.BackLayer.enabled = false;
-                    __instance.FrontLayer.enabled = true;
-                    __instance.FrontLayer.sprite = asset.MainImage;
-                } else if (asset.BackImage) {
-                    __instance.BackLayer.enabled = true;
-                    __instance.FrontLayer.enabled = true;
-                    __instance.BackLayer.sprite = asset.BackImage;
-                    __instance.FrontLayer.sprite = asset.MainImage;
-                } else {
-                    __instance.BackLayer.enabled = true;
-                    __instance.FrontLayer.enabled = false;
-                    __instance.FrontLayer.sprite = null;
-                    __instance.BackLayer.sprite = asset.MainImage;
-                }
-                if (__instance.options.Initialized && __instance.HideHat()) {
-                    __instance.FrontLayer.enabled = false;
-                    __instance.BackLayer.enabled = false;
-                }
-                return false;
-            }
-        }
-
-        [HarmonyPatch(typeof(CosmeticsCache), nameof(CosmeticsCache.GetHat))]
-        public class CosmeticsCacheGetHatPatch {
-            public static bool Prefix(string id, ref HatViewData __result) {
-                TheOtherRolesPlugin.Logger.LogMessage($"trying to load hat {id} from cosmetics cache");
-                if (CustomHatViewDatas.ContainsKey(id)) {
-                    __result = CustomHatViewDatas[id];
-                    return false;
-                }
-                return true;
-            }
-        }
-
-        [HarmonyPatch(typeof(HatsTab), nameof(HatsTab.OnEnable))]
-        public class HatsTabOnEnablePatch {
-            public static string innerslothPackageName = "Innersloth Hats";
-            private static TMPro.TMP_Text textTemplate;
-
-            public static float createHatPackage(List<System.Tuple<HatData, HatExtension>> hats, string packageName, float YStart, HatsTab __instance) {
-                bool isDefaultPackage = innerslothPackageName == packageName;
-                if (!isDefaultPackage)
-                    hats = hats.OrderBy(x => x.Item1.name).ToList();
-                float offset = YStart;
-
-                if (textTemplate != null) {
-                    TMPro.TMP_Text title = UnityEngine.Object.Instantiate<TMPro.TMP_Text>(textTemplate, __instance.scroller.Inner);
-                    title.transform.localPosition = new Vector3(2.25f, YStart, -1f);
-                    title.transform.localScale = Vector3.one * 1.5f;
-                    title.fontSize *= 0.5f;
-                    title.enableAutoSizing = false;
-                    __instance.StartCoroutine(Effects.Lerp(0.1f, new System.Action<float>((p) => { title.SetText(packageName); })));
-                    offset -= 0.8f * __instance.YOffset;
-                }
-                for (int i = 0; i < hats.Count; i++) {
-                    HatData hat = hats[i].Item1;
-                    HatExtension ext = hats[i].Item2;
-
-                    float xpos = __instance.XRange.Lerp((i % __instance.NumPerRow) / (__instance.NumPerRow - 1f));
-                    float ypos = offset - (i / __instance.NumPerRow) * (isDefaultPackage ? 1f : 1.5f) * __instance.YOffset;
-                    ColorChip colorChip = UnityEngine.Object.Instantiate<ColorChip>(__instance.ColorTabPrefab, __instance.scroller.Inner);
-                    if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) {
-                        colorChip.Button.OnMouseOver.AddListener((System.Action)(() => __instance.SelectHat(hat)));
-                        colorChip.Button.OnMouseOut.AddListener((System.Action)(() => __instance.SelectHat(FastDestroyableSingleton<HatManager>.Instance.GetHatById(DataManager.Player.Customization.Hat))));
-                        colorChip.Button.OnClick.AddListener((System.Action)(() => __instance.ClickEquip()));
-                    } else {
-                        colorChip.Button.OnClick.AddListener((System.Action)(() => __instance.SelectHat(hat)));
-                    }
-                    colorChip.Button.ClickMask = __instance.scroller.Hitbox;
-                    Transform background = colorChip.transform.FindChild("Background");
-                    Transform foreground = colorChip.transform.FindChild("ForeGround");
-
-                    if (ext != null) {
-                        if (background != null) {
-                            background.localPosition = Vector3.down * 0.243f;
-                            background.localScale = new Vector3(background.localScale.x, 0.8f, background.localScale.y);
-                        }
-                        if (foreground != null) {
-                            foreground.localPosition = Vector3.down * 0.243f;
-                        }
-                
-                        if (textTemplate != null) {
-                            TMPro.TMP_Text description = UnityEngine.Object.Instantiate<TMPro.TMP_Text>(textTemplate, colorChip.transform);
-                            description.transform.localPosition = new Vector3(0f, -0.65f, -1f);
-                            description.alignment = TMPro.TextAlignmentOptions.Center;
-                            description.transform.localScale = Vector3.one * 0.65f;
-                            __instance.StartCoroutine(Effects.Lerp(0.1f, new System.Action<float>((p) => { description.SetText($"{hat.name}\nby {ext.author}"); })));
-                        }
-                    }
-                    
-                    colorChip.transform.localPosition = new Vector3(xpos, ypos, -1f);
-                    colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? CachedPlayer.LocalPlayer.Data.DefaultOutfit.ColorId : ((int)DataManager.Player.Customization.Color));
-                    colorChip.Inner.transform.localPosition = hat.ChipOffset;
-                    colorChip.Tag = hat;
-                    colorChip.SelectionHighlight.gameObject.SetActive(false);
-                    __instance.ColorChips.Add(colorChip);
-                }
-                return offset - ((hats.Count - 1) / __instance.NumPerRow) * (isDefaultPackage ? 1f : 1.5f) * __instance.YOffset - 1.75f;
-            }
-
-            public static void Postfix(HatsTab __instance) {
-                for (int i = 0; i < __instance.scroller.Inner.childCount; i++)
-                    UnityEngine.Object.Destroy(__instance.scroller.Inner.GetChild(i).gameObject);
-                __instance.ColorChips = new Il2CppSystem.Collections.Generic.List<ColorChip>();
-
-                HatData[] unlockedHats = FastDestroyableSingleton<HatManager>.Instance.GetUnlockedHats();
-                Dictionary<string, List<System.Tuple<HatData, HatExtension>>> packages = new Dictionary<string, List<System.Tuple<HatData, HatExtension>>>();
-
-                foreach (HatData hatBehaviour in unlockedHats) {
-                    HatExtension ext = hatBehaviour.getHatExtension();
-
-                    if (ext != null) {
-                        if (!packages.ContainsKey(ext.package)) 
-                            packages[ext.package] = new List<System.Tuple<HatData, HatExtension>>();
-                        packages[ext.package].Add(new System.Tuple<HatData, HatExtension>(hatBehaviour, ext));
-                    } else {
-                        if (!packages.ContainsKey(innerslothPackageName)) 
-                            packages[innerslothPackageName] = new List<System.Tuple<HatData, HatExtension>>();
-                        packages[innerslothPackageName].Add(new System.Tuple<HatData, HatExtension>(hatBehaviour, null));
-                    }
-                }
-
-                packages.Remove("Horse Hats");  // Cannot be selected!
-
-                float YOffset = __instance.YStart;
-                textTemplate = GameObject.Find("HatsGroup").transform.FindChild("Text").GetComponent<TMPro.TMP_Text>();
-
-                var orderedKeys = packages.Keys.OrderBy((string x) => {
-                    if (x == innerslothPackageName) return 1000;
-                    if (x == "Developer Hats") return 0;
-                    return 500;
-                });
-                foreach (string key in orderedKeys) {
-                    List<System.Tuple<HatData, HatExtension>> value = packages[key];
-                    YOffset = createHatPackage(value, key, YOffset, __instance);
-                }
-
-                __instance.scroller.ContentYBounds.max = -(YOffset + 4.1f);
-            }
-        }
-
-    }
-
-    public class CustomHatLoader {
-        public static bool running = false;
-        private const string REPO = "https://raw.githubusercontent.com/Eisbison/TheOtherHats/master";
-
-        public static List<CustomHatOnline> hatdetails = new List<CustomHatOnline>();
-        private static Task hatFetchTask = null;
-        public static void LaunchHatFetcher() {
-            if (running)
-                return;
-            running = true;
-            hatFetchTask = LaunchHatFetcherAsync();
-        }
-
-        private static async Task LaunchHatFetcherAsync() {
-            try {
-                HttpStatusCode status = await FetchHats();
-                if (status != HttpStatusCode.OK)
-                    System.Console.WriteLine("Custom Hats could not be loaded\n");
-            } catch (System.Exception e) {
-                System.Console.WriteLine("Unable to fetch hats\n" + e.Message);
-            }
-           running = false;
-        }
-
-        private static string sanitizeResourcePath(string res) {
-            if (res == null || !res.EndsWith(".png")) 
-                return null;
-
-            res = res.Replace("\\", "")
-                     .Replace("/", "")
-                     .Replace("*", "")
-                     .Replace("..", "");   
-            return res;
-        }
-
-        public static async Task<HttpStatusCode> FetchHats() {
-            HttpClient http = new HttpClient();
-            http.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue{ NoCache = true };
-                       var response = await http.GetAsync(new System.Uri($"{REPO}/CustomHats.json"), HttpCompletionOption.ResponseContentRead);
-            try {
-                if (response.StatusCode != HttpStatusCode.OK) return response.StatusCode;
-                if (response.Content == null) {
-                    System.Console.WriteLine("Server returned no data: " + response.StatusCode.ToString());
-                    return HttpStatusCode.ExpectationFailed;
-                }
-                string json = await response.Content.ReadAsStringAsync();
-                JToken jobj = JObject.Parse(json)["hats"];
-                if (!jobj.HasValues) return HttpStatusCode.ExpectationFailed;
-
-                List<CustomHatOnline> hatdatas = new List<CustomHatOnline>();
-
-                for (JToken current = jobj.First; current != null; current = current.Next) {
-                    if (current.HasValues) {
-                        CustomHatOnline info = new CustomHatOnline();
-
-                        info.name = current["name"]?.ToString();
-                        info.resource = sanitizeResourcePath(current["resource"]?.ToString());
-                        if (info.resource == null || info.name == null) // required
-                            continue;
-                        info.reshasha = current["reshasha"]?.ToString();
-                        info.backresource = sanitizeResourcePath(current["backresource"]?.ToString());
-                        info.reshashb = current["reshashb"]?.ToString();
-                        info.climbresource = sanitizeResourcePath(current["climbresource"]?.ToString());
-                        info.reshashc = current["reshashc"]?.ToString();
-                        info.flipresource = sanitizeResourcePath(current["flipresource"]?.ToString());
-                        info.reshashf = current["reshashf"]?.ToString();
-                        info.backflipresource = sanitizeResourcePath(current["backflipresource"]?.ToString());
-                        info.reshashbf = current["reshashbf"]?.ToString();
-
-                        info.author = current["author"]?.ToString();
-                        info.package = current["package"]?.ToString();
-                        info.condition = current["condition"]?.ToString();
-                        info.bounce = current["bounce"] != null;
-                        info.adaptive = current["adaptive"] != null;
-                        info.behind = current["behind"] != null;
-                        hatdatas.Add(info);
-                    }
-                }
-
-                List<string> markedfordownload = new List<string>();
-
-                string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\";
-                if (!Directory.Exists(filePath)) Directory.CreateDirectory(filePath);
-                MD5 md5 = MD5.Create();
-                foreach (CustomHatOnline data in hatdatas) {
-                   if (doesResourceRequireDownload(filePath + data.resource, data.reshasha, md5))
-                        markedfordownload.Add(data.resource);
-                   if (data.backresource != null && doesResourceRequireDownload(filePath + data.backresource, data.reshashb, md5))
-                        markedfordownload.Add(data.backresource);
-                   if (data.climbresource != null && doesResourceRequireDownload(filePath + data.climbresource, data.reshashc, md5))
-                        markedfordownload.Add(data.climbresource);
-                   if (data.flipresource != null && doesResourceRequireDownload(filePath + data.flipresource, data.reshashf, md5))
-                        markedfordownload.Add(data.flipresource);
-                   if (data.backflipresource != null && doesResourceRequireDownload(filePath + data.backflipresource, data.reshashbf, md5))
-                        markedfordownload.Add(data.backflipresource);
-                }
-                
-                foreach(var file in markedfordownload) {
-                    
-                    var hatFileResponse = await http.GetAsync($"{REPO}/hats/{file}", HttpCompletionOption.ResponseContentRead);
-                    if (hatFileResponse.StatusCode != HttpStatusCode.OK) continue;
-                    using (var responseStream = await hatFileResponse.Content.ReadAsStreamAsync()) {
-                        using (var fileStream = File.Create($"{filePath}\\{file}")) {
-                            responseStream.CopyTo(fileStream);
-                        }
-                    }
-                }
-
-                if (EventUtility.canBeEnabled || EventUtility.isEnabled) addHorseHats(ref hatdatas);
-                
-
-                hatdetails = hatdatas;
-            } catch (System.Exception ex) {
-                TheOtherRolesPlugin.Instance.Log.LogError(ex.ToString());
-                System.Console.WriteLine(ex);
-            }
-            return HttpStatusCode.OK;
-        }
-
-        private static bool doesResourceRequireDownload(string respath, string reshash, MD5 md5) {
-            if (reshash == null || !File.Exists(respath)) 
-                return true;
-
-            using (var stream = File.OpenRead(respath)) {
-                var hash = System.BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
-                return !reshash.Equals(hash);
-            }
-        }
-
-
-        public static List<string> horseHatProductIds = null;
-        private static void addHorseHats(ref List<CustomHatOnline> hatdatas) {
-
-            horseHatProductIds = new();
-            Assembly assembly = Assembly.GetExecutingAssembly();
-            string[] resourceNames = assembly.GetManifestResourceNames();
-            List<string> hatFiles = new();
-            Dictionary<string, List<string>> hatFilesSorted = new Dictionary<string, List<string>>();
-            foreach (string resourceName in resourceNames) {
-                if (resourceName.Contains("TheOtherRoles.Resources.HorseHats.") && resourceName.Contains(".png")) {
-                    hatFiles.Add(resourceName);
-                }
-            }
-
-            foreach (string s in hatFiles) {
-                string value = s.Substring(0, s.LastIndexOf("HorseSpecialHat") + 17);
-                if (value.Contains(".")) value.Remove(value.LastIndexOf("."));
-                if (!hatFilesSorted.ContainsKey(value)) hatFilesSorted.Add(value, new List<string>());
-                hatFilesSorted[value].Add(s);
-            }
-
-            foreach (var item in hatFilesSorted) {
-                CustomHatOnline info = new CustomHatOnline();
-                info.name = "HorseHat_" + item.Key;
-                info.resource = item.Value.FirstOrDefault(x => !x.Contains("back"));
-                info.backresource = item.Value.FirstOrDefault(x => x.Contains("back"));
-                info.adaptive = info.resource != null && info.resource.Contains("adaptive");
-                info.flipresource = item.Value.FirstOrDefault(x => x.Contains("flip"));
-                info.climbresource = item.Value.FirstOrDefault(x => x.Contains("climb"));
-                info.package = "Horse Hats";
-                if (info.resource == null || info.name == null) // required
-                    continue;
-                horseHatProductIds.Add("hat_" + info.name.Replace(" ", "_"));
-                hatdatas.Add(info);
-            }
-        }
-
-        public class CustomHatOnline : CustomHats.CustomHat { 
-            public string reshasha { get; set;}
-            public string reshashb { get; set;}
-            public string reshashc { get; set;}
-            public string reshashf { get; set;}
-            public string reshashbf { get; set;}
-        }   
-    }
-    public static class CustomHatExtensions {
-        public static CustomHats.HatExtension getHatExtension(this HatData hat) {
-            CustomHats.HatExtension ret = null;
-            if (CustomHats.TestExt != null && CustomHats.TestExt.condition.Equals(hat.name)) {
-                return CustomHats.TestExt;
-            }
-            CustomHats.CustomHatRegistry.TryGetValue(hat.name, out ret);
-            return ret;
-        }
-    }
-}
diff --git a/TheOtherRoles/Modules/CustomHats/CustomHat.cs b/TheOtherRoles/Modules/CustomHats/CustomHat.cs
new file mode 100644 (file)
index 0000000..210c8b9
--- /dev/null
@@ -0,0 +1,30 @@
+using System.Text.Json.Serialization;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public class CustomHat : CustomHatHashes
+{
+    [JsonPropertyName("author")] public string Author { get; set; }
+
+    [JsonPropertyName("bounce")] public bool Bounce { get; set; }
+
+    [JsonPropertyName("climbresource")] public string ClimbResource { get; set; }
+
+    [JsonPropertyName("condition")] public string Condition { get; set; }
+
+    [JsonPropertyName("name")] public string Name { get; set; }
+
+    [JsonPropertyName("package")] public string Package { get; set; }
+
+    [JsonPropertyName("resource")] public string Resource { get; set; }
+
+    [JsonPropertyName("adaptive")] public bool Adaptive { get; set; }
+
+    [JsonPropertyName("behind")] public bool Behind { get; set; }
+
+    [JsonPropertyName("backresource")] public string BackResource { get; set; }
+
+    [JsonPropertyName("backflipresource")] public string BackFlipResource { get; set; }
+
+    [JsonPropertyName("flipresource")] public string FlipResource { get; set; }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/CustomHatHashes.cs b/TheOtherRoles/Modules/CustomHats/CustomHatHashes.cs
new file mode 100644 (file)
index 0000000..ead2350
--- /dev/null
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public class CustomHatHashes
+{
+    [JsonPropertyName("reshasha")] public string ResHashA { get; set; }
+
+    [JsonPropertyName("reshashb")] public string ResHashB { get; set; }
+
+    [JsonPropertyName("reshashbf")] public string ResHashBf { get; set; }
+
+    [JsonPropertyName("reshashc")] public string ResHashC { get; set; }
+
+    [JsonPropertyName("reshashf")] public string ResHashF { get; set; }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/CustomHatManager.cs b/TheOtherRoles/Modules/CustomHats/CustomHatManager.cs
new file mode 100644 (file)
index 0000000..4eb5185
--- /dev/null
@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Cryptography;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public static class CustomHatManager
+{
+    public const string ResourcesDirectory = "TheOtherHats";
+    public const string InnerslothPackageName = "Innersloth Hats";
+    public const string DeveloperPackageName = "Developer Hats";
+    
+    internal static readonly Tuple<string, string> Repository = new("TheOtherRolesAU", "TheOtherHats");
+    internal static string RepositoryUrl
+    {
+        get
+        {
+            var (owner, repository) = Repository;
+            return $"https://raw.githubusercontent.com/{owner}/{repository}/master";
+        }
+    }
+
+    internal static readonly string ManifestFileName = "CustomHats.json";
+    
+    internal static string CustomSkinsDirectory => Path.Combine(Path.GetDirectoryName(Application.dataPath)!, ResourcesDirectory);
+    internal static string HatsDirectory => CustomSkinsDirectory;
+    
+    internal static readonly List<CustomHat> UnregisteredHats = new();
+    internal static readonly Dictionary<string, HatViewData> ViewDataCache = new();
+    internal static readonly Dictionary<string, HatExtension> ExtensionCache = new();
+    
+    private static readonly HatsLoader Loader;
+    private static Material cachedShader;
+    
+    internal static HatExtension TestExtension { get; private set; }
+
+    static CustomHatManager()
+    {
+        Loader = TheOtherRolesPlugin.Instance.AddComponent<HatsLoader>();
+    }
+    
+    internal static void LoadHats()
+    {
+        Loader.FetchHats();
+    }
+    
+    internal static bool TryGetCached(this HatParent hatParent, out HatViewData asset)
+    {
+        if (hatParent && hatParent.Hat) return hatParent.Hat.TryGetCached(out asset);
+        asset = null;
+        return false;
+    }
+    
+    internal static bool TryGetCached(this HatData hat, out HatViewData asset)
+    {
+        return ViewDataCache.TryGetValue(hat.name, out asset);
+    }
+
+    internal static bool IsCached(this HatData hat)
+    {
+        return ViewDataCache.ContainsKey(hat.name);
+    }
+    
+    internal static bool IsCached(this HatParent hatParent)
+    {
+        return hatParent.Hat.IsCached();
+    }
+    
+    internal static HatData CreateHatBehaviour(CustomHat ch, bool testOnly = false)
+    {
+        if (cachedShader == null) cachedShader = DestroyableSingleton<HatManager>.Instance.PlayerMaterial;
+
+        var viewData = ViewDataCache[ch.Name] = ScriptableObject.CreateInstance<HatViewData>();
+        var hat = ScriptableObject.CreateInstance<HatData>();
+
+        viewData.MainImage = CreateHatSprite(ch.Resource);
+        if (viewData.MainImage == null) {
+            throw new FileNotFoundException("File not downloaded yet");
+        }
+        viewData.FloorImage = viewData.MainImage;
+        if (ch.BackResource != null)
+        {
+            viewData.BackImage = CreateHatSprite(ch.BackResource);
+            ch.Behind = true;
+        }
+
+        if (ch.ClimbResource != null)
+        {
+            viewData.ClimbImage = CreateHatSprite(ch.ClimbResource);
+            viewData.LeftClimbImage = viewData.ClimbImage;
+        }
+
+        hat.name = ch.Name;
+        hat.displayOrder = 99;
+        hat.ProductId = "hat_" + ch.Name.Replace(' ', '_');
+        hat.InFront = !ch.Behind;
+        hat.NoBounce = !ch.Bounce;
+        hat.ChipOffset = new Vector2(0f, 0.2f);
+        hat.Free = true;
+
+        if (ch.Adaptive && cachedShader != null)
+        {
+            viewData.AltShader = cachedShader;
+        }
+        
+        var extend = new HatExtension
+        {
+            Author = ch.Author ?? "Unknown",
+            Package = ch.Package ?? "Misc.",
+            Condition = ch.Condition ?? "none"
+        };
+
+        if (ch.FlipResource != null)
+        {
+            extend.FlipImage = CreateHatSprite(ch.FlipResource);
+        }
+
+        if (ch.BackFlipResource != null)
+        {
+            extend.BackFlipImage = CreateHatSprite(ch.BackFlipResource);
+        }
+
+        if (testOnly)
+        {
+            TestExtension = extend;
+            TestExtension.Condition = hat.name;
+        }
+        else
+        {
+            ExtensionCache[hat.name] = extend;
+        }
+        
+        hat.ViewDataRef = new AssetReference(ViewDataCache[hat.name].Pointer);
+        hat.CreateAddressableAsset();
+        return hat;
+    }
+    
+    private static Sprite CreateHatSprite(string path)
+    {
+        var texture = Helpers.loadTextureFromDisk(Path.Combine(HatsDirectory, path));
+        if (texture == null) return null;
+        var sprite = Sprite.Create(texture, 
+            new Rect(0, 0, texture.width, texture.height),
+            new Vector2(0.53f, 0.575f),
+            texture.width * 0.375f);
+        if (sprite == null) return null;
+        texture.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset;
+        sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset;
+
+        return sprite;
+    }
+
+    public static List<CustomHat> CreateHatDetailsFromFileNames(string[] fileNames, bool fromDisk = false)
+    {
+        var fronts = new Dictionary<string, CustomHat>();
+        var backs = new Dictionary<string, string>();
+        var flips = new Dictionary<string, string>();
+        var backFlips = new Dictionary<string, string>();
+        var climbs = new Dictionary<string, string>();
+
+        foreach (var fileName in fileNames)
+        {
+            var index = fileName.LastIndexOf("\\", StringComparison.InvariantCulture) + 1;
+            var s = fromDisk ? fileName[index..].Split('.')[0] : fileName.Split('.')[3];
+            var p = s.Split('_');
+            var options = new HashSet<string>(p);
+            if (options.Contains("back") && options.Contains("flip"))
+            {
+                backFlips[p[0]] = fileName;
+            }
+            else if (options.Contains("climb"))
+            {
+                climbs[p[0]] = fileName;
+            }
+            else if (options.Contains("back"))
+            {
+                backs[p[0]] = fileName;
+            }
+            else if (options.Contains("flip"))
+            {
+                flips[p[0]] = fileName;
+            }
+            else
+            {
+                fronts[p[0]] = new CustomHat
+                {
+                    Resource = fileName,
+                    Name = p[0].Replace('-', ' '),
+                    Bounce = options.Contains("bounce"),
+                    Adaptive = options.Contains("adaptive"),
+                    Behind = options.Contains("behind"),
+                };
+            }
+        }
+
+        var hats = new List<CustomHat>();
+
+        foreach (var frontKvP in fronts)
+        {
+            var k = frontKvP.Key;
+            var hat = frontKvP.Value;
+            backs.TryGetValue(k, out var backResource);
+            climbs.TryGetValue(k, out var climbResource);
+            flips.TryGetValue(k, out var flipResource);
+            backFlips.TryGetValue(k, out var backFlipResource);
+            if (backResource != null) hat.BackResource = backResource;
+            if (climbResource != null) hat.ClimbResource = climbResource;
+            if (flipResource != null) hat.FlipResource = flipResource;
+            if (backFlipResource != null) hat.BackFlipResource = backFlipResource;
+            if (hat.BackResource != null) hat.Behind = true;
+            hats.Add(hat);
+        }
+
+        return hats;
+    }
+
+    internal static List<CustomHat> SanitizeHats(SkinsConfigFile response)
+    {
+        foreach (var hat in response.Hats)
+        {
+            hat.Resource = SanitizeFileName(hat.Resource);
+            hat.BackResource = SanitizeFileName(hat.BackResource);
+            hat.ClimbResource = SanitizeFileName(hat.ClimbResource);
+            hat.FlipResource = SanitizeFileName(hat.FlipResource);
+            hat.BackFlipResource = SanitizeFileName(hat.BackFlipResource);
+        }
+
+        return response.Hats;
+    }
+
+    private static string SanitizeFileName(string path)
+    {
+        if (path == null || !path.EndsWith(".png")) return null;
+        return path.Replace("\\", "")
+            .Replace("/", "")
+            .Replace("*", "")
+            .Replace("..", "");
+    }
+
+    private static bool ResourceRequireDownload(string resFile, string resHash, HashAlgorithm algorithm)
+    {
+        var filePath = Path.Combine(HatsDirectory, resFile);
+        if (resHash == null || !File.Exists(filePath))
+        {
+            return true;
+        }
+        using var stream = File.OpenRead(filePath);
+        var hash = BitConverter.ToString(algorithm.ComputeHash(stream))
+            .Replace("-", string.Empty)
+            .ToLowerInvariant();
+        return !resHash.Equals(hash);
+    }
+
+    internal static List<string> GenerateDownloadList(List<CustomHat> hats)
+    {
+        var algorithm = MD5.Create();
+        var toDownload = new List<string>();
+
+        foreach (var hat in hats)
+        {
+            var files = new List<Tuple<string, string>>
+            {
+                new(hat.Resource, hat.ResHashA),
+                new(hat.BackResource, hat.ResHashB),
+                new(hat.ClimbResource, hat.ResHashC),
+                new(hat.FlipResource, hat.ResHashF),
+                new(hat.BackFlipResource, hat.ResHashBf)
+            };
+            foreach (var (fileName, fileHash) in files)
+            {
+                if (fileName != null && ResourceRequireDownload(fileName, fileHash, algorithm))
+                {
+                    toDownload.Add(fileName);
+                }
+            }
+        }
+
+        return toDownload;
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Extensions/HatDataExtensions.cs b/TheOtherRoles/Modules/CustomHats/Extensions/HatDataExtensions.cs
new file mode 100644 (file)
index 0000000..d7a1120
--- /dev/null
@@ -0,0 +1,14 @@
+namespace TheOtherRoles.Modules.CustomHats.Extensions;
+
+internal static class HatDataExtensions
+{
+    public static HatExtension GetHatExtension(this HatData hat)
+    {
+        if (CustomHatManager.TestExtension != null && CustomHatManager.TestExtension.Condition.Equals(hat.name))
+        {
+            return CustomHatManager.TestExtension;
+        }
+
+        return CustomHatManager.ExtensionCache.TryGetValue(hat.name, out var extension) ? extension : null;
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/HatExtension.cs b/TheOtherRoles/Modules/CustomHats/HatExtension.cs
new file mode 100644 (file)
index 0000000..a50f5b7
--- /dev/null
@@ -0,0 +1,12 @@
+using UnityEngine;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public class HatExtension
+{
+    public string Author { get; set; }
+    public string Package { get; set; }
+    public string Condition { get; set; }
+    public Sprite FlipImage { get; set; }
+    public Sprite BackFlipImage { get; set; }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/HatsLoader.cs b/TheOtherRoles/Modules/CustomHats/HatsLoader.cs
new file mode 100644 (file)
index 0000000..d8b0584
--- /dev/null
@@ -0,0 +1,103 @@
+using System.Collections;
+using System.IO;
+using System.Text.Json;
+using BepInEx.Unity.IL2CPP.Utils;
+using UnityEngine;
+using UnityEngine.Networking;
+using static TheOtherRoles.Modules.CustomHats.CustomHatManager;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public class HatsLoader : MonoBehaviour
+{
+    private bool isRunning;
+
+    public void FetchHats()
+    {
+        if (isRunning) return;
+        this.StartCoroutine(CoFetchHats());
+    }
+
+    [HideFromIl2Cpp]
+    private IEnumerator CoFetchHats()
+    {
+        isRunning = true;
+        var www = new UnityWebRequest();
+        www.SetMethod(UnityWebRequest.UnityWebRequestMethod.Get);
+        TheOtherRolesPlugin.Logger.LogMessage($"Download manifest at: {RepositoryUrl}/{ManifestFileName}");
+        www.SetUrl($"{RepositoryUrl}/{ManifestFileName}");
+        www.downloadHandler = new DownloadHandlerBuffer();
+        var operation = www.SendWebRequest();
+
+        while (!operation.isDone)
+        {
+            yield return new WaitForEndOfFrame();
+        }
+
+        if (www.isNetworkError || www.isHttpError)
+        {
+            TheOtherRolesPlugin.Logger.LogError(www.error);
+            yield break;
+        }
+
+        var response = JsonSerializer.Deserialize<SkinsConfigFile>(www.downloadHandler.text, new JsonSerializerOptions
+        {
+            AllowTrailingCommas = true
+        });
+        www.downloadHandler.Dispose();
+        www.Dispose();
+
+        if (!Directory.Exists(HatsDirectory)) Directory.CreateDirectory(HatsDirectory);
+
+        UnregisteredHats.AddRange(SanitizeHats(response));
+        var toDownload = GenerateDownloadList(UnregisteredHats);
+
+        TheOtherRolesPlugin.Logger.LogMessage($"I'll download {toDownload.Count} hat files");
+
+        foreach (var fileName in toDownload)
+        {
+            yield return CoDownloadHatAsset(fileName);
+        }
+
+        isRunning = false;
+    }
+
+    private static IEnumerator CoDownloadHatAsset(string fileName)
+    {
+        var www = new UnityWebRequest();
+        www.SetMethod(UnityWebRequest.UnityWebRequestMethod.Get);
+        fileName = fileName.Replace(" ", "%20");
+        TheOtherRolesPlugin.Logger.LogMessage($"downloading hat: {fileName}");
+        www.SetUrl($"{RepositoryUrl}/hats/{fileName}");
+        www.downloadHandler = new DownloadHandlerBuffer();
+        var operation = www.SendWebRequest();
+
+        while (!operation.isDone)
+        {
+            yield return new WaitForEndOfFrame();
+        }
+
+        if (www.isNetworkError || www.isHttpError)
+        {
+            TheOtherRolesPlugin.Logger.LogError(www.error);
+            yield break;
+        }
+
+        var filePath = Path.Combine(HatsDirectory, fileName);
+        filePath = filePath.Replace("%20", " ");
+        var persistTask = File.WriteAllBytesAsync(filePath, www.downloadHandler.data);
+        while (!persistTask.IsCompleted)
+        {
+            if (persistTask.Exception != null)
+            {
+                TheOtherRolesPlugin.Logger.LogError(persistTask.Exception.Message);
+                break;
+            }
+
+            yield return new WaitForEndOfFrame();
+        }
+
+        www.downloadHandler.Dispose();
+        www.Dispose();
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Patches/CosmeticsCachePatches.cs b/TheOtherRoles/Modules/CustomHats/Patches/CosmeticsCachePatches.cs
new file mode 100644 (file)
index 0000000..31b843f
--- /dev/null
@@ -0,0 +1,16 @@
+using HarmonyLib;
+using TheOtherRoles;
+
+namespace TheOtherRoles.Modules.CustomHats.Patches;
+
+[HarmonyPatch(typeof(CosmeticsCache))]
+internal static class CosmeticsCachePatches
+{
+    [HarmonyPatch(nameof(CosmeticsCache.GetHat))]
+    [HarmonyPrefix]
+    private static bool GetHatPrefix(string id, ref HatViewData __result)
+    {
+        TheOtherRolesPlugin.Logger.LogMessage($"trying to load hat {id} from cosmetics cache");
+        return !CustomHatManager.ViewDataCache.TryGetValue(id, out __result);
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Patches/HatManagerPatches.cs b/TheOtherRoles/Modules/CustomHats/Patches/HatManagerPatches.cs
new file mode 100644 (file)
index 0000000..cdfc2ab
--- /dev/null
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Cpp2IL.Core.Extensions;
+using HarmonyLib;
+using TheOtherRoles;
+
+namespace TheOtherRoles.Modules.CustomHats.Patches;
+
+[HarmonyPatch(typeof(HatManager))]
+internal static class HatManagerPatches
+{
+    private static bool isRunning;
+    private static bool isLoaded;
+    private static List<HatData> allHats;
+        
+    [HarmonyPatch(nameof(HatManager.GetHatById))]
+    [HarmonyPrefix]
+    private static void GetHatByIdPrefix(HatManager __instance)
+    {
+        if (isRunning || isLoaded) return;
+        isRunning = true;
+        // Maybe we can use lock keyword to ensure simultaneous list manipulations ?
+        allHats = __instance.allHats.ToList();
+        var cache = CustomHatManager.UnregisteredHats.Clone();
+        foreach (var hat in cache)
+        {
+            try
+            {
+                allHats.Add(CustomHatManager.CreateHatBehaviour(hat));
+                CustomHatManager.UnregisteredHats.Remove(hat);
+            }
+            catch
+            {
+                // This means the file has not been downloaded yet, do nothing...
+            }
+        }
+        if (CustomHatManager.UnregisteredHats.Count == 0)
+            isLoaded = true;
+        cache.Clear();
+
+        __instance.allHats = allHats.ToArray();
+    }
+        
+    [HarmonyPatch(nameof(HatManager.GetHatById))]
+    [HarmonyPostfix]
+    private static void GetHatByIdPostfix()
+    {
+        isRunning = false;
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Patches/HatParentPatches.cs b/TheOtherRoles/Modules/CustomHats/Patches/HatParentPatches.cs
new file mode 100644 (file)
index 0000000..6112de0
--- /dev/null
@@ -0,0 +1,269 @@
+using System;
+using System.IO;
+using System.Linq;
+using HarmonyLib;
+using PowerTools;
+using TheOtherRoles;
+using UnityEngine;
+
+namespace TheOtherRoles.Modules.CustomHats.Patches;
+
+[HarmonyPatch(typeof(HatParent))]
+internal static class HatParentPatches
+{
+    [HarmonyPatch(nameof(HatParent.SetHat), typeof(int))]
+    [HarmonyPriority(Priority.High)]
+    [HarmonyPrefix]
+    private static void SetHatPrefix(HatParent __instance)
+    {
+        SetCustomHat(__instance);
+    }
+
+    [HarmonyPatch(nameof(HatParent.SetHat), typeof(HatData), typeof(int))]
+    [HarmonyPrefix]
+    private static bool SetHatPrefix(HatParent __instance, HatData hat, int color)
+    {
+        if (SetCustomHat(__instance)) return true;
+        __instance.PopulateFromHatViewData();
+        __instance.SetMaterialColor(color);
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.SetHat), typeof(int))]
+    [HarmonyPrefix]
+    private static bool SetHatPrefix(HatParent __instance, int color)
+    {
+        if (!__instance.IsCached()) return true;
+        __instance.hatDataAsset = null;
+        __instance.PopulateFromHatViewData();
+        __instance.SetMaterialColor(color);
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.UpdateMaterial))]
+    [HarmonyPrefix]
+    private static bool UpdateMaterialPrefix(HatParent __instance)
+    {
+        if (!__instance.TryGetCached(out var asset)) return true;
+        if (asset && asset.AltShader)
+        {
+            __instance.FrontLayer.sharedMaterial = asset.AltShader;
+            if (__instance.BackLayer)
+            {
+                __instance.BackLayer.sharedMaterial = asset.AltShader;
+            }
+        }
+        else
+        {
+            __instance.FrontLayer.sharedMaterial = DestroyableSingleton<HatManager>.Instance.DefaultShader;
+            if (__instance.BackLayer)
+            {
+                __instance.BackLayer.sharedMaterial = DestroyableSingleton<HatManager>.Instance.DefaultShader;
+            }
+        }
+
+        var colorId = __instance.matProperties.ColorId;
+        PlayerMaterial.SetColors(colorId, __instance.FrontLayer);
+        if (__instance.BackLayer)
+        {
+            PlayerMaterial.SetColors(colorId, __instance.BackLayer);
+        }
+
+        __instance.FrontLayer.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer);
+        if (__instance.BackLayer)
+        {
+            __instance.BackLayer.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer);
+        }
+
+        var maskType = __instance.matProperties.MaskType;
+        switch (maskType)
+        {
+            case PlayerMaterial.MaskType.ScrollingUI:
+                if (__instance.FrontLayer)
+                {
+                    __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+                }
+
+                if (__instance.BackLayer)
+                {
+                    __instance.BackLayer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+                }
+
+                break;
+            case PlayerMaterial.MaskType.Exile:
+                if (__instance.FrontLayer)
+                {
+                    __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask;
+                }
+
+                if (__instance.BackLayer)
+                {
+                    __instance.BackLayer.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask;
+                }
+
+                break;
+            default:
+                if (__instance.FrontLayer)
+                {
+                    __instance.FrontLayer.maskInteraction = SpriteMaskInteraction.None;
+                }
+
+                if (__instance.BackLayer)
+                {
+                    __instance.BackLayer.maskInteraction = SpriteMaskInteraction.None;
+                }
+
+                break;
+        }
+
+        if (__instance.matProperties.MaskLayer > 0) return false;
+        PlayerMaterial.SetMaskLayerBasedOnLocalPlayer(__instance.FrontLayer, __instance.matProperties.IsLocalPlayer);
+        if (!__instance.BackLayer) return false;
+        PlayerMaterial.SetMaskLayerBasedOnLocalPlayer(__instance.BackLayer, __instance.matProperties.IsLocalPlayer);
+
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.LateUpdate))]
+    [HarmonyPrefix]
+    private static bool LateUpdatePrefix(HatParent __instance)
+    {
+        if (!__instance.Parent || !__instance.Hat) return false;
+        if (!__instance.TryGetCached(out var hatViewData)) return true;
+        if (__instance.FrontLayer.sprite != hatViewData.ClimbImage &&
+            __instance.FrontLayer.sprite != hatViewData.FloorImage)
+        {
+            if ((__instance.Hat.InFront || hatViewData.BackImage) && hatViewData.LeftMainImage)
+            {
+                __instance.FrontLayer.sprite =
+                    __instance.Parent.flipX ? hatViewData.LeftMainImage : hatViewData.MainImage;
+            }
+
+            if (hatViewData.BackImage && hatViewData.LeftBackImage)
+            {
+                __instance.BackLayer.sprite =
+                    __instance.Parent.flipX ? hatViewData.LeftBackImage : hatViewData.BackImage;
+                return false;
+            }
+
+            if (!hatViewData.BackImage && !__instance.Hat.InFront && hatViewData.LeftMainImage)
+            {
+                __instance.BackLayer.sprite =
+                    __instance.Parent.flipX ? hatViewData.LeftMainImage : hatViewData.MainImage;
+                return false;
+            }
+        }
+        else if (__instance.FrontLayer.sprite == hatViewData.ClimbImage ||
+                 __instance.FrontLayer.sprite == hatViewData.LeftClimbImage)
+        {
+            var spriteAnimNodeSync = __instance.SpriteSyncNode != null
+                ? __instance.SpriteSyncNode
+                : __instance.GetComponent<SpriteAnimNodeSync>();
+            if (spriteAnimNodeSync)
+            {
+                spriteAnimNodeSync.NodeId = 0;
+            }
+        }
+
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.SetFloorAnim))]
+    [HarmonyPrefix]
+    private static bool SetFloorAnimPrefix(HatParent __instance)
+    {
+        if (!__instance.TryGetCached(out var hatViewData)) return true;
+        __instance.BackLayer.enabled = false;
+        __instance.FrontLayer.enabled = true;
+        __instance.FrontLayer.sprite = hatViewData.FloorImage;
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.SetIdleAnim))]
+    [HarmonyPrefix]
+    private static bool SetIdleAnimPrefix(HatParent __instance, int colorId)
+    {
+        if (!__instance.Hat) return false;
+        if (!__instance.IsCached()) return true;
+        __instance.hatDataAsset = null;
+        __instance.PopulateFromHatViewData();
+        __instance.SetMaterialColor(colorId);
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.SetClimbAnim))]
+    [HarmonyPrefix]
+    private static bool SetClimbAnimPrefix(HatParent __instance)
+    {
+        if (!__instance.TryGetCached(out var hatViewData)) return true;
+        if (!__instance.options.ShowForClimb) return false;
+        __instance.BackLayer.enabled = false;
+        __instance.FrontLayer.enabled = true;
+        __instance.FrontLayer.sprite = hatViewData.ClimbImage;
+        return false;
+    }
+
+    [HarmonyPatch(nameof(HatParent.PopulateFromHatViewData))]
+    [HarmonyPrefix]
+    private static bool PopulateFromHatViewDataPrefix(HatParent __instance)
+    {
+        if (!__instance.TryGetCached(out var asset)) return true;
+        __instance.UpdateMaterial();
+
+        var spriteAnimNodeSync = __instance.SpriteSyncNode
+            ? __instance.SpriteSyncNode
+            : __instance.GetComponent<SpriteAnimNodeSync>();
+        if (spriteAnimNodeSync)
+        {
+            spriteAnimNodeSync.NodeId = __instance.Hat.NoBounce ? 1 : 0;
+        }
+
+        if (__instance.Hat.InFront)
+        {
+            __instance.BackLayer.enabled = false;
+            __instance.FrontLayer.enabled = true;
+            __instance.FrontLayer.sprite = asset.MainImage;
+        }
+        else if (asset.BackImage)
+        {
+            __instance.BackLayer.enabled = true;
+            __instance.FrontLayer.enabled = true;
+            __instance.BackLayer.sprite = asset.BackImage;
+            __instance.FrontLayer.sprite = asset.MainImage;
+        }
+        else
+        {
+            __instance.BackLayer.enabled = true;
+            __instance.FrontLayer.enabled = false;
+            __instance.FrontLayer.sprite = null;
+            __instance.BackLayer.sprite = asset.MainImage;
+        }
+
+        if (!__instance.options.Initialized || !__instance.HideHat()) return false;
+        __instance.FrontLayer.enabled = false;
+        __instance.BackLayer.enabled = false;
+        return false;
+    }
+
+    private static bool SetCustomHat(HatParent hatParent)
+    {
+        var dirPath = Path.Combine(CustomHatManager.HatsDirectory, "Test");
+        if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
+        if (!DestroyableSingleton<TutorialManager>.InstanceExists) return true;
+        var d = new DirectoryInfo(dirPath);
+        var filePaths = d.GetFiles("*.png").Select(x => x.FullName).ToArray();
+        var hats = CustomHatManager.CreateHatDetailsFromFileNames(filePaths, true);
+        if (hats.Count <= 0) return false;
+        try
+        {
+            hatParent.Hat = CustomHatManager.CreateHatBehaviour(hats[0], true);
+        }
+        catch (Exception err)
+        {
+            TheOtherRolesPlugin.Logger.LogWarning($"Unable to create test hat \n{err}");
+            return true;
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Patches/HatsTabPatches.cs b/TheOtherRoles/Modules/CustomHats/Patches/HatsTabPatches.cs
new file mode 100644 (file)
index 0000000..5a258e4
--- /dev/null
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AmongUs.Data;
+using TheOtherRoles.Modules.CustomHats.Extensions;
+using HarmonyLib;
+using TMPro;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace TheOtherRoles.Modules.CustomHats.Patches;
+
+[HarmonyPatch(typeof(HatsTab))]
+internal static class HatsTabPatches
+{
+    private static TextMeshPro textTemplate;
+    
+    [HarmonyPatch(nameof(HatsTab.OnEnable))]
+    [HarmonyPostfix]
+    private static void OnEnablePostfix(HatsTab __instance)
+    {
+        for (var i = 0; i < __instance.scroller.Inner.childCount; i++)
+        {
+            Object.Destroy(__instance.scroller.Inner.GetChild(i).gameObject);
+        }
+
+        __instance.ColorChips = new Il2CppSystem.Collections.Generic.List<ColorChip>();
+        var unlockedHats = DestroyableSingleton<HatManager>.Instance.GetUnlockedHats();
+        var packages = new Dictionary<string, List<Tuple<HatData, HatExtension>>>();
+
+        foreach (var hatBehaviour in unlockedHats)
+        {
+            var ext = hatBehaviour.GetHatExtension();
+            if (ext != null)
+            {
+                if (!packages.ContainsKey(ext.Package))
+                {
+                    packages[ext.Package] = new List<Tuple<HatData, HatExtension>>();
+                }
+                packages[ext.Package].Add(new Tuple<HatData, HatExtension>(hatBehaviour, ext));
+            }
+            else
+            {
+                if (!packages.ContainsKey(CustomHatManager.InnerslothPackageName))
+                {
+                    packages[CustomHatManager.InnerslothPackageName] = new List<Tuple<HatData, HatExtension>>();
+                }
+                packages[CustomHatManager.InnerslothPackageName].Add(new Tuple<HatData, HatExtension>(hatBehaviour, null));
+            }
+        }
+
+        var yOffset = __instance.YStart;
+        textTemplate = GameObject.Find("HatsGroup").transform.FindChild("Text").GetComponent<TextMeshPro>();
+
+        var orderedKeys = packages.Keys.OrderBy(x =>
+            x switch
+            {
+                CustomHatManager.InnerslothPackageName => 1000,
+                CustomHatManager.DeveloperPackageName => 0,
+                _ => 500
+            });
+        foreach (var key in orderedKeys)
+        {
+            var value = packages[key];
+            yOffset = CreateHatPackage(value, key, yOffset, __instance);
+        }
+        
+        __instance.scroller.ContentYBounds.max = -(yOffset + 4.1f);
+    }
+
+    private static float CreateHatPackage(List<Tuple<HatData, HatExtension>> hats, string packageName, float yStart,
+        HatsTab hatsTab)
+    {
+        var isDefaultPackage = CustomHatManager.InnerslothPackageName == packageName;
+        if (!isDefaultPackage)
+        {
+            hats = hats.OrderBy(x => x.Item1.name).ToList();
+        }
+
+        var offset = yStart;
+        if (textTemplate != null)
+        {
+            var title = Object.Instantiate(textTemplate, hatsTab.scroller.Inner);
+            title.transform.localPosition = new Vector3(2.25f, yStart, -1f);
+            title.transform.localScale = Vector3.one * 1.5f;
+            title.fontSize *= 0.5f;
+            title.enableAutoSizing = false;
+            hatsTab.StartCoroutine(Effects.Lerp(0.1f, new Action<float>(p => { title.SetText(packageName); })));
+            offset -= 0.8f * hatsTab.YOffset;
+        }
+
+        for (var i = 0; i < hats.Count; i++)
+        {
+            var (hat, ext) = hats[i];
+            var xPos = hatsTab.XRange.Lerp(i % hatsTab.NumPerRow / (hatsTab.NumPerRow - 1f));
+            var yPos = offset - i / hatsTab.NumPerRow * (isDefaultPackage ? 1f : 1.5f) * hatsTab.YOffset;
+            var colorChip = Object.Instantiate(hatsTab.ColorTabPrefab, hatsTab.scroller.Inner);
+            if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
+            {
+                colorChip.Button.OnMouseOver.AddListener((Action)(() => hatsTab.SelectHat(hat)));
+                colorChip.Button.OnMouseOut.AddListener((Action)(() => hatsTab.SelectHat(DestroyableSingleton<HatManager>.Instance.GetHatById(DataManager.Player.Customization.Hat))));
+                colorChip.Button.OnClick.AddListener((Action)hatsTab.ClickEquip);
+            }
+            else
+            {
+                colorChip.Button.OnClick.AddListener((Action)(() => hatsTab.SelectHat(hat)));
+            }
+            colorChip.Button.ClickMask = hatsTab.scroller.Hitbox;
+            colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.ScrollingUI);
+            hatsTab.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
+            var background = colorChip.transform.FindChild("Background");
+            var foreground = colorChip.transform.FindChild("ForeGround");
+
+            if (ext != null)
+            {
+                if (background != null) {
+                    background.localPosition = Vector3.down * 0.243f;
+                    background.localScale = new Vector3(background.localScale.x, 0.8f, background.localScale.y);
+                }
+                if (foreground != null) {
+                    foreground.localPosition = Vector3.down * 0.243f;
+                }
+                
+                if (textTemplate != null) {
+                    var description = Object.Instantiate(textTemplate, colorChip.transform);
+                    description.transform.localPosition = new Vector3(0f, -0.65f, -1f);
+                    description.alignment = TextAlignmentOptions.Center;
+                    description.transform.localScale = Vector3.one * 0.65f;
+                    hatsTab.StartCoroutine(Effects.Lerp(0.1f, new Action<float>(p => { description.SetText($"{hat.name}\nby {ext.Author}"); })));
+                }
+            }
+            
+            colorChip.transform.localPosition = new Vector3(xPos, yPos, -1f);
+            colorChip.Inner.SetHat(hat, hatsTab.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
+            colorChip.Inner.transform.localPosition = hat.ChipOffset;
+            colorChip.Tag = hat;
+            colorChip.SelectionHighlight.gameObject.SetActive(false);
+            hatsTab.ColorChips.Add(colorChip);
+        }
+
+        return offset - (hats.Count - 1) / hatsTab.NumPerRow * (isDefaultPackage ? 1f : 1.5f) * hatsTab.YOffset -
+               1.75f;
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/Patches/PlayerPhysicsPatches.cs b/TheOtherRoles/Modules/CustomHats/Patches/PlayerPhysicsPatches.cs
new file mode 100644 (file)
index 0000000..e454b70
--- /dev/null
@@ -0,0 +1,45 @@
+using TheOtherRoles.Modules.CustomHats.Extensions;
+using HarmonyLib;
+
+namespace TheOtherRoles.Modules.CustomHats.Patches;
+
+[HarmonyPatch(typeof(PlayerPhysics))]
+internal static class PlayerPhysicsPatches
+{
+    [HarmonyPatch(nameof(PlayerPhysics.HandleAnimation))]
+    [HarmonyPostfix]
+    private static void HandleAnimationPostfix(PlayerPhysics __instance)
+    {
+        var currentAnimation = __instance.Animations.Animator.GetCurrentAnimation();
+        if (currentAnimation == __instance.Animations.group.ClimbUpAnim) return;
+        if (currentAnimation == __instance.Animations.group.ClimbDownAnim) return;
+        var hatParent = __instance.myPlayer.cosmetics.hat;
+        if (hatParent == null || hatParent == null) return;
+        if (!hatParent.TryGetCached(out var viewData)) return;
+        var extend = hatParent.Hat.GetHatExtension();
+        if (extend == null) return;
+        if (extend.FlipImage != null)
+        {
+            if (__instance.FlipX)
+            {
+                hatParent.FrontLayer.sprite = extend.FlipImage;
+            }
+            else
+            {
+                hatParent.FrontLayer.sprite = viewData.MainImage;
+            }
+        }
+
+        if (extend.BackFlipImage != null)
+        {
+            if (__instance.FlipX)
+            {
+                hatParent.BackLayer.sprite = extend.BackFlipImage;
+            }
+            else
+            {
+                hatParent.BackLayer.sprite = viewData.BackImage;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/TheOtherRoles/Modules/CustomHats/SkinsConfigFile.cs b/TheOtherRoles/Modules/CustomHats/SkinsConfigFile.cs
new file mode 100644 (file)
index 0000000..a840cca
--- /dev/null
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace TheOtherRoles.Modules.CustomHats;
+
+public class SkinsConfigFile
+{
+    [JsonPropertyName("hats")] public List<CustomHat> Hats { get; set; }
+}
\ No newline at end of file
index d751e994bf92e6585fbc1e0f9058d98eb5032f05..b072bf5d6b00327aae7a5f45e5816e36212bc924 100644 (file)
@@ -893,6 +893,7 @@ namespace TheOtherRoles {
     public class AmongUsClientOnPlayerJoinedPatch {
         public static void Postfix() {
             if (PlayerControl.LocalPlayer != null && AmongUsClient.Instance.AmHost) {
+                GameManager.Instance.LogicOptions.SyncOptions();
                 CustomOption.ShareOptionSelections();
             }
         }
@@ -1129,6 +1130,89 @@ namespace TheOtherRoles {
         }
     }
 
+    [HarmonyPatch]
+    public class AddToKillDistanceSetting
+    {
+        [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.AreInvalid))]
+        [HarmonyPrefix]
+        
+        public static bool Prefix(GameOptionsData __instance, ref int maxExpectedPlayers)
+        {
+            //making the killdistances bound check higher since extra short is added
+            return __instance.MaxPlayers > maxExpectedPlayers || __instance.NumImpostors < 1
+                    || __instance.NumImpostors > 3 || __instance.KillDistance < 0
+                    || __instance.KillDistance >= GameOptionsData.KillDistances.Count
+                    || __instance.PlayerSpeedMod <= 0f || __instance.PlayerSpeedMod > 3f;
+        }
+
+        [HarmonyPatch(typeof(NormalGameOptionsV07), nameof(NormalGameOptionsV07.AreInvalid))]
+        [HarmonyPrefix]
+        
+        public static bool Prefix(NormalGameOptionsV07 __instance, ref int maxExpectedPlayers)
+        {
+            return __instance.MaxPlayers > maxExpectedPlayers || __instance.NumImpostors < 1
+                    || __instance.NumImpostors > 3 || __instance.KillDistance < 0
+                    || __instance.KillDistance >= GameOptionsData.KillDistances.Count
+                    || __instance.PlayerSpeedMod <= 0f || __instance.PlayerSpeedMod > 3f;
+        }
+
+        [HarmonyPatch(typeof(StringOption), nameof(StringOption.OnEnable))]
+        [HarmonyPrefix]
+        
+        public static void Prefix(StringOption __instance)
+        {
+            //prevents indexoutofrange exception breaking the setting if long happens to be selected
+            //when host opens the laptop
+            if (__instance.Title == StringNames.GameKillDistance && __instance.Value == 3) {
+                __instance.Value = 1;
+                GameOptionsManager.Instance.currentNormalGameOptions.KillDistance = 1;
+                GameManager.Instance.LogicOptions.SyncOptions();
+            }
+        }
+
+        [HarmonyPatch(typeof(StringOption), nameof(StringOption.OnEnable))]
+        [HarmonyPostfix]
+        
+        public static void Postfix(StringOption __instance)
+        {
+            if (__instance.Title == StringNames.GameKillDistance && __instance.Values.Count == 3) {
+                __instance.Values = new(
+                        new StringNames[] { (StringNames)49999, StringNames.SettingShort, StringNames.SettingMedium, StringNames.SettingLong });
+            }
+        }
+
+        [HarmonyPatch(typeof(IGameOptionsExtensions), nameof(IGameOptionsExtensions.AppendItem),
+            new Type[] { typeof(Il2CppSystem.Text.StringBuilder), typeof(StringNames), typeof(string) })]
+        [HarmonyPrefix]
+        
+        public static void Prefix(ref StringNames stringName, ref string value)
+        {
+            if (stringName == StringNames.GameKillDistance) {
+                var index = GameOptionsManager.Instance.currentNormalGameOptions.KillDistance;
+                value = GameOptionsData.KillDistanceStrings[index];
+            }
+        }
+
+        [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString),
+            new[] { typeof(StringNames), typeof(Il2CppReferenceArray<Il2CppSystem.Object>) })]
+        [HarmonyPriority(Priority.Last)]
+        
+        public static bool Prefix(ref string __result, ref StringNames id)
+        {
+            if ((int)id == 49999) {
+                __result = "Very Short";
+                return false;
+            }
+            return true;
+        }
+
+        public static void addKillDistance()
+        {
+            GameOptionsData.KillDistances = new(new float[] { 0.5f, 1f, 1.8f, 2.5f });
+            GameOptionsData.KillDistanceStrings = new(new string[] { "Very Short", "Short", "Medium", "Long" });
+        }
+    }
+
     [HarmonyPatch(typeof(KeyboardJoystick), nameof(KeyboardJoystick.Update))]
     public static class GameOptionsNextPagePatch
     {
index 44dda533f487bcad3e04219b9862258db9d5437a..65b15100f444ccec087803a36cb08314b7bb3aa1 100644 (file)
-using System;
+using System;
 using System.Collections;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Net.Http;
-using System.Reflection;
-using System.Threading.Tasks;
-using AmongUs.Data;
-using Assets.InnerNet;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using BepInEx;
-using BepInEx.Bootstrap;
-using BepInEx.Unity.IL2CPP;
 using BepInEx.Unity.IL2CPP.Utils;
-using Mono.Cecil;
-using Newtonsoft.Json.Linq;
-using TMPro;
-using Twitch;
 using UnityEngine;
-using UnityEngine.SceneManagement;
+using UnityEngine.Networking;
 using UnityEngine.UI;
-using Action = System.Action;
-using IntPtr = System.IntPtr;
-using Version = SemanticVersioning.Version;
-
-namespace TheOtherRoles.Modules 
-{
-    public class ModUpdateBehaviour : MonoBehaviour
-    {
-        public static readonly bool CheckForSubmergedUpdates = true;
-        public static bool showPopUp = true;
-        public static bool updateInProgress = false;
-
-        public static ModUpdateBehaviour Instance { get; private set; }
-        public ModUpdateBehaviour(IntPtr ptr) : base(ptr) { }
-        public class UpdateData
-        {
-            public string Content;
-            public string Tag;
-            public string TimeString;
-            public JObject Request;
-            public Version Version => Version.Parse(Tag);
-
-            public UpdateData(JObject data)
-            {
-                Tag = data["tag_name"]?.ToString().TrimStart('v');
-                Content = data["body"]?.ToString();
-                TimeString = DateTime.FromBinary(((Il2CppSystem.DateTime)data["published_at"]).ToBinaryRaw()).ToString();
-                Request = data;
-            }
+using UnityEngine.SceneManagement;
+using AmongUs.Data;
+using Assets.InnerNet;
+using Twitch;
+using static StarGen;
 
-            public bool IsNewer(Version version)
-            {
-                if (!Version.TryParse(Tag, out var myVersion)) return false;
-                return myVersion.BaseVersion() > version.BaseVersion();
+namespace TheOtherRoles.Modules {
+    public class ModUpdater : MonoBehaviour {
+        public const string RepositoryOwner = "TheOtherRolesAU";
+        public const string RepositoryName = "TheOtherRoles";
+        public static ModUpdater Instance { get; private set; }
+
+        public ModUpdater(IntPtr ptr) : base(ptr) { }
+
+        private bool _busy;
+        private bool showPopUp = true;
+        public List<GithubRelease> Releases;
+
+        public void Awake() {
+            if (Instance) Destroy(Instance);
+            Instance = this;
+            foreach (var file in Directory.GetFiles(Paths.PluginPath, "*.old")) {
+                File.Delete(file);
             }
         }
 
-        public UpdateData TORUpdate;
-        public UpdateData SubmergedUpdate;
-
-        [HideFromIl2Cpp]
-        public UpdateData RequiredUpdateData => TORUpdate ?? SubmergedUpdate;
+        private void Start() {
+            if (_busy) return;
+            this.StartCoroutine(CoCheckForUpdate());
+            SceneManager.add_sceneLoaded((System.Action<Scene, LoadSceneMode>)(OnSceneLoaded));
+        }
         
-        public void Awake()
-        {
-            if (Instance) Destroy(this);
-            Instance = this;
             
-            SceneManager.add_sceneLoaded((System.Action<Scene, LoadSceneMode>) (OnSceneLoaded));
-            this.StartCoroutine(CoCheckUpdates());
+        [HideFromIl2Cpp]
+        public void StartDownloadRelease(GithubRelease release) {
+            if (_busy) return;
+            this.StartCoroutine(CoDownloadRelease(release));
+        }
+
+        [HideFromIl2Cpp]
+        private IEnumerator CoCheckForUpdate() {
+            _busy = true;
+            var www = new UnityWebRequest();
+            www.SetMethod(UnityWebRequest.UnityWebRequestMethod.Get);
+            www.SetUrl($"https://api.github.com/repos/{RepositoryOwner}/{RepositoryName}/releases");
+            www.downloadHandler = new DownloadHandlerBuffer();
+            var operation = www.SendWebRequest();
+
+            while (!operation.isDone) {
+                yield return new WaitForEndOfFrame();
+            }
+
+            if (www.isNetworkError || www.isHttpError) {
+                yield break;
+            }
+
+            Releases = JsonSerializer.Deserialize<List<GithubRelease>>(www.downloadHandler.text);
+            www.downloadHandler.Dispose();
+            www.Dispose();
+            Releases.Sort(SortReleases);
+            _busy = false;
+        }
+
+        [HideFromIl2Cpp]
+        private IEnumerator CoDownloadRelease(GithubRelease release) {
+            _busy = true;
+
+            var popup = Instantiate(TwitchManager.Instance.TwitchPopup);
+            popup.TextAreaTMP.fontSize *= 0.7f;
+            popup.TextAreaTMP.enableAutoSizing = false;
+
+            popup.Show();
+
+            var button = popup.transform.GetChild(2).gameObject;
+            button.SetActive(false);
+            popup.TextAreaTMP.text = $"Updating TOR\nPlease wait...";
+
+            var asset = release.Assets.Find(FilterPluginAsset);
+            var www = new UnityWebRequest();
+            www.SetMethod(UnityWebRequest.UnityWebRequestMethod.Get);
+            www.SetUrl(asset.DownloadUrl);
+            www.downloadHandler = new DownloadHandlerBuffer();
+            var operation = www.SendWebRequest();
+
+            while (!operation.isDone) {
+                int stars = Mathf.CeilToInt(www.downloadProgress * 10);
+                string progress = $"Updating TOR\nPlease wait...\nDownloading...\n{new String((char)0x25A0, stars) + new String((char)0x25A1, 10 - stars)}";
+                popup.TextAreaTMP.text = progress;
+                yield return new WaitForEndOfFrame();
+            }
             
-            foreach (var file in Directory.GetFiles(Paths.PluginPath, "*.old"))
-            {
-                File.Delete(file);
+            if (www.isNetworkError || www.isHttpError) {
+                popup.TextAreaTMP.text = "Update wasn't successful\nTry again later,\nor update manually.";
+                yield break;
             }
+            popup.TextAreaTMP.text = $"Updating TOR\nPlease wait...\n\nDownload complete\ncopying file...";
+
+            var filePath = Path.Combine(Paths.PluginPath, asset.Name);
+
+            if (File.Exists(filePath + ".old")) File.Delete(filePath + "old");
+            if (File.Exists(filePath)) File.Move(filePath, filePath + ".old");
+
+            var persistTask = File.WriteAllBytesAsync(filePath, www.downloadHandler.data);
+            var hasError = false;
+            while (!persistTask.IsCompleted) {
+                if (persistTask.Exception != null) {
+                    hasError = true;
+                    break;
+                }
+                
+                yield return new WaitForEndOfFrame();
+            }
+
+            www.downloadHandler.Dispose();
+            www.Dispose();
+
+            if (!hasError) {
+                popup.TextAreaTMP.text = $"TheOtherRoles\nupdated successfully\nPlease restart the game.";
+            }
+            button.SetActive(true);
+            _busy = false;
+        }
+
+        [HideFromIl2Cpp]
+        private static bool FilterLatestRelease(GithubRelease release) {
+            return release.IsNewer(TheOtherRolesPlugin.Version) && release.Assets.Any(FilterPluginAsset);
+        }
+
+        [HideFromIl2Cpp]
+        private static bool FilterPluginAsset(GithubAsset asset) {
+            return asset.Name == "TheOtherRoles.dll";
         }
 
-        private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
-        {
-            if (updateInProgress || scene.name != "MainMenu") return;
-            if (RequiredUpdateData is null) {
-                showPopUp = false;
+        [HideFromIl2Cpp]
+        private static int SortReleases(GithubRelease a, GithubRelease b) {
+            if (a.IsNewer(b.Version)) return -1;
+            if (b.IsNewer(a.Version)) return 1;
+            return 0;
+        }
+
+        private void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
+            if (_busy || scene.name != "MainMenu") return;
+            var latestRelease = Releases.FirstOrDefault();
+            if (latestRelease == null || latestRelease.Version <= TheOtherRolesPlugin.Version)
                 return;
-            }
 
             var template = GameObject.Find("ExitGameButton");
             if (!template) return;
-            
+
             var button = Instantiate(template, null);
             var buttonTransform = button.transform;
             //buttonTransform.localPosition = new Vector3(-2f, -2f);
@@ -93,74 +166,26 @@ namespace TheOtherRoles.Modules
 
             PassiveButton passiveButton = button.GetComponent<PassiveButton>();
             passiveButton.OnClick = new Button.ButtonClickedEvent();
-            passiveButton.OnClick.AddListener((Action) (() =>
+            passiveButton.OnClick.AddListener((Action)(() =>
             {
-                this.StartCoroutine(CoUpdate());
+                StartDownloadRelease(latestRelease);
                 button.SetActive(false);
             }));
 
             var text = button.transform.GetComponentInChildren<TMPro.TMP_Text>();
             string t = "Update TOR";
-            if (TORUpdate is null && SubmergedUpdate is not null) t = SubmergedCompatibility.Loaded ? $"Update\nSubmerged" : $"Download\nSubmerged";
-
             StartCoroutine(Effects.Lerp(0.1f, (System.Action<float>)(p => text.SetText(t))));
             passiveButton.OnMouseOut.AddListener((Action)(() => text.color = Color.red));
             passiveButton.OnMouseOver.AddListener((Action)(() => text.color = Color.white));
-
-            var isSubmerged = TORUpdate == null;
-            var announcement = $"<size=150%>A new {(isSubmerged ? "Submerged" : "THE OTHER ROLES")} update to {(isSubmerged ? SubmergedUpdate.Tag : TORUpdate.Tag)} is available</size>\n{(isSubmerged ? SubmergedUpdate.Content : TORUpdate.Content)}";
+            var announcement = $"<size=150%>A new THE OTHER ROLES update to {latestRelease.Tag} is available</size>\n{latestRelease.Description}";
             var mgr = FindObjectOfType<MainMenuManager>(true);
-
-            if (!isSubmerged) {
-                try {
-                    string updateVersion = TORUpdate.Content[^5..];
-                    if (Version.Parse(TheOtherRolesPlugin.VersionString).BaseVersion() < Version.Parse(updateVersion).BaseVersion()) {
-                        passiveButton.OnClick.RemoveAllListeners();
-                        passiveButton.OnClick = new Button.ButtonClickedEvent();
-                        passiveButton.OnClick.AddListener((Action)(() => {
-                            mgr.StartCoroutine(CoShowAnnouncement($"<size=150%>A MANUAL UPDATE IS REQUIRED</size>"));
-                        }));
-                    }
-                } catch {  
-                    TheOtherRolesPlugin.Logger.LogError("parsing version for auto updater failed :(");
-                }
-
-            }
-            
-            if (isSubmerged && !SubmergedCompatibility.Loaded) showPopUp = false;
-            if (showPopUp) mgr.StartCoroutine(CoShowAnnouncement(announcement, shortTitle: isSubmerged ? "Submerged Update" : "TOR Update", date: isSubmerged ? SubmergedUpdate.TimeString : TORUpdate.TimeString));
+            if (showPopUp) mgr.StartCoroutine(CoShowAnnouncement(announcement, shortTitle: "TOR Update", date : latestRelease.PublishedAt)) ;
             showPopUp = false;
-        }
-        
-        [HideFromIl2Cpp]
-        public IEnumerator CoUpdate()
-        {
-            updateInProgress = true;
-            var isSubmerged = TORUpdate is null;
-            var updateName = (isSubmerged ? "Submerged" : "The Other Roles");
-            
-            var popup = Instantiate(TwitchManager.Instance.TwitchPopup);
-            popup.TextAreaTMP.fontSize *= 0.7f;
-            popup.TextAreaTMP.enableAutoSizing = false;
-            
-            popup.Show();
 
-            var button = popup.transform.GetChild(2).gameObject;
-            button.SetActive(false);
-            popup.TextAreaTMP.text = $"Updating {updateName}\nPlease wait...";
-            
-            var download = Task.Run(DownloadUpdate);
-            while (!download.IsCompleted) yield return null;
-            
-            button.SetActive(true);
-            popup.TextAreaTMP.text = download.Result ? $"{updateName}\nupdated successfully\nPlease restart the game." : "Update wasn't successful\nTry again later,\nor update manually.";
         }
 
-
-        private static int announcementNumber = 501;
         [HideFromIl2Cpp]
-        public IEnumerator CoShowAnnouncement(string announcement, bool show=true, string shortTitle="TOR Update", string title="", string date="")
-        {
+        public IEnumerator CoShowAnnouncement(string announcement, bool show = true, string shortTitle = "TOR Update", string title = "", string date = "") {
             var mgr = FindObjectOfType<MainMenuManager>(true);
             var popUpTemplate = UnityEngine.Object.FindObjectOfType<AnnouncementPopUp>(true);
             if (popUpTemplate == null) {
@@ -169,12 +194,12 @@ namespace TheOtherRoles.Modules
             }
             var popUp = UnityEngine.Object.Instantiate(popUpTemplate);
 
-            popUp.gameObject.SetActive(true);            
+            popUp.gameObject.SetActive(true);
 
             Assets.InnerNet.Announcement creditsAnnouncement = new() {
                 Id = "torAnnouncement",
                 Language = 0,
-                Number = announcementNumber++,
+                Number = 6969,
                 Title = title == "" ? "The Other Roles Announcement" : title,
                 ShortTitle = shortTitle,
                 SubTitle = "",
@@ -195,112 +220,57 @@ namespace TheOtherRoles.Modules
                 }
             })));
         }
+    }
 
-        [HideFromIl2Cpp]
-        public static IEnumerator CoCheckUpdates()
-        {
-            // Since running the update check task causes a crash for some users, allow the user to disable the updater by creating a file called noupdater.txt
-            // in their Among Us folder (root)
-            if (System.IO.File.Exists(System.IO.Directory.GetCurrentDirectory() + "\\noupdater.txt")) yield break;
-            var torUpdateCheck = Task.Run(() => Instance.GetGithubUpdate("Eisbison", "TheOtherRoles"));
-            while (!torUpdateCheck.IsCompleted) yield return null;
-            if (torUpdateCheck.Result != null && torUpdateCheck.Result.IsNewer(Version.Parse(TheOtherRolesPlugin.VersionString)))
-            {
-                Instance.TORUpdate = torUpdateCheck.Result;
-            }
-            if (CheckForSubmergedUpdates)
-            {
-                var submergedUpdateCheck = Task.Run(() => Instance.GetGithubUpdate("SubmergedAmongUs", "Submerged"));
-                while (!submergedUpdateCheck.IsCompleted) yield return null;
-                if (submergedUpdateCheck.Result != null && (!SubmergedCompatibility.Loaded || submergedUpdateCheck.Result.IsNewer(SubmergedCompatibility.Version)))
-                {
-                    Instance.SubmergedUpdate = submergedUpdateCheck.Result;
-                    if (Instance.SubmergedUpdate.Tag.Equals("2022.10.26") || IL2CPPChainloader.Instance.Plugins.TryGetValue("com.DigiWorm.LevelImposter", out PluginInfo _)) Instance.SubmergedUpdate = null;
-                }
-            }
-            Instance.OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
-        }
+    public class GithubRelease {
+        [JsonPropertyName("id")]
+        public int Id { get; set; }
 
-        [HideFromIl2Cpp]
-        public async Task<UpdateData> GetGithubUpdate(string owner, string repo)
-        {
-            var client = new HttpClient();
-            client.DefaultRequestHeaders.Add("User-Agent", "TheOtherRoles Updater");
-            try {
-                var req = await client.GetAsync($"https://api.github.com/repos/{owner}/{repo}/releases/latest", HttpCompletionOption.ResponseContentRead);
-                if (!req.IsSuccessStatusCode) return null;
-
-                var dataString = await req.Content.ReadAsStringAsync();
-                JObject data = JObject.Parse(dataString);
-                return new UpdateData(data);
-            } catch (HttpRequestException) {
-                return null;
-            }
-        }
+        [JsonPropertyName("tag_name")]
+        public string Tag { get; set; }
 
-        private bool TryUpdateSubmergedInternally()
-        {
-            if (SubmergedUpdate == null) return false;
-            try
-            {
-                if (!SubmergedCompatibility.LoadedExternally) return false;
-                var thisAsm = Assembly.GetCallingAssembly();
-                var resourceName = thisAsm.GetManifestResourceNames().FirstOrDefault(s => s.EndsWith("Submerged.dll"));
-                if (resourceName == default) return false;
-
-                using var submergedStream = thisAsm.GetManifestResourceStream(resourceName)!;
-                var asmDef = AssemblyDefinition.ReadAssembly(submergedStream, TypeLoader.ReaderParameters);
-                var pluginType = asmDef.MainModule.Types.FirstOrDefault(t => t.IsSubtypeOf(typeof(BasePlugin)));
-                var info = IL2CPPChainloader.ToPluginInfo(pluginType, "");
-                if (SubmergedUpdate.IsNewer(info.Metadata.Version)) return false;
-                File.Delete(SubmergedCompatibility.Assembly.Location);
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
 
-            }
-            catch (Exception e)
-            {
-                TheOtherRolesPlugin.Logger.LogError(e);
-                return false;
-            }
-            return true;
-        }
-            
-        
-        [HideFromIl2Cpp]
-        public async Task<bool> DownloadUpdate()
-        {
-            var isSubmerged = TORUpdate is null;
-            if (isSubmerged && TryUpdateSubmergedInternally()) return true;
-            var data = isSubmerged ? SubmergedUpdate : TORUpdate;
-            
-            var client = new HttpClient();
-            client.DefaultRequestHeaders.Add("User-Agent", "TheOtherRoles Updater");
-            
-            JToken assets = data.Request["assets"];
-            string downloadURI = "";
-            for (JToken current = assets.First; current != null; current = current.Next) 
-            {
-                string browser_download_url = current["browser_download_url"]?.ToString();
-                if (browser_download_url != null && current["content_type"] != null) {
-                    if (current["content_type"].ToString().Equals("application/x-msdownload") &&
-                        browser_download_url.EndsWith(".dll")) {
-                        downloadURI = browser_download_url;
-                        break;
-                    }
-                }
-            }
+        [JsonPropertyName("draft")]
+        public bool Draft { get; set; }
 
-            if (downloadURI.Length == 0) return false;
+        [JsonPropertyName("prerelease")]
+        public bool Prerelease { get; set; }
 
-            var res = await client.GetAsync(downloadURI, HttpCompletionOption.ResponseContentRead);
-            string filePath = Path.Combine(Paths.PluginPath, isSubmerged ? "Submerged.dll" : "TheOtherRoles.dll");
-            if (File.Exists(filePath + ".old")) File.Delete(filePath + ".old");
-            if (File.Exists(filePath)) File.Move(filePath, filePath + ".old");
+        [JsonPropertyName("created_at")]
+        public string CreatedAt { get; set; }
+
+        [JsonPropertyName("published_at")]
+        public string PublishedAt { get; set; }
+
+        [JsonPropertyName("body")]
+        public string Description { get; set; }
 
-            await using var responseStream = await res.Content.ReadAsStreamAsync();
-            await using var fileStream = File.Create(filePath);
-            await responseStream.CopyToAsync(fileStream);
+        [JsonPropertyName("assets")]
+        public List<GithubAsset> Assets { get; set; }
 
-            return true;
+        public Version Version => Version.Parse(Tag.Replace("v", string.Empty));
+
+        public bool IsNewer(Version version) {
+            return Version > version;
         }
     }
+
+    public class GithubAsset {
+        [JsonPropertyName("url")]
+        public string Url { get; set; }
+
+        [JsonPropertyName("id")]
+        public int Id { get; set; }
+
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+
+        [JsonPropertyName("size")]
+        public int Size { get; set; }
+
+        [JsonPropertyName("browser_download_url")]
+        public string DownloadUrl { get; set; }
+    }
 }
index 6b9ec645e8178f96740027926adf9c58437d4b59..b3af38f36d1678add8176180deada543eab93454 100644 (file)
@@ -46,7 +46,7 @@ namespace TheOtherRoles.Objects {
 
             FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(10f, new Action<float>((p) => {
             Color c = color;
-            if (Camouflager.camouflageTimer > 0) c = Palette.PlayerColors[6];
+            if (Camouflager.camouflageTimer > 0 || Helpers.MushroomSabotageActive()) c = Palette.PlayerColors[6];
             if (spriteRenderer) spriteRenderer.color = new Color(c.r, c.g, c.b, Mathf.Clamp01(1 - p));
 
             if (p == 1f && blood != null) {
index c3aa892c3e8e7fac39fef16b187faf85a8f9ff6f..8d8138e61ed847b7e95f97e52d8a2c029dd62c00 100644 (file)
@@ -86,7 +86,7 @@ namespace TheOtherRoles.Objects
                 }
                 
                 Color color;
-                if (AnonymousFootprints || Camouflager.camouflageTimer > 0)
+                if (AnonymousFootprints || Camouflager.camouflageTimer > 0 || Helpers.MushroomSabotageActive())
                 {
                     color = Palette.PlayerColors[6];
                 }
index 152513d1eb4611edab76df4b2d1637d247c2d0c0..e44ba283607fcf59d4996238db5b05c101fa6866 100644 (file)
@@ -4,6 +4,7 @@ using UnityEngine;
 using System.Linq;
 using TheOtherRoles.Players;
 using TheOtherRoles.Utilities;
+using Reactor.Utilities.Extensions;
 
 namespace TheOtherRoles.Objects {
 
@@ -36,6 +37,7 @@ namespace TheOtherRoles.Objects {
         private GameObject gameObject;
         public Vent vent;
         private SpriteRenderer boxRenderer;
+        private SpriteRenderer ventRenderer;
 
         public JackInTheBox(Vector2 p) {
             gameObject = new GameObject("JackInTheBox"){layer = 11};
@@ -46,6 +48,7 @@ namespace TheOtherRoles.Objects {
             gameObject.transform.position = position;
             boxRenderer = gameObject.AddComponent<SpriteRenderer>();
             boxRenderer.sprite = getBoxAnimationSprite(0);
+            boxRenderer.color = boxRenderer.color.SetAlpha(0.5f);
 
             // Create the vent
             var referenceVent = UnityEngine.Object.FindObjectOfType<Vent>();
@@ -60,7 +63,13 @@ namespace TheOtherRoles.Objects {
             vent.Offset = new Vector3(0f, 0.25f, 0f);
             vent.GetComponent<PowerTools.SpriteAnim>()?.Stop();
             vent.Id = MapUtilities.CachedShipStatus.AllVents.Select(x => x.Id).Max() + 1; // Make sure we have a unique id
-            var ventRenderer = vent.GetComponent<SpriteRenderer>();
+            ventRenderer = vent.GetComponent<SpriteRenderer>();
+            if (Helpers.isFungle()) {
+                ventRenderer = vent.transform.GetChild(3).GetComponent<SpriteRenderer>();
+                var animator = vent.transform.GetChild(3).GetComponent<PowerTools.SpriteAnim>();
+                animator?.Stop();
+            }
+            //ventRenderer.Destroy();
             ventRenderer.sprite = null;  // Use the box.boxRenderer instead
             vent.myRend = ventRenderer;
             var allVentsList = MapUtilities.CachedShipStatus.AllVents.ToList();
@@ -69,9 +78,9 @@ namespace TheOtherRoles.Objects {
             vent.gameObject.SetActive(false);
             vent.name = "JackInTheBoxVent_" + vent.Id;
 
-            // Only render the box for the Trickster
-            var playerIsTrickster = CachedPlayer.LocalPlayer.PlayerControl == Trickster.trickster;
-            gameObject.SetActive(playerIsTrickster);
+            // Only render the box for the Trickster and for Ghosts
+            var showBoxToLocalPlayer = CachedPlayer.LocalPlayer.PlayerControl == Trickster.trickster || PlayerControl.LocalPlayer.Data.IsDead;
+            gameObject.SetActive(showBoxToLocalPlayer);
 
             AllJackInTheBoxes.Add(this);
         }
@@ -79,14 +88,16 @@ namespace TheOtherRoles.Objects {
         public static void UpdateStates() {
             if (boxesConvertedToVents == true) return;
             foreach (var box in AllJackInTheBoxes) {
-                var playerIsTrickster = CachedPlayer.LocalPlayer.PlayerControl == Trickster.trickster;
-                box.gameObject.SetActive(playerIsTrickster);
+                var showBoxToLocalPlayer = CachedPlayer.LocalPlayer.PlayerControl == Trickster.trickster || PlayerControl.LocalPlayer.Data.IsDead;
+                box.gameObject.SetActive(showBoxToLocalPlayer);
             }
         }
 
         public void convertToVent() {
             gameObject.SetActive(true);
             vent.gameObject.SetActive(true);
+            boxRenderer.color = boxRenderer.color.SetAlpha(1f);
+            ventRenderer.sprite = null;
             return;
         }
 
index d68e9af4eebeaf1ba4eae5ece3ef9813e8151927..a512a322ac7369f7e23a914b8a1abb8a16f83045 100644 (file)
@@ -52,7 +52,7 @@ namespace TheOtherRoles.Objects {
 
             int colorId = playerControl.Data.DefaultOutfit.ColorId;
 
-            if (Camouflager.camouflageTimer > 0) {
+            if (Camouflager.camouflageTimer > 0 || Helpers.MushroomSabotageActive()) {
                 playerNameDisplay = "A camouflaged player";
                 colorId = 6;
             }
diff --git a/TheOtherRoles/Patches/AirShipSetAntiTpPosition.cs b/TheOtherRoles/Patches/AirShipSetAntiTpPosition.cs
deleted file mode 100644 (file)
index fcfd22d..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-using HarmonyLib;
-using System;
-
-namespace TheOtherRoles.Patches {
-    [HarmonyPatch]
-    public static class AirShipSetAntiTpPosition {
-
-        // Save the position of the player prior to starting the climb / gap platform
-        [HarmonyPrefix]
-        [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.ClimbLadder))]
-        public static void prefix() {
-            AntiTeleport.position = Players.CachedPlayer.LocalPlayer.transform.position;
-        }
-
-        [HarmonyPrefix]
-        [HarmonyPatch(typeof(MovingPlatformBehaviour), nameof(MovingPlatformBehaviour.UsePlatform))]
-        public static void prefix2() {
-            AntiTeleport.position = Players.CachedPlayer.LocalPlayer.transform.position;
-        }
-    }
-}
index 132929b2dcb8ae0c65a1d1782ebc6da9bb1390b4..f212c5cf87994ba6e09585d507dd372af8654b48 100644 (file)
@@ -13,14 +13,15 @@ namespace TheOtherRoles.Patches
     [HarmonyPatch]
     public static class ClientOptionsPatch
     {
-        private static SelectionBehaviour[] AllOptions = {
-            new SelectionBehaviour("Ghosts See Tasks & Other Info", () => TORMapOptions.ghostsSeeInformation = TheOtherRolesPlugin.GhostsSeeInformation.Value = !TheOtherRolesPlugin.GhostsSeeInformation.Value, TheOtherRolesPlugin.GhostsSeeInformation.Value),
-            new SelectionBehaviour("Ghosts Can See Votes", () => TORMapOptions.ghostsSeeVotes = TheOtherRolesPlugin.GhostsSeeVotes.Value = !TheOtherRolesPlugin.GhostsSeeVotes.Value, TheOtherRolesPlugin.GhostsSeeVotes.Value),
-            new SelectionBehaviour("Ghosts Can See Roles", () => TORMapOptions.ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value = !TheOtherRolesPlugin.GhostsSeeRoles.Value, TheOtherRolesPlugin.GhostsSeeRoles.Value),
-            new SelectionBehaviour("Ghosts Can Additionally See Modifier", () => TORMapOptions.ghostsSeeModifier = TheOtherRolesPlugin.GhostsSeeModifier.Value = !TheOtherRolesPlugin.GhostsSeeModifier.Value, TheOtherRolesPlugin.GhostsSeeModifier.Value),
-            new SelectionBehaviour("Show Role Summary", () => TORMapOptions.showRoleSummary = TheOtherRolesPlugin.ShowRoleSummary.Value = !TheOtherRolesPlugin.ShowRoleSummary.Value, TheOtherRolesPlugin.ShowRoleSummary.Value),
-            new SelectionBehaviour("Show Lighter / Darker", () => TORMapOptions.showLighterDarker = TheOtherRolesPlugin.ShowLighterDarker.Value = !TheOtherRolesPlugin.ShowLighterDarker.Value, TheOtherRolesPlugin.ShowLighterDarker.Value),
-            new SelectionBehaviour("Enable Sound Effects", () => TORMapOptions.enableSoundEffects = TheOtherRolesPlugin.EnableSoundEffects.Value = !TheOtherRolesPlugin.EnableSoundEffects.Value, TheOtherRolesPlugin.EnableSoundEffects.Value),
+        private static readonly SelectionBehaviour[] AllOptions = {
+            new("Ghosts See Tasks & Other Info", () => TORMapOptions.ghostsSeeInformation = TheOtherRolesPlugin.GhostsSeeInformation.Value = !TheOtherRolesPlugin.GhostsSeeInformation.Value, TheOtherRolesPlugin.GhostsSeeInformation.Value),
+            new("Ghosts Can See Votes", () => TORMapOptions.ghostsSeeVotes = TheOtherRolesPlugin.GhostsSeeVotes.Value = !TheOtherRolesPlugin.GhostsSeeVotes.Value, TheOtherRolesPlugin.GhostsSeeVotes.Value),
+            new("Ghosts Can See Roles", () => TORMapOptions.ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value = !TheOtherRolesPlugin.GhostsSeeRoles.Value, TheOtherRolesPlugin.GhostsSeeRoles.Value),
+            new("Ghosts Can Additionally See Modifier", () => TORMapOptions.ghostsSeeModifier = TheOtherRolesPlugin.GhostsSeeModifier.Value = !TheOtherRolesPlugin.GhostsSeeModifier.Value, TheOtherRolesPlugin.GhostsSeeModifier.Value),
+            new("Show Role Summary", () => TORMapOptions.showRoleSummary = TheOtherRolesPlugin.ShowRoleSummary.Value = !TheOtherRolesPlugin.ShowRoleSummary.Value, TheOtherRolesPlugin.ShowRoleSummary.Value),
+            new("Show Lighter / Darker", () => TORMapOptions.showLighterDarker = TheOtherRolesPlugin.ShowLighterDarker.Value = !TheOtherRolesPlugin.ShowLighterDarker.Value, TheOtherRolesPlugin.ShowLighterDarker.Value),
+            new("Enable Sound Effects", () => TORMapOptions.enableSoundEffects = TheOtherRolesPlugin.EnableSoundEffects.Value = !TheOtherRolesPlugin.EnableSoundEffects.Value, TheOtherRolesPlugin.EnableSoundEffects.Value),
+            new("Show Vents On Map", () => TORMapOptions.ShowVentsOnMap = TheOtherRolesPlugin.ShowVentsOnMap.Value = !TheOtherRolesPlugin.ShowVentsOnMap.Value, TheOtherRolesPlugin.ShowVentsOnMap.Value),
         };
         
         private static GameObject popUp;
@@ -49,12 +50,11 @@ namespace TheOtherRoles.Patches
         public static void OptionsMenuBehaviour_StartPostfix(OptionsMenuBehaviour __instance)
         {
             if (!__instance.CensorChatButton) return;
-
+            
             if (!popUp)
             {
                 CreateCustom(__instance);
             }
-
             if (!buttonPrefab)
             {
                 buttonPrefab = Object.Instantiate(__instance.CensorChatButton);
@@ -108,12 +108,14 @@ namespace TheOtherRoles.Patches
             moreOptionsButton.OnClick = new ButtonClickedEvent();
             moreOptionsButton.OnClick.AddListener((Action) (() =>
             {
+                bool closeUnderlying = false;
                 if (!popUp) return;
 
                 if (__instance.transform.parent && __instance.transform.parent == FastDestroyableSingleton<HudManager>.Instance.transform)
                 {
                     popUp.transform.SetParent(FastDestroyableSingleton<HudManager>.Instance.transform);
                     popUp.transform.localPosition = new Vector3(0, 0, -800f);
+                    closeUnderlying = true;
                 }
                 else
                 {
@@ -123,6 +125,8 @@ namespace TheOtherRoles.Patches
                 
                 CheckSetTitle();
                 RefreshOpen();
+                if (closeUnderlying)
+                    __instance.Close();                    
             }));
         }
 
index b310363c68abab134093032a5eda80ac0312a5a7..c9a5fc3fbaab9f349154cd86dde3d0fe12918007 100644 (file)
@@ -1,11 +1,11 @@
-using  HarmonyLib;
+using HarmonyLib;
 
 namespace TheOtherRoles.Patches {
     [HarmonyPatch(typeof(Constants), nameof(Constants.GetBroadcastVersion))]
     public static class ConstantsPatch {
         public static void Postfix(ref int __result) {
             if (AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame)
-                __result = Constants.GetVersion(2222, 0, 0, 0);
+                __result += 25;
         }
     }
-}
+}
\ No newline at end of file
index 28b13aeb98b4df63338f5ba1a42159073bda5aa4..9f8bcccc402a502da30c6e4306655002723f28fb 100644 (file)
@@ -25,7 +25,7 @@ $@"Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>Thunderstorm584
 Design by <color=#FCCE03FF>Bavari</color>";
 
         public static string contributorsCredentials =
-$@"<size=60%> <color=#FCCE03FF>Special thanks to K3ndo & Smeggy</color></size>";
+$@"<size=60%> <color=#FCCE03FF>Special thanks to Smeggy</color></size>";
 
         [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))]
         internal static class PingTrackerPatch
index 5a88ba2db0465508ea76d3d92efc44d157e713dd..8a1c2fd5778624834f3faf3de0d775be4db4b42c 100644 (file)
@@ -262,13 +262,13 @@ namespace TheOtherRoles.Patches {
                 float num7 = Mathf.Lerp(1f, 0.65f, num4) * 0.9f;
                 Vector3 vector = new Vector3(num7, num7, 1f);
                 poolablePlayer.transform.localScale = vector;
-                poolablePlayer.UpdateFromPlayerOutfit((GameData.PlayerOutfit) winningPlayerData2, PlayerMaterial.MaskType.ComplexUI, winningPlayerData2.IsDead, true);
                 if (winningPlayerData2.IsDead) {
-                    poolablePlayer.cosmetics.currentBodySprite.BodySprite.sprite = poolablePlayer.cosmetics.currentBodySprite.GhostSprite;
+                    poolablePlayer.SetBodyAsGhost();
                     poolablePlayer.SetDeadFlipX(i % 2 == 0);
                 } else {
                     poolablePlayer.SetFlipX(i % 2 == 0);
                 }
+                poolablePlayer.UpdateFromPlayerOutfit(winningPlayerData2, PlayerMaterial.MaskType.None, winningPlayerData2.IsDead, true);
 
                 poolablePlayer.cosmetics.nameText.color = Color.white;
                 poolablePlayer.cosmetics.nameText.transform.localScale = new Vector3(1f / vector.x, 1f / vector.y, 1f / vector.z);
index e2ad90a33fc07b29c1f0cdc476333206f756250d..63a4ef52a643e751d3477461aef6d7e8001a223d 100644 (file)
@@ -62,8 +62,7 @@ namespace TheOtherRoles.Patches {
 
                 if ((witchDiesWithExiledLover || exiledIsWitch) && Witch.witchVoteSavesTargets) Witch.futureSpelled = new List<PlayerControl>();
                 foreach (PlayerControl target in Witch.futureSpelled) {
-                    if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill)
-                    {
+                    if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill){
                         if (exiled != null && Lawyer.lawyer != null && (target == Lawyer.lawyer || target == Lovers.otherLover(Lawyer.lawyer)) && Lawyer.target != null && Lawyer.isProsecutor && Lawyer.target.PlayerId == exiled.PlayerId)
                             continue;
                         if (target == Lawyer.target && Lawyer.lawyer != null) {
@@ -101,13 +100,20 @@ namespace TheOtherRoles.Patches {
             TORMapOptions.camerasToAdd = new List<SurvCamera>();
 
             foreach (Vent vent in TORMapOptions.ventsToSeal) {
-                PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>(); 
-                animator?.Stop();
+                PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>();
                 vent.EnterVentAnim = vent.ExitVentAnim = null;
-                vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                Sprite newSprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                SpriteRenderer rend = vent.myRend;
+                if (Helpers.isFungle()) {
+                    newSprite = SecurityGuard.getFungleVentSealedSprite();
+                    rend = vent.transform.GetChild(3).GetComponent<SpriteRenderer>();
+                    animator = vent.transform.GetChild(3).GetComponent<PowerTools.SpriteAnim>();
+                }
+                animator?.Stop();
+                rend.sprite = newSprite;
                 if (SubmergedCompatibility.IsSubmerged && vent.Id == 0) vent.myRend.sprite = SecurityGuard.getSubmergedCentralUpperSealedSprite();
                 if (SubmergedCompatibility.IsSubmerged && vent.Id == 14) vent.myRend.sprite = SecurityGuard.getSubmergedCentralLowerSealedSprite();
-                vent.myRend.color = Color.white;
+                rend.color = Color.white;
                 vent.name = "SealedVent_" + vent.name;
             }
             TORMapOptions.ventsToSeal = new List<Vent>();
@@ -136,6 +142,13 @@ namespace TheOtherRoles.Patches {
         // Workaround to add a "postfix" to the destroying of the exile controller (i.e. cutscene) and SpwanInMinigame of submerged
         [HarmonyPatch(typeof(UnityEngine.Object), nameof(UnityEngine.Object.Destroy), new Type[] { typeof(GameObject) })]
         public static void Prefix(GameObject obj) {
+            // Nightvision:
+            if (obj != null && obj.name != null && obj.name.Contains("FungleSecurity")) {
+                SurveillanceMinigamePatch.resetNightVision();
+                return;
+            }
+
+            // submerged
             if (!SubmergedCompatibility.IsSubmerged) return;
             if (obj.name.Contains("ExileCutscene")) {
                 WrapUpPostfix(ExileControllerBeginPatch.lastExiled);
index a49d03a8c8993a238b184352b1cc797d723b690b..c6b25762682b2ec6c206ca80ec2fca1fcaaccf11 100644 (file)
@@ -50,6 +50,7 @@ namespace TheOtherRoles.Patches {
         
             public static void Prefix(GameStartManager __instance) {
                 if (!GameData.Instance ) return; // No instance
+                __instance.MinPlayers = 1;
                 update = GameData.Instance.PlayerCount != __instance.LastPlayerCount;
             }
 
@@ -187,7 +188,7 @@ namespace TheOtherRoles.Patches {
                             break;
                         }
                     }
-                    if ((continueStart && TORMapOptions.gameMode == CustomGamemodes.HideNSeek || TORMapOptions.gameMode == CustomGamemodes.PropHunt) && GameOptionsManager.Instance.CurrentGameOptions.MapId != 6) {
+                    if (continueStart && (TORMapOptions.gameMode == CustomGamemodes.HideNSeek || TORMapOptions.gameMode == CustomGamemodes.PropHunt) && GameOptionsManager.Instance.CurrentGameOptions.MapId != 6) {
                         byte mapId = 0;
                         if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek) mapId = (byte)CustomOptionHolder.hideNSeekMap.getSelection();
                         else if (TORMapOptions.gameMode == CustomGamemodes.PropHunt) mapId = (byte)CustomOptionHolder.propHuntMap.getSelection();
@@ -210,6 +211,7 @@ namespace TheOtherRoles.Patches {
                         probabilities.Add(CustomOptionHolder.dynamicMapEnableMira.getSelection() / 10f);
                         probabilities.Add(CustomOptionHolder.dynamicMapEnablePolus.getSelection() / 10f);
                         probabilities.Add(CustomOptionHolder.dynamicMapEnableAirShip.getSelection() / 10f);
+                        probabilities.Add(CustomOptionHolder.dynamicMapEnableFungle.getSelection() / 10f);
                         probabilities.Add(CustomOptionHolder.dynamicMapEnableSubmerged.getSelection() / 10f);
 
                         // if any map is at 100%, remove all maps that are not!
index e1f8951eb81300885abf18fe9449a0897f42bcb4..8dbdc85bc6ccf3bb1d20f04136aa4d0fb411b3e3 100644 (file)
@@ -21,7 +21,6 @@ namespace TheOtherRoles.Modules {
         private static AnnouncementPopUp popUp;
 
         private static void Prefix(MainMenuManager __instance) {
-            CustomHatLoader.LaunchHatFetcher();
             var template = GameObject.Find("ExitGameButton");
             var template2 = GameObject.Find("CreditsButton");
             if (template == null || template2 == null) return;
@@ -78,18 +77,27 @@ namespace TheOtherRoles.Modules {
                 popUp = Object.Instantiate(popUpTemplate);
 
                 popUp.gameObject.SetActive(true);
-                string creditsString = @$"<align=""center""><b>Github Contributors:</b>
+                string creditsString = @$"<align=""center""><b>Team:</b>
+Mallöris    K3ndo    Bavari    Gendelo
+
+<b>Former Team Members:</b>
+Eisbison (GOAT)    Thunderstorm584    EndOfFile
+
+<b>Additional Devs:</b>
+EnoPM    twix    NesTT
+
+<b>Github Contributors:</b>
 Alex2911    amsyarasyiq    MaximeGillot
 Psynomit    probablyadnf    JustASysAdmin
 
-[https://discord.gg/77RkMJHWsM]Discord[] Moderators:
-Streamblox    Draco Cordraconis
+<b>[https://discord.gg/77RkMJHWsM]Discord[] Moderators:</b>
+Draco Cordraconis    Streamblox (formerly)
 Thanks to all our discord helpers!
 
-Thanks to miniduikboot & GD for hosting modded servers
+Thanks to miniduikboot & GD for hosting modded servers (and so much more)
 
 ";
-                creditsString += $@"<size=60%> Other Credits & Resources:
+                creditsString += $@"<size=60%> <b>Other Credits & Resources:</b>
 OxygenFilter - For the versions v2.3.0 to v2.6.1, we were using the OxygenFilter for automatic deobfuscation
 Reactor - The framework used for all versions before v2.0.0, and again since 4.2.0
 BepInEx - Used to hook game functions
@@ -130,7 +138,7 @@ ugackMiner53 - Idea and core code for the Prop Hunt game mode</size>";
                         DataManager.Player.Announcements.SetAnnouncements(new Announcement[] { creditsAnnouncement });
                         popUp.CreateAnnouncementList();
                         popUp.UpdateAnnouncementText(creditsAnnouncement.Number);
-                        popUp.visibleAnnouncements[0].PassiveButton.OnClick.RemoveAllListeners();
+                        popUp.visibleAnnouncements._items[0].PassiveButton.OnClick.RemoveAllListeners();
                         DataManager.Player.Announcements.allAnnouncements = backup;
                     }
                 })));
index 4825fd47520ce2fa968531f5c068c20f02d930d7..8456f7d090c0b0813ef71fefe284e551925a7e42 100644 (file)
@@ -12,18 +12,36 @@ using UnityEngine;
 namespace TheOtherRoles.Patches {
 
        [HarmonyPatch(typeof(MapBehaviour))]
-       class MapBehaviourPatch {
-               public static Dictionary<PlayerControl, SpriteRenderer> herePoints = new Dictionary<PlayerControl, SpriteRenderer>();
+       static class MapBehaviourPatch {
+               public static Dictionary<PlayerControl, SpriteRenderer> herePoints = new();
+
+               public static Sprite Vent = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Vent.png", 150f);
+
+               public static List<List<Vent>> VentNetworks = new();
+
+               public static Dictionary<string, GameObject> mapIcons = new();
+
+               public static void clearAndReload() {
+                       foreach (var mapIcon in mapIcons.Values) {
+                               mapIcon.Destroy();
+                       }
+                       mapIcons = new();
+                       VentNetworks = new();
+                       herePoints = new();
+
+        }
+
 
                [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.FixedUpdate))]
                static void Postfix(MapBehaviour __instance) {
+                       __instance.HerePoint.transform.SetLocalZ(-2.1f);
                        if (Trapper.trapper != null && CachedPlayer.LocalPlayer.PlayerId == Trapper.trapper.PlayerId) {
                                foreach (PlayerControl player in Trapper.playersOnMap) {
                                        if (herePoints.ContainsKey(player)) continue;
                                        Vector3 v = Trap.trapPlayerIdMap[player.PlayerId].trap.transform.position;
                                        v /= MapUtilities.CachedShipStatus.MapScale;
                                        v.x *= Mathf.Sign(MapUtilities.CachedShipStatus.transform.localScale.x);
-                                       v.z = -1f;
+                                       v.z = -2.1f;
                                        var herePoint = UnityEngine.Object.Instantiate(__instance.HerePoint, __instance.HerePoint.transform.parent, true);
                                        herePoint.transform.localPosition = v;
                                        herePoint.enabled = true;
@@ -38,41 +56,141 @@ namespace TheOtherRoles.Patches {
                                        herePoints.Remove(s.Key);
                                }
                        } else if (Snitch.snitch != null && CachedPlayer.LocalPlayer.PlayerId == Snitch.snitch.PlayerId && !Snitch.snitch.Data.IsDead && Snitch.mode != Snitch.Mode.Chat) {
-                var (playerCompleted, playerTotal) = TasksHandler.taskInfo(Snitch.snitch.Data);
-                int numberOfTasks = playerTotal - playerCompleted;
+                               var (playerCompleted, playerTotal) = TasksHandler.taskInfo(Snitch.snitch.Data);
+                               int numberOfTasks = playerTotal - playerCompleted;
 
-                if (numberOfTasks == 0) {
+                               if (numberOfTasks == 0) {
                                        if (MeetingHud.Instance == null) {
-                        foreach (PlayerControl player in CachedPlayer.AllPlayers) {
-                            if (Snitch.targets == Snitch.Targets.EvilPlayers && !Helpers.isEvil(player)) continue;
-                            else if (Snitch.targets == Snitch.Targets.Killers && !Helpers.isKiller(player)) continue;
+                                               foreach (PlayerControl player in CachedPlayer.AllPlayers) {
+                                                       if (Snitch.targets == Snitch.Targets.EvilPlayers && !Helpers.isEvil(player)) continue;
+                                                       else if (Snitch.targets == Snitch.Targets.Killers && !Helpers.isKiller(player)) continue;
                                                        if (player.Data.IsDead) continue;
-                            Vector3 v = player.transform.position;
-                            v /= MapUtilities.CachedShipStatus.MapScale;
-                            v.x *= Mathf.Sign(MapUtilities.CachedShipStatus.transform.localScale.x);
-                            v.z = -1f;
+                                                       Vector3 v = player.transform.position;
+                                                       v /= MapUtilities.CachedShipStatus.MapScale;
+                                                       v.x *= Mathf.Sign(MapUtilities.CachedShipStatus.transform.localScale.x);
+                                                       v.z = -2.1f;
                                                        if (herePoints.ContainsKey(player)) {
                                                                herePoints[player].transform.localPosition = v;
                                                                continue;
                                                        }
-                            var herePoint = UnityEngine.Object.Instantiate(__instance.HerePoint, __instance.HerePoint.transform.parent, true);
-                            herePoint.transform.localPosition = v;
-                            herePoint.enabled = true;
-                            int colorId = player.CurrentOutfit.ColorId;
-                            player.CurrentOutfit.ColorId = 6;
-                            player.SetPlayerMaterialColors(herePoint);
-                            player.CurrentOutfit.ColorId = colorId;
-                            herePoints.Add(player, herePoint);
-                        }
-                    } else {
-                        foreach (var s in herePoints) {
-                            UnityEngine.Object.Destroy(s.Value);
-                            herePoints.Remove(s.Key);
-                        }
-                    }
+                                                       var herePoint = UnityEngine.Object.Instantiate(__instance.HerePoint, __instance.HerePoint.transform.parent, true);
+                                                       herePoint.transform.localPosition = v;
+                                                       herePoint.enabled = true;
+                                                       int colorId = player.CurrentOutfit.ColorId;
+                                                       player.CurrentOutfit.ColorId = 6;
+                                                       player.SetPlayerMaterialColors(herePoint);
+                                                       player.CurrentOutfit.ColorId = colorId;
+                                                       herePoints.Add(player, herePoint);
+                                               }
+                                       } else {
+                                               foreach (var s in herePoints) {
+                                                       UnityEngine.Object.Destroy(s.Value);
+                                                       herePoints.Remove(s.Key);
+                                               }
+                                       }
+                               }
+                       }
+
+                       foreach (var vent in MapUtilities.CachedShipStatus.AllVents) {
+                               if ((vent.name.StartsWith("JackInThe") && !(PlayerControl.LocalPlayer == Trickster.trickster || PlayerControl.LocalPlayer.Data.IsDead))) continue; //for trickster vents
+
+                               if (!TheOtherRolesPlugin.ShowVentsOnMap.Value) {
+                                       if (mapIcons.Count > 0) {
+                                               mapIcons.Values.Do((x) => x.Destroy());
+                                               mapIcons.Clear();
+                                       }
+                                       break;
+                               }
+
+                               var Instance = DestroyableSingleton<MapTaskOverlay>.Instance;
+                               var task = PlayerControl.LocalPlayer.myTasks.ToArray().FirstOrDefault(x => x.TaskType == TaskTypes.VentCleaning);
+
+                               var location = vent.transform.position / MapUtilities.CachedShipStatus.MapScale;
+                               location.z = -2f; //show above sabotage buttons
+                               
+                               GameObject MapIcon;
+                               if (!mapIcons.ContainsKey($"vent {vent.Id} icon")) {
+                                       MapIcon = GameObject.Instantiate(__instance.HerePoint.gameObject, __instance.HerePoint.transform.parent);
+                                       mapIcons.Add($"vent {vent.Id} icon", MapIcon);
+                               }
+                               else {
+                                       MapIcon = mapIcons[$"vent {vent.Id} icon"];
+                               }
+                               
+                               MapIcon.GetComponent<SpriteRenderer>().sprite = Vent;
+                               
+                               MapIcon.name = $"vent {vent.Id} icon";
+                               MapIcon.transform.localPosition = location;
+
+                               if (task?.IsComplete == false && task.FindConsoles()[0].ConsoleId == vent.Id) {
+                                       MapIcon.transform.localScale *= 0.6f;
+                               }
+                if (vent.name.StartsWith("JackInThe")) {
+                    MapIcon.GetComponent<SpriteRenderer>().sprite = JackInTheBox.getBoxAnimationSprite(0);
+                                       MapIcon.transform.localScale = new Vector3(0.5f, 0.5f, 1);
+                                       MapIcon.GetComponent<SpriteRenderer>().color = vent.isActiveAndEnabled ? Color.yellow : Color.yellow.SetAlpha(0.5f);
                 }
+
+                if (AllVentsRegistered(Instance)) {
+                                       var array = VentNetworks.ToArray();
+                                       foreach (var connectedgroup in VentNetworks) {
+                                               var index = Array.IndexOf(array, connectedgroup);
+                                               if (connectedgroup[0].name.StartsWith("JackInThe"))
+                                                       continue;
+                                               connectedgroup.Do(x => GetIcon(x).GetComponent<SpriteRenderer>().color = Palette.PlayerColors[index]);
+                                       }
+                                       continue;
+                               }
+
+                               HandleMiraOrSub();
+
+                               var network = GetNetworkFor(vent);
+                               if (network == null) {
+                                       VentNetworks.Add(new(vent.NearbyVents.Where(x => x != null)) { vent });
+                               }
+                               else {
+                                       if (!network.Any(x => x == vent)) network.Add(vent);
+                               }
                        }
             HudManagerUpdate.CloseSettings();
         }
+
+               public static List<Vent> GetNetworkFor(Vent vent)
+               {
+                       return VentNetworks.FirstOrDefault(x => x.Any(y => y == vent || y == vent.Left || y == vent.Center || y == vent.Right));
+               }
+
+               public static bool AllVentsRegistered(MapTaskOverlay __instance)
+               {
+                       foreach (var vent in MapUtilities.CachedShipStatus.AllVents)
+                       {
+                               if (!vent.isActiveAndEnabled) continue;
+                               var network = GetNetworkFor(vent);
+                               if (network == null || !network.Any(x => x == vent)) return false;
+                               if (!mapIcons.ContainsKey($"vent {vent.Id} icon")) return false;
+                       }
+                       return true;
+               }
+
+               public static GameObject GetIcon(Vent vent)
+               {
+                       var icon = mapIcons[$"vent {vent.Id} icon"];
+                       return icon;
+               }
+
+               public static void HandleMiraOrSub()
+               {
+                       if (VentNetworks.Count != 0) return;
+                       
+                       if (Helpers.isMira()) {
+                               var vents = MapUtilities.CachedShipStatus.AllVents.Where(x => !x.name.Contains("JackInTheBoxVent_"));
+                               VentNetworks.Add(vents.ToList());
+                               return;
+                       }
+                       if (MapUtilities.CachedShipStatus.Type == SubmergedCompatibility.SUBMERGED_MAP_TYPE) {
+                               var vents = MapUtilities.CachedShipStatus.AllVents.Where(x => x.Id is 12 or 13 or 15 or 16);
+                               VentNetworks.Add(vents.ToList());
+                       }
+               }
        }
 }
index 717a08c843b6724bb594e9ee501af7086090dbde..30cc11da82d171baeecdb24274fcdc2ab00769d8 100644 (file)
@@ -137,25 +137,39 @@ namespace TheOtherRoles.Patches {
 
         [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.BloopAVoteIcon))]
         class MeetingHudBloopAVoteIconPatch {
-            public static bool Prefix(MeetingHud __instance, [HarmonyArgument(0)]GameData.PlayerInfo voterPlayer, [HarmonyArgument(1)]int index, [HarmonyArgument(2)]Transform parent) {
-                SpriteRenderer spriteRenderer = UnityEngine.Object.Instantiate<SpriteRenderer>(__instance.PlayerVotePrefab);
-                int cId = voterPlayer.DefaultOutfit.ColorId;
-                if (!(!GameOptionsManager.Instance.currentNormalGameOptions.AnonymousVotes || (CachedPlayer.LocalPlayer.Data.IsDead && TORMapOptions.ghostsSeeVotes) || Mayor.mayor != null && CachedPlayer.LocalPlayer.PlayerControl == Mayor.mayor && Mayor.canSeeVoteColors && TasksHandler.taskInfo(CachedPlayer.LocalPlayer.Data).Item1 >= Mayor.tasksNeededToSeeVoteColors))
-                    voterPlayer.Object.SetColor(6);                    
-                voterPlayer.Object.SetPlayerMaterialColors(spriteRenderer);
-                spriteRenderer.transform.SetParent(parent);
-                spriteRenderer.transform.localScale = Vector3.zero;
-                __instance.StartCoroutine(Effects.Bloop((float)index * 0.3f, spriteRenderer.transform, 1f, 0.5f));
+            public static bool Prefix(MeetingHud __instance, GameData.PlayerInfo voterPlayer, int index, Transform parent) {
+                var spriteRenderer = UnityEngine.Object.Instantiate<SpriteRenderer>(__instance.PlayerVotePrefab);
+                var showVoteColors = !GameManager.Instance.LogicOptions.GetAnonymousVotes() ||
+                                      (CachedPlayer.LocalPlayer.Data.IsDead && TORMapOptions.ghostsSeeVotes) || 
+                                      (Mayor.mayor != null && Mayor.mayor == CachedPlayer.LocalPlayer.PlayerControl && Mayor.canSeeVoteColors && TasksHandler.taskInfo(CachedPlayer.LocalPlayer.Data).Item1 >= Mayor.tasksNeededToSeeVoteColors);
+                if (showVoteColors)
+                {
+                    PlayerMaterial.SetColors(voterPlayer.DefaultOutfit.ColorId, spriteRenderer);
+                }
+                else
+                {
+                    PlayerMaterial.SetColors(Palette.DisabledGrey, spriteRenderer);
+                }
+
+                var transform = spriteRenderer.transform;
+                transform.SetParent(parent);
+                transform.localScale = Vector3.zero;
+                var component = parent.GetComponent<PlayerVoteArea>();
+                if (component != null)
+                {
+                    spriteRenderer.material.SetInt(PlayerMaterial.MaskLayer, component.MaskLayer);
+                }
+
+                __instance.StartCoroutine(Effects.Bloop(index * 0.3f, transform));
                 parent.GetComponent<VoteSpreader>().AddVote(spriteRenderer);
-                voterPlayer.Object.SetColor(cId);
                 return false;
             }
-        } 
+        }
 
         [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.PopulateResults))]
         class MeetingHudPopulateVotesPatch {
             
-            static bool Prefix(MeetingHud __instance, Il2CppStructArray<MeetingHud.VoterState> states) {
+            private static bool Prefix(MeetingHud __instance, Il2CppStructArray<MeetingHud.VoterState> states) {
                 // Swapper swap
 
                 PlayerVoteArea swapped1 = null;
@@ -662,10 +676,11 @@ namespace TheOtherRoles.Patches {
                 // Resett Bait list
                 Bait.active = new Dictionary<DeadPlayer, float>();
                 // Save AntiTeleport position, if the player is able to move (i.e. not on a ladder or a gap thingy)
-                if (CachedPlayer.LocalPlayer.PlayerPhysics.enabled && CachedPlayer.LocalPlayer.PlayerControl.moveable || CachedPlayer.LocalPlayer.PlayerControl.inVent
+                if (CachedPlayer.LocalPlayer.PlayerPhysics.enabled && (CachedPlayer.LocalPlayer.PlayerControl.moveable || CachedPlayer.LocalPlayer.PlayerControl.inVent
                     || HudManagerStartPatch.hackerVitalsButton.isEffectActive || HudManagerStartPatch.hackerAdminTableButton.isEffectActive || HudManagerStartPatch.securityGuardCamButton.isEffectActive
-                    || Portal.isTeleporting && Portal.teleportedPlayers.Last().playerId == CachedPlayer.LocalPlayer.PlayerId) {
-                    AntiTeleport.position = CachedPlayer.LocalPlayer.transform.position;
+                    || Portal.isTeleporting && Portal.teleportedPlayers.Last().playerId == CachedPlayer.LocalPlayer.PlayerId)) {
+                    if (!CachedPlayer.LocalPlayer.PlayerControl.inMovingPlat)
+                        AntiTeleport.position = CachedPlayer.LocalPlayer.transform.position;
                 }
 
                 // Medium meeting start time
@@ -726,7 +741,7 @@ namespace TheOtherRoles.Patches {
                             if (x == 1f) {
                                 foreach (PlayerControl p in CachedPlayer.AllPlayers) {
                                     if (Snitch.targets == Snitch.Targets.Killers && !Helpers.isKiller(p)) continue;
-                                    else if (Snitch.targets == Snitch.Targets.EvilPlayers && !Helpers.isEvil(p)) continue;
+                                    if (Snitch.targets == Snitch.Targets.EvilPlayers && !Helpers.isEvil(p)) continue;
                                     if (!Snitch.playerRoomMap.ContainsKey(p.PlayerId)) continue;
                                     if (p.Data.IsDead) continue;
                                     var room = Snitch.playerRoomMap[p.PlayerId];
index b251d7cabd18023f79101ed101341c6639bf10ff..456732f37fd76f15f166f959978e077655e26d13 100644 (file)
@@ -12,6 +12,7 @@ using UnityEngine;
 using TheOtherRoles.CustomGameModes;
 using static UnityEngine.GraphicsBuffer;
 using AmongUs.GameOptions;
+using Assets.CoreScripts;
 using Sentry.Internal.Extensions;
 using Reactor.Utilities.Extensions;
 
@@ -68,7 +69,7 @@ namespace TheOtherRoles.Patches {
                 bool isMorphedMorphling = target == Morphling.morphling && Morphling.morphTarget != null && Morphling.morphTimer > 0f;
                 bool hasVisibleShield = false;
                 Color color = Medic.shieldedColor;
-                if (Camouflager.camouflageTimer <= 0f && Medic.shielded != null && ((target == Medic.shielded && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == Medic.shielded))) {
+                if (Camouflager.camouflageTimer <= 0f && !Helpers.MushroomSabotageActive() && Medic.shielded != null && ((target == Medic.shielded && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == Medic.shielded))) {
                     hasVisibleShield = Medic.showShielded == 0 || Helpers.shouldShowGhostInfo() // Everyone or Ghost info
                         || (Medic.showShielded == 1 && (CachedPlayer.LocalPlayer.PlayerControl == Medic.shielded || CachedPlayer.LocalPlayer.PlayerControl == Medic.medic)) // Shielded + Medic
                         || (Medic.showShielded == 2 && CachedPlayer.LocalPlayer.PlayerControl == Medic.medic); // Medic only
@@ -76,7 +77,7 @@ namespace TheOtherRoles.Patches {
                     hasVisibleShield = hasVisibleShield && (Medic.meetingAfterShielding || !Medic.showShieldAfterMeeting || CachedPlayer.LocalPlayer.PlayerControl == Medic.medic || Helpers.shouldShowGhostInfo());
                 }
 
-                if (Camouflager.camouflageTimer <= 0f && TORMapOptions.firstKillPlayer != null && TORMapOptions.shieldFirstKill && ((target == TORMapOptions.firstKillPlayer && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == TORMapOptions.firstKillPlayer))) {
+                if (Camouflager.camouflageTimer <= 0f && !Helpers.MushroomSabotageActive() && TORMapOptions.firstKillPlayer != null && TORMapOptions.shieldFirstKill && ((target == TORMapOptions.firstKillPlayer && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == TORMapOptions.firstKillPlayer))) {
                     hasVisibleShield = true;
                     color = Color.blue;
                 }
@@ -91,6 +92,15 @@ namespace TheOtherRoles.Patches {
             }
         }
 
+        static void setPetVisibility() {
+            bool localalive = !CachedPlayer.LocalPlayer.Data.IsDead;
+            foreach (var player in CachedPlayer.AllPlayers)
+            {
+                bool playeralive = !player.Data.IsDead;
+                player.PlayerControl.cosmetics.SetPetVisible((localalive && playeralive) || !localalive);
+            }
+        }
+
         public static void bendTimeUpdate() {
             if (TimeMaster.isRewinding) {
                 if (localPlayerPositions.Count > 0) {
@@ -451,7 +461,7 @@ namespace TheOtherRoles.Patches {
             collider.offset = Mini.defaultColliderOffset * Vector2.down;
 
             // Set adapted player size to Mini and Morphling
-            if (Mini.mini == null || Camouflager.camouflageTimer > 0f || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0) return;
+            if (Mini.mini == null || Camouflager.camouflageTimer > 0f || Helpers.MushroomSabotageActive() || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0) return;
 
             float growingProgress = Mini.growingProgress();
             float scale = growingProgress * 0.35f + 0.35f;
@@ -739,12 +749,17 @@ namespace TheOtherRoles.Patches {
             Medium.target = target;
         }
 
+        static bool mushroomSaboWasActive = false;
         static void morphlingAndCamouflagerUpdate() {
+            bool mushRoomSaboIsActive = Helpers.MushroomSabotageActive();
+            if (!mushroomSaboWasActive) mushroomSaboWasActive = mushRoomSaboIsActive;
+            
             float oldCamouflageTimer = Camouflager.camouflageTimer;
             float oldMorphTimer = Morphling.morphTimer;
             Camouflager.camouflageTimer = Mathf.Max(0f, Camouflager.camouflageTimer - Time.fixedDeltaTime);
             Morphling.morphTimer = Mathf.Max(0f, Morphling.morphTimer - Time.fixedDeltaTime);
 
+            if (mushRoomSaboIsActive) return;
 
             // Camouflage reset and set Morphling look if necessary
             if (oldCamouflageTimer > 0f && Camouflager.camouflageTimer <= 0f) {
@@ -755,9 +770,22 @@ namespace TheOtherRoles.Patches {
                 }
             }
 
+            // If the MushRoomSabotage ends while Morph is still active set the Morphlings look to the target's look
+            if (mushroomSaboWasActive) {
+                if (Morphling.morphTimer > 0f && Morphling.morphling != null && Morphling.morphTarget != null) {
+                    PlayerControl target = Morphling.morphTarget;
+                    Morphling.morphling.setLook(target.Data.PlayerName, target.Data.DefaultOutfit.ColorId, target.Data.DefaultOutfit.HatId, target.Data.DefaultOutfit.VisorId, target.Data.DefaultOutfit.SkinId, target.Data.DefaultOutfit.PetId);
+                }
+                if (Camouflager.camouflageTimer > 0) {
+                    foreach (PlayerControl player in CachedPlayer.AllPlayers)
+                        player.setLook("", 6, "", "", "", "");
+                }
+            }
+
             // Morphling reset (only if camouflage is inactive)
             if (Camouflager.camouflageTimer <= 0f && oldMorphTimer > 0f && Morphling.morphTimer <= 0f && Morphling.morphling != null)
                 Morphling.resetMorph();
+            mushroomSaboWasActive = false;
         }
 
         public static void lawyerUpdate() {
@@ -980,6 +1008,9 @@ namespace TheOtherRoles.Patches {
                 // Update Player Info
                 updatePlayerInfo();
 
+                //Update pet visibility
+                setPetVisibility();
+
                 // Time Master
                 bendTimeUpdate();
                 // Morphling
@@ -1076,7 +1107,7 @@ namespace TheOtherRoles.Patches {
     class PlayerPhysicsWalkPlayerToPatch {
         private static Vector2 offset = Vector2.zero;
         public static void Prefix(PlayerPhysics __instance) {
-            bool correctOffset = Camouflager.camouflageTimer <= 0f && (__instance.myPlayer == Mini.mini ||  (Morphling.morphling != null && __instance.myPlayer == Morphling.morphling && Morphling.morphTarget == Mini.mini && Morphling.morphTimer > 0f));
+            bool correctOffset = Camouflager.camouflageTimer <= 0f && !Helpers.MushroomSabotageActive() && (__instance.myPlayer == Mini.mini ||  (Morphling.morphling != null && __instance.myPlayer == Morphling.morphling && Morphling.morphTarget == Mini.mini && Morphling.morphTimer > 0f));
             correctOffset = correctOffset && !(Mini.mini == Morphling.morphling && Morphling.morphTimer > 0f);
             if (correctOffset) {
                 float currentScaling = (Mini.growingProgress() + 1) * 0.5f;
@@ -1138,7 +1169,7 @@ namespace TheOtherRoles.Patches {
                         }
                         if (msg.IndexOf("who", StringComparison.OrdinalIgnoreCase) >= 0)
                         {
-                            FastDestroyableSingleton<Assets.CoreScripts.Telemetry>.Instance.SendWho();
+                            FastDestroyableSingleton<UnityTelemetry>.Instance.SendWho();
                         }
                     }
                 }
index 9aada93857d653c89e2a81fff643991a024c8c61..18d48a222093d109e62c3ae13add6dc5f4519784 100644 (file)
@@ -15,7 +15,7 @@ namespace TheOtherRoles.Patches {
         [HarmonyPrefix]
         [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.CalculateLightRadius))]
         public static bool Prefix(ref float __result, ShipStatus __instance, [HarmonyArgument(0)] GameData.PlayerInfo player) {
-            if (!__instance.Systems.ContainsKey(SystemTypes.Electrical) || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return true;
+            if ((!__instance.Systems.ContainsKey(SystemTypes.Electrical) && !Helpers.isFungle()) || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return true;
 
             // If Game Mode is PropHunt:
             if (PropHunt.isPropHuntGM) {
@@ -86,9 +86,11 @@ namespace TheOtherRoles.Patches {
             }
 
             if (isImpostor) return shipStatus.MaxLightRadius * GameOptionsManager.Instance.currentNormalGameOptions.ImpostorLightMod;
-
-            SwitchSystem switchSystem = MapUtilities.Systems[SystemTypes.Electrical].CastFast<SwitchSystem>();
-            float lerpValue = switchSystem.Value / 255f;
+            float lerpValue = 1.0f;
+            try {
+                SwitchSystem switchSystem = MapUtilities.Systems[SystemTypes.Electrical].CastFast<SwitchSystem>();
+                lerpValue = switchSystem.Value / 255f;
+            } catch { }
 
             return Mathf.Lerp(shipStatus.MinLightRadius, shipStatus.MaxLightRadius, lerpValue) * GameOptionsManager.Instance.currentNormalGameOptions.CrewLightMod;
         }
@@ -117,7 +119,7 @@ namespace TheOtherRoles.Patches {
 
             if (TORMapOptions.gameMode != CustomGamemodes.HideNSeek) {
                 var commonTaskCount = __instance.CommonTasks.Count;
-                var normalTaskCount = __instance.NormalTasks.Count;
+                var normalTaskCount = __instance.ShortTasks.Count;
                 var longTaskCount = __instance.LongTasks.Count;
 
                 if (TORMapOptions.gameMode == CustomGamemodes.PropHunt) {
@@ -134,6 +136,7 @@ namespace TheOtherRoles.Patches {
                 GameOptionsManager.Instance.currentNormalGameOptions.NumLongTasks = Mathf.RoundToInt(CustomOptionHolder.hideNSeekLongTasks.getFloat());
             }
 
+            MapBehaviourPatch.VentNetworks.Clear();
             return true;
         }
 
diff --git a/TheOtherRoles/Patches/TransportationToolPatches.cs b/TheOtherRoles/Patches/TransportationToolPatches.cs
new file mode 100644 (file)
index 0000000..1924aee
--- /dev/null
@@ -0,0 +1,77 @@
+using HarmonyLib;
+using Il2CppSystem.Collections.Generic;
+using System;
+using UnityEngine.Windows.Speech;
+using TheOtherRoles;
+using static UnityEngine.GraphicsBuffer;
+
+namespace TheOtherRoles.Patches {
+    [HarmonyPatch]
+    public static class TransportationToolPatches {
+        /* 
+         * Moving Plattform / Zipline / Ladders move the player out of bounds, thus we want to disable functions of the mod if the player is currently using one of these.
+         * Save the players anti tp position before using it.
+         * 
+         * Zipline can also break camo, fix that one too.
+         */
+       
+        public static bool isUsingTransportation(PlayerControl pc) {
+            return pc.inMovingPlat || pc.onLadder;
+        }
+
+        // Zipline:
+        [HarmonyPrefix]
+        [HarmonyPatch(typeof(ZiplineBehaviour), nameof(ZiplineBehaviour.Use), new Type[] {typeof(PlayerControl), typeof(bool)})]
+        public static void prefix3(ZiplineBehaviour __instance, PlayerControl player, bool fromTop) {
+            AntiTeleport.position = Players.CachedPlayer.LocalPlayer.transform.position;
+        }
+
+        [HarmonyPostfix]
+        [HarmonyPatch(typeof(ZiplineBehaviour), nameof(ZiplineBehaviour.Use), new Type[] { typeof(PlayerControl), typeof(bool) })]
+        public static void postfix(ZiplineBehaviour __instance, PlayerControl player, bool fromTop) {
+            // Fix camo:
+            __instance.StartCoroutine(Effects.Lerp(fromTop ? __instance.downTravelTime : __instance.upTravelTime, new System.Action<float>((p) => {
+                HandZiplinePoolable hand;
+                __instance.playerIdHands.TryGetValue(player.PlayerId, out hand);
+                if (hand != null) {
+                    if (Camouflager.camouflageTimer <= 0 && !Helpers.MushroomSabotageActive()) {
+                        if (player == Morphling.morphling && Morphling.morphTimer > 0) {
+                            hand.SetPlayerColor(Morphling.morphTarget.CurrentOutfit, PlayerMaterial.MaskType.None);
+                            // Also set hat color, cause the line destroys it...
+                            player.RawSetHat(Morphling.morphTarget.Data.DefaultOutfit.HatId, Morphling.morphTarget.Data.DefaultOutfit.ColorId);
+                        } else {
+                            hand.SetPlayerColor(player.CurrentOutfit, PlayerMaterial.MaskType.None);
+                        }
+                    } else {
+                        PlayerMaterial.SetColors(6, hand.handRenderer);
+                    }
+                }
+            })));
+        }
+
+        // Save the position of the player prior to starting the climb / gap platform
+        [HarmonyPrefix]
+        [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.ClimbLadder))]
+        public static void prefix() {
+            AntiTeleport.position = Players.CachedPlayer.LocalPlayer.transform.position;
+        }
+
+        [HarmonyPostfix]
+        [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.ClimbLadder))]
+        public static void postfix2(PlayerPhysics __instance, Ladder source, byte climbLadderSid) {
+            // Fix camo:
+            var player = __instance.myPlayer;
+            __instance.StartCoroutine(Effects.Lerp(5.0f, new System.Action<float>((p) => {
+                if (Camouflager.camouflageTimer <= 0 && !Helpers.MushroomSabotageActive() && player == Morphling.morphling && Morphling.morphTimer > 0.1f) {
+                    player.RawSetHat(Morphling.morphTarget.Data.DefaultOutfit.HatId, Morphling.morphTarget.Data.DefaultOutfit.ColorId);
+                }
+            })));
+        }
+
+        [HarmonyPrefix]
+        [HarmonyPatch(typeof(MovingPlatformBehaviour), nameof(MovingPlatformBehaviour.UsePlatform))]
+        public static void prefix2() {
+            AntiTeleport.position = Players.CachedPlayer.LocalPlayer.transform.position;
+        }
+    }
+}
index 8ef4a720d49b2deb0c062f58f97bda8300e6cda0..1b2bfa1dd4c2ea9ec925a8cf25ad5e9ba613cf1c 100644 (file)
@@ -253,7 +253,7 @@ namespace TheOtherRoles.Patches {
         }
 
         public static void miniUpdate() {
-            if (Mini.mini == null || Camouflager.camouflageTimer > 0f || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0f || Mini.mini == Ninja.ninja && Ninja.isInvisble || SurveillanceMinigamePatch.nightVisionIsActive) return;
+            if (Mini.mini == null || Camouflager.camouflageTimer > 0f || Helpers.MushroomSabotageActive() || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0f || Mini.mini == Ninja.ninja && Ninja.isInvisble || SurveillanceMinigamePatch.nightVisionIsActive) return;
                 
             float growingProgress = Mini.growingProgress();
             float scale = growingProgress * 0.35f + 0.35f;
index 2835ec9a2aeb9a180604e4ac084be3c0d6e7f86a..24b09e80ae7b9280c7ac7ad5f9f56e98c71b1990 100644 (file)
@@ -580,6 +580,14 @@ namespace TheOtherRoles.Patches {
             }
         }
 
+        [HarmonyPatch(typeof(FungleSurveillanceMinigame), nameof(FungleSurveillanceMinigame.Update))]
+        class FungleSurveillanceMinigameUpdatePatch {
+            public static void Postfix(FungleSurveillanceMinigame __instance) {
+                nightVisionUpdate(FungleCamMinigame: __instance);
+            }
+        }
+
+
         [HarmonyPatch(typeof(SurveillanceMinigame), nameof(SurveillanceMinigame.OnDestroy))]
         class SurveillanceMinigameDestroyPatch {
             public static void Prefix() {
@@ -594,10 +602,9 @@ namespace TheOtherRoles.Patches {
             }
         }
 
-
-        private static void nightVisionUpdate(SurveillanceMinigame SkeldCamsMinigame = null, PlanetSurveillanceMinigame SwitchCamsMinigame = null) {
+        private static void nightVisionUpdate(SurveillanceMinigame SkeldCamsMinigame = null, PlanetSurveillanceMinigame SwitchCamsMinigame = null, FungleSurveillanceMinigame FungleCamMinigame = null) {
+            GameObject closeButton = null;
             if (nightVisionOverlays == null) {
-                GameObject closeButton = null;
                 List<MeshRenderer> viewPorts = new();
                 Transform viewablesTransform = null;
                 if (SkeldCamsMinigame != null) {
@@ -608,15 +615,31 @@ namespace TheOtherRoles.Patches {
                     closeButton = SwitchCamsMinigame.Viewables.transform.Find("CloseButton").gameObject;
                     viewPorts.Add(SwitchCamsMinigame.ViewPort);
                     viewablesTransform = SwitchCamsMinigame.Viewables.transform;
+                } else if (FungleCamMinigame != null) {
+                    closeButton = FungleCamMinigame.transform.Find("CloseButton").gameObject;
+                    viewPorts.Add(FungleCamMinigame.viewport);
+                    viewablesTransform = FungleCamMinigame.viewport.transform;
                 } else return;
 
                 nightVisionOverlays = new List<GameObject>();
 
                 foreach (var renderer in viewPorts) {
-                    GameObject overlayObject = GameObject.Instantiate(closeButton, viewablesTransform);
-                    overlayObject.transform.position = new Vector3(renderer.transform.position.x, renderer.transform.position.y, overlayObject.transform.position.z);
-                    overlayObject.transform.localScale = (SkeldCamsMinigame != null) ? new Vector3(0.91f, 0.612f, 1f) : new Vector3(2.124f, 1.356f, 1f);
-                    overlayObject.layer = closeButton.layer;
+                    GameObject overlayObject;
+                    float zPosition;
+                    if (FungleCamMinigame != null) {
+                        overlayObject = GameObject.Instantiate(closeButton, renderer.transform);
+                        overlayObject.layer = renderer.gameObject.layer;
+                        zPosition = - 0.5f;
+                        overlayObject.transform.localPosition = new Vector3(0, 0, zPosition);
+                    } else {
+                        overlayObject = GameObject.Instantiate(closeButton, viewablesTransform);
+                        zPosition = overlayObject.transform.position.z;
+                        overlayObject.layer = closeButton.layer;
+                        overlayObject.transform.position = new Vector3(renderer.transform.position.x, renderer.transform.position.y, zPosition);
+                    }
+                    Vector3 localScale = (SkeldCamsMinigame != null) ? new Vector3(0.91f, 0.612f, 1f) : new Vector3(2.124f, 1.356f, 1f);
+                    localScale = (FungleCamMinigame != null) ? new Vector3(10f, 10f, 1f) : localScale;
+                    overlayObject.transform.localScale = localScale;
                     var overlayRenderer = overlayObject.GetComponent<SpriteRenderer>();
                     overlayRenderer.sprite = overlaySprite;
                     overlayObject.SetActive(false);
@@ -625,8 +648,7 @@ namespace TheOtherRoles.Patches {
                 }
             }
 
-
-            isLightsOut = CachedPlayer.LocalPlayer.PlayerControl.myTasks.ToArray().Any(x => x.name.Contains("FixLightsTask"));
+            isLightsOut = CachedPlayer.LocalPlayer.PlayerControl.myTasks.ToArray().Any(x => x.name.Contains("FixLightsTask")) || Trickster.lightsOutTimer > 0;
             bool ignoreNightVision = CustomOptionHolder.camsNoNightVisionIfImpVision.getBool() && Helpers.hasImpVision(GameData.Instance.GetPlayerById(CachedPlayer.LocalPlayer.PlayerId)) || CachedPlayer.LocalPlayer.Data.IsDead;
             bool nightVisionEnabled = CustomOptionHolder.camsNightVision.getBool();
 
@@ -652,7 +674,7 @@ namespace TheOtherRoles.Patches {
             }
         }
 
-        private static void resetNightVision() {
+        public static void resetNightVision() {
             foreach (var go in nightVisionOverlays) {
                 go.Destroy();
             }
index 0e64211fbb4464e5114b95c25a84d6bb90306717..4edcbdf1a58ccf1c6d57b86f2a26a46fcb8ffb3d 100644 (file)
@@ -15,7 +15,7 @@ using TheOtherRoles.Utilities;
 using TheOtherRoles.CustomGameModes;
 using AmongUs.Data;
 using AmongUs.GameOptions;
-using UnityEngine.UIElements;
+using Assets.CoreScripts;
 
 namespace TheOtherRoles
 {
@@ -184,6 +184,7 @@ namespace TheOtherRoles
             GameStartManagerPatch.GameStartManagerUpdatePatch.startingTimer = 0;
             SurveillanceMinigamePatch.nightVisionOverlays = null;
             EventUtility.clearAndReload();
+            MapBehaviourPatch.clearAndReload();
         }
 
     public static void HandleShareOptions(byte numberOfOptions, MessageReader reader) {            
@@ -601,8 +602,10 @@ namespace TheOtherRoles
             if (Camouflager.camouflager == null) return;
 
             Camouflager.camouflageTimer = Camouflager.duration;
+            if (Helpers.MushroomSabotageActive()) return; // Dont overwrite the fungle "camo"
             foreach (PlayerControl player in CachedPlayer.AllPlayers)
                 player.setLook("", 6, "", "", "", "");
+
         }
 
         public static void vampireSetBitten(byte targetId, byte performReset) {
@@ -808,7 +811,7 @@ namespace TheOtherRoles
                 target.cosmetics.colorBlindText.gameObject.SetActive(DataManager.Settings.Accessibility.ColorBlindMode);
                 target.cosmetics.colorBlindText.color = target.cosmetics.colorBlindText.color.SetAlpha(1f);
 
-                if (Camouflager.camouflageTimer <= 0) target.setDefaultLook();
+                if (Camouflager.camouflageTimer <= 0 && !Helpers.MushroomSabotageActive()) target.setDefaultLook();
                 Ninja.isInvisble = false;
                 return;
             }
@@ -893,12 +896,20 @@ namespace TheOtherRoles
             SecurityGuard.remainingScrews -= SecurityGuard.ventPrice;
             if (CachedPlayer.LocalPlayer.PlayerControl == SecurityGuard.securityGuard) {
                 PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>(); 
-                animator?.Stop();
+                
                 vent.EnterVentAnim = vent.ExitVentAnim = null;
-                vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                Sprite newSprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                SpriteRenderer rend = vent.myRend;
+                if (Helpers.isFungle()) {
+                    newSprite = SecurityGuard.getFungleVentSealedSprite();
+                    rend = vent.transform.GetChild(3).GetComponent<SpriteRenderer>();
+                    animator = vent.transform.GetChild(3).GetComponent<PowerTools.SpriteAnim>();
+                }
+                animator?.Stop();
+                rend.sprite = newSprite;
                 if (SubmergedCompatibility.IsSubmerged && vent.Id == 0) vent.myRend.sprite = SecurityGuard.getSubmergedCentralUpperSealedSprite();
                 if (SubmergedCompatibility.IsSubmerged && vent.Id == 14) vent.myRend.sprite = SecurityGuard.getSubmergedCentralLowerSealedSprite();
-                vent.myRend.color = new Color(1f, 1f, 1f, 0.5f);
+                rend.color = new Color(1f, 1f, 1f, 0.5f);
                 vent.name = "FutureSealedVent_" + vent.name;
             }
 
@@ -948,6 +959,15 @@ namespace TheOtherRoles
                 }
             }
 
+            if (Lawyer.lawyer != null && !Lawyer.isProsecutor && Lawyer.lawyer.PlayerId == killerId && Lawyer.target != null && Lawyer.target.PlayerId == dyingTargetId) {
+                // Lawyer guessed client.
+                if (CachedPlayer.LocalPlayer.PlayerControl == Lawyer.lawyer) {
+                    FastDestroyableSingleton<HudManager>.Instance.KillOverlay.ShowKillAnimation(Lawyer.lawyer.Data, Lawyer.lawyer.Data);
+                    if (MeetingHudPatch.guesserUI != null) MeetingHudPatch.guesserUIExitButton.OnClick.Invoke();
+                }
+                Lawyer.lawyer.Exiled();
+            }
+
             dyingTarget.Exiled();
             GameHistory.overrideDeathReasonAndKiller(dyingTarget, DeadPlayer.CustomDeathReason.Guess, guesser);
             byte partnerId = dyingLoverPartner != null ? dyingLoverPartner.PlayerId : dyingTargetId;
@@ -1004,7 +1024,7 @@ namespace TheOtherRoles
                 if (AmongUsClient.Instance.AmClient && FastDestroyableSingleton<HudManager>.Instance)
                     FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(guesser, msg);
                 if (msg.IndexOf("who", StringComparison.OrdinalIgnoreCase) >= 0)
-                    FastDestroyableSingleton<Assets.CoreScripts.Telemetry>.Instance.SendWho();
+                    FastDestroyableSingleton<UnityTelemetry>.Instance.SendWho();
             }
         }
 
@@ -1149,7 +1169,11 @@ namespace TheOtherRoles
             PlayerControl player = Helpers.playerById(playerId);
             var prop = PropHunt.FindPropByNameAndPos(propName, posX);
             if (prop == null) return;
-            player.GetComponent<SpriteRenderer>().sprite = prop.GetComponent<SpriteRenderer>().sprite;
+            try {
+                player.GetComponent<SpriteRenderer>().sprite = prop.GetComponent<SpriteRenderer>().sprite;
+            } catch {
+                player.GetComponent<SpriteRenderer>().sprite = prop.transform.GetComponentInChildren<SpriteRenderer>().sprite;
+            }
             player.transform.localScale = prop.transform.lossyScale;
             player.Visible = false;
             PropHunt.currentObject[player.PlayerId] = new Tuple<string, float>(propName, posX);
diff --git a/TheOtherRoles/Resources/FungleVentSealed.png b/TheOtherRoles/Resources/FungleVentSealed.png
new file mode 100644 (file)
index 0000000..16b157c
Binary files /dev/null and b/TheOtherRoles/Resources/FungleVentSealed.png differ
index 906cae8c1172e7442b67c6dfb157fcd24cc608b2..9234681069d9700d15627417f44e5758375575e2 100644 (file)
@@ -89,4 +89,8 @@ NormalMeetingVent
 console-mr-callmeeting
 deco-mr-table
 console-mr-fishfeed
-NormalAdminVent
\ No newline at end of file
+NormalAdminVent
+
+Debris
+MetalPlate_
+Mushroom (
\ No newline at end of file
diff --git a/TheOtherRoles/Resources/Vent.png b/TheOtherRoles/Resources/Vent.png
new file mode 100644 (file)
index 0000000..29fa8f4
Binary files /dev/null and b/TheOtherRoles/Resources/Vent.png differ
index 21f4a3000f6014ceaae2e02d5e90a751ec9ecfa3..f3311b66fada55cc43ca8ae96946582ab5b10beb 100644 (file)
@@ -20,7 +20,7 @@ namespace TheOtherRoles
         }
         
         public const string SUBMERGED_GUID = "Submerged";
-        public const ShipStatus.MapType SUBMERGED_MAP_TYPE = (ShipStatus.MapType) 5;
+        public const ShipStatus.MapType SUBMERGED_MAP_TYPE = (ShipStatus.MapType) 6;
         
         public static SemanticVersioning.Version Version { get; private set; }
         public static bool Loaded { get; private set; }
index 053206af66aaf2d695d705772284b1c6dba3a13b..6143779be7b0bc28ce35d8507595ddec8cc29b5f 100644 (file)
@@ -695,9 +695,10 @@ namespace TheOtherRoles
         public static Sprite getAdminSprite() {
             byte mapId = GameOptionsManager.Instance.currentNormalGameOptions.MapId;
             UseButtonSettings button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.PolusAdminButton]; // Polus
-            if (mapId == 0 || mapId == 3) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.AdminMapButton]; // Skeld || Dleks
-            else if (mapId == 1) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.MIRAAdminButton]; // Mira HQ
-            else if (mapId == 4) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.AirshipAdminButton]; // Airship
+            if (Helpers.isSkeld() || mapId == 3) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.AdminMapButton]; // Skeld || Dleks
+            else if (Helpers.isMira()) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.MIRAAdminButton]; // Mira HQ
+            else if (Helpers.isAirship()) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.AirshipAdminButton]; // Airship
+            else if (Helpers.isFungle()) button = FastDestroyableSingleton<HudManager>.Instance.UseButton.fastUseSettings[ImageNames.AdminMapButton];  // Hacker can Access the Admin panel on Fungle
             adminSprite = button.Image;
             return adminSprite;
         }
@@ -1140,6 +1141,14 @@ namespace TheOtherRoles
             return staticVentSealedSprite;
         }
 
+        private static Sprite fungleVentSealedSprite;
+        public static Sprite getFungleVentSealedSprite() {
+            if (fungleVentSealedSprite) return fungleVentSealedSprite;
+            fungleVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.FungleVentSealed.png", 160f);
+            return fungleVentSealedSprite;
+        }
+
+
         private static Sprite submergedCentralUpperVentSealedSprite;
         public static Sprite getSubmergedCentralUpperSealedSprite() {
             if (submergedCentralUpperVentSealedSprite) return submergedCentralUpperVentSealedSprite;
@@ -1941,8 +1950,10 @@ namespace TheOtherRoles
                     chameleonPlayer.SetHatAndVisorAlpha(visibility);
                     chameleonPlayer.cosmetics.skin.layer.color = chameleonPlayer.cosmetics.skin.layer.color.SetAlpha(visibility);
                     chameleonPlayer.cosmetics.nameText.color = chameleonPlayer.cosmetics.nameText.color.SetAlpha(visibility);
-                    chameleonPlayer.cosmetics.currentPet.rend.color = chameleonPlayer.cosmetics.currentPet.rend.color.SetAlpha(petVisibility);
-                    chameleonPlayer.cosmetics.currentPet.shadowRend.color = chameleonPlayer.cosmetics.currentPet.shadowRend.color.SetAlpha(petVisibility);
+                    foreach (var rend in chameleonPlayer.cosmetics.currentPet.renderers)
+                        rend.color = rend.color.SetAlpha(petVisibility);
+                    foreach (var shadowRend in chameleonPlayer.cosmetics.currentPet.shadows)
+                        shadowRend.color = shadowRend.color.SetAlpha(petVisibility);
                 } catch { }
             }
                 
index 5faf18aa3b0dcf662de1d01f6da108ee0293939a..c2ced5320f32940b1fa658f1fac0f210ab38f215 100644 (file)
@@ -1,13 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>net6.0</TargetFramework>
-        <Version>4.4.2</Version>
+        <Version>4.5.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
                <LangVersion>latest</LangVersion>
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
         <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
         <PlatformTarget>x86</PlatformTarget>
+        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
     </PropertyGroup>
 
     <ItemGroup>
@@ -15,8 +16,8 @@
     </ItemGroup>
    
        <ItemGroup>
-               <PackageReference Include="AmongUs.GameLibs.Steam" Version="2023.7.11" />
-        <PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.671" />
+               <PackageReference Include="AmongUs.GameLibs.Steam" Version="2023.11.28" />
+               <PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.671" />
                <PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" />
         <PackageReference Include="Reactor" Version="2.2.0" />
     </ItemGroup>
index b04f2cb35f2a45ee8a21a009a7304ed840c73f7a..caacedcd6d1d9cd141323c82e5808f7f29d571e4 100644 (file)
@@ -10,149 +10,150 @@ using System.Linq;
 using InnerNet;
 using TheOtherRoles.Modules;
 
-namespace TheOtherRoles.Utilities {
-    public static class EventUtility {
-
-        public enum EventTypes {
-            Communication,
-            Animation,
-            Invert,
-            KnockKnock
-        }
+namespace TheOtherRoles.Utilities;
 
-        public static readonly float[] eventFrequencies = {15f, 60f, 60f, 300f};
-        public static readonly float[] eventDurations = {0f, 1f, 5f, 0f};
-        public static double[] eventProbabilities;
-        private static bool knocked = false;
-        public static bool disableHorses = false;
-
-        public static void Load() {
-            if (!isEnabled) return;
-            eventProbabilities = new double[6];
-            foreach (EventTypes curEvent in Enum.GetValues(typeof(EventTypes))) {
-                float desired_trials = 60 * eventFrequencies[(int)curEvent];
-                eventProbabilities[(int)curEvent] = desired_trials != 0 ? 1f / desired_trials : 1f;
-            }
-                
-        } 
+public static class EventUtility {
 
-        private static List<EventTypes> eventQueue = null;
-        public static bool eventInvert = false;
+    public enum EventTypes {
+        Communication,
+        Animation,
+        Invert,
+        KnockKnock
+    }
 
-        public static void clearAndReload() {
-            eventQueue = new List<EventTypes>();
-            eventInvert = false;
-            if (canBeEnabled && CustomOptionHolder.enableCodenameDisableHorses != null)
-                disableHorses = CustomOptionHolder.enableCodenameDisableHorses.getBool();
+    public static readonly float[] eventFrequencies = {15f, 60f, 60f, 300f};
+    public static readonly float[] eventDurations = {0f, 1f, 5f, 0f};
+    public static double[] eventProbabilities;
+    private static bool knocked = false;
+    public static bool disableHorses = false;
+
+    public static void Load() {
+        if (!isEnabled) return;
+        eventProbabilities = new double[6];
+        foreach (EventTypes curEvent in Enum.GetValues(typeof(EventTypes))) {
+            float desired_trials = 60 * eventFrequencies[(int)curEvent];
+            eventProbabilities[(int)curEvent] = desired_trials != 0 ? 1f / desired_trials : 1f;
         }
+                
+    } 
 
-        public static void Update() {
-            if (!isEnabled || eventQueue == null || AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started || TheOtherRoles.rnd == null || IntroCutscene.Instance) return;
-            foreach (EventTypes curEvent in eventQueue.ToArray()) {
-                if (TheOtherRoles.rnd.NextSingle() < eventProbabilities[(int)curEvent]) {
-                    eventQueue.Remove(curEvent);
-                    StartEvent(curEvent);
-                }
-            }
+    private static List<EventTypes> eventQueue = null;
+    public static bool eventInvert = false;
 
-            AddToQueue(EventTypes.Animation);
-            AddToQueue(EventTypes.Invert);
-            if (!knocked) {
-                AddToQueue(EventTypes.KnockKnock);
+    public static void clearAndReload() {
+        eventQueue = new List<EventTypes>();
+        eventInvert = false;
+        if (canBeEnabled && CustomOptionHolder.enableCodenameDisableHorses != null)
+            disableHorses = CustomOptionHolder.enableCodenameDisableHorses.getBool();
+    }
+
+    public static void Update() {
+        if (!isEnabled || eventQueue == null || AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started || TheOtherRoles.rnd == null || IntroCutscene.Instance) return;
+        foreach (EventTypes curEvent in eventQueue.ToArray()) {
+            if (TheOtherRoles.rnd.NextSingle() < eventProbabilities[(int)curEvent]) {
+                eventQueue.Remove(curEvent);
+                StartEvent(curEvent);
             }
         }
 
-        private static DateTime enabled = DateTime.FromBinary(-8585213068854775808);
-        public static bool isEventDate => DateTime.Today.Date == enabled;
+        AddToQueue(EventTypes.Animation);
+        AddToQueue(EventTypes.Invert);
+        if (!knocked) {
+            AddToQueue(EventTypes.KnockKnock);
+        }
+    }
 
-        public static bool canBeEnabled => DateTime.Today.Date > enabled && DateTime.Today.Date <= enabled.AddDays(7); // One Week after the EVENT
-        public static bool isEnabled => isEventDate || canBeEnabled && CustomOptionHolder.enableCodenameHorsemode != null && CustomOptionHolder.enableCodenameHorsemode.getBool();
+    private static DateTime enabled = DateTime.FromBinary(-8585213068854775808);
+    public static bool isEventDate => DateTime.Today.Date == enabled;
 
-        public static void AddToQueue(EventTypes newEvent) {
-            if (!isEnabled || eventQueue == null || eventQueue.Contains(newEvent)) return;
-            eventQueue.Add(newEvent);
-        }
+    public static bool canBeEnabled => DateTime.Today.Date > enabled && DateTime.Today.Date <= enabled.AddDays(7); // One Week after the EVENT
+    public static bool isEnabled => isEventDate || canBeEnabled && CustomOptionHolder.enableCodenameHorsemode != null && CustomOptionHolder.enableCodenameHorsemode.getBool();
 
+    public static void AddToQueue(EventTypes newEvent) {
+        if (!isEnabled || eventQueue == null || eventQueue.Contains(newEvent)) return;
+        eventQueue.Add(newEvent);
+    }
 
-        private static string defaultHat = "default";
-        public static void meetingEndsUpdate() {
-            if (!isEnabled) return;
-            PlayerControl.LocalPlayer.RpcSetHat(CustomHatLoader.horseHatProductIds[rnd.Next(CustomHatLoader.horseHatProductIds.Count)]);
-        }
 
+    private static string defaultHat = "default";
+    public static void meetingEndsUpdate() {
+        if (!isEnabled) return;
+        // TODO - Implement Horse hats
+        // PlayerControl.LocalPlayer.RpcSetHat(CustomHatLoader.horseHatProductIds[rnd.Next(CustomHatLoader.horseHatProductIds.Count)]);
+    }
 
-        public static void meetingStartsUpdate() {
-            if (!isEnabled) return;
-            if (rnd.NextDouble() <= 0.3f) { AddToQueue(EventTypes.Communication); }
-            PlayerControl.LocalPlayer.RpcSetHat(defaultHat);
-            HudManager.Instance.StartCoroutine(Effects.Lerp(1f, new Action<float>((p) => {
-                if (MeetingHud.Instance && MeetingHud.Instance.playerStates != null) {
-                    foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) {
-                        GameData.PlayerInfo pInfo = GameData.Instance.AllPlayers.ToArray().First(x => x.PlayerId == pva.TargetPlayerId);
-                        pva.SetCosmetics(pInfo);  // Needed cause cosmetics are set async'd.
-                    }
+
+    public static void meetingStartsUpdate() {
+        if (!isEnabled) return;
+        if (rnd.NextDouble() <= 0.3f) { AddToQueue(EventTypes.Communication); }
+        PlayerControl.LocalPlayer.RpcSetHat(defaultHat);
+        HudManager.Instance.StartCoroutine(Effects.Lerp(1f, new Action<float>((p) => {
+            if (MeetingHud.Instance && MeetingHud.Instance.playerStates != null) {
+                foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) {
+                    GameData.PlayerInfo pInfo = GameData.Instance.AllPlayers.ToArray().First(x => x.PlayerId == pva.TargetPlayerId);
+                    pva.SetCosmetics(pInfo);  // Needed cause cosmetics are set async'd.
                 }
-            })));
-        }
+            }
+        })));
+    }
 
-        public static void gameStartsUpdate() {
-            if (!isEnabled) return;
-            defaultHat = PlayerControl.LocalPlayer.Data.DefaultOutfit.HatId;
-            meetingEndsUpdate();
-            List<CachedPlayer> relevantPlayers = CachedPlayer.AllPlayers.Where(x => !x.Data.IsDead && x != CachedPlayer.LocalPlayer).ToList();
-            foreach (CachedPlayer pc in relevantPlayers)
-                pc.PlayerControl.MyPhysics.SetBodyType(disableHorses ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse);
+    public static void gameStartsUpdate() {
+        if (!isEnabled) return;
+        defaultHat = PlayerControl.LocalPlayer.Data.DefaultOutfit.HatId;
+        meetingEndsUpdate();
+        List<CachedPlayer> relevantPlayers = CachedPlayer.AllPlayers.Where(x => !x.Data.IsDead && x != CachedPlayer.LocalPlayer).ToList();
+        foreach (CachedPlayer pc in relevantPlayers)
+            pc.PlayerControl.MyPhysics.SetBodyType(disableHorses ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse);
 
-        }
+    }
 
-        public static void gameEndsUpdate() {
-            if (!isEnabled) return;
-            if (defaultHat != "default")
-                PlayerControl.LocalPlayer.RpcSetHat(defaultHat);
-        }
+    public static void gameEndsUpdate() {
+        if (!isEnabled) return;
+        if (defaultHat != "default")
+            PlayerControl.LocalPlayer.RpcSetHat(defaultHat);
+    }
 
-        public static void StartEvent(EventTypes eventToStart) {
-            List<CachedPlayer> relevantPlayers = CachedPlayer.AllPlayers.Where(x => !x.Data.IsDead && x != CachedPlayer.LocalPlayer).ToList();
-            switch (eventToStart) {
-                case EventTypes.Animation:
-                    CachedPlayer animationPlayer = relevantPlayers[rnd.Next(relevantPlayers.Count)];
-                    animationPlayer.PlayerPhysics.SetBodyType(rnd.Next(2) > 0 ? (disableHorses ? PlayerBodyTypes.Horse : PlayerBodyTypes.Normal) : PlayerBodyTypes.Seeker);
-                    FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(eventDurations[(int)EventTypes.Animation], new Action<float>((p) => {
-                        if (p==1)
-                            animationPlayer.PlayerControl.MyPhysics.SetBodyType(disableHorses ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse);
-                    })));
-                    break;
-                case EventTypes.Communication:
-                    int index = TheOtherRoles.rnd.Next(relevantPlayers.Count);
-                    CachedPlayer firstPlayer = relevantPlayers[index];
-                    relevantPlayers.RemoveAt(index);
-                    string msg = firstPlayer.Data.PlayerName + " ";
-                    foreach (CachedPlayer pc in relevantPlayers.ToArray()) {
-                        if (TheOtherRoles.rnd.NextSingle() < 1f / relevantPlayers.Count) {
-                            relevantPlayers.Remove(pc);
-                            msg += pc.Data.PlayerName + " ";
-                        }
+    public static void StartEvent(EventTypes eventToStart) {
+        List<CachedPlayer> relevantPlayers = CachedPlayer.AllPlayers.Where(x => !x.Data.IsDead && x != CachedPlayer.LocalPlayer).ToList();
+        switch (eventToStart) {
+            case EventTypes.Animation:
+                CachedPlayer animationPlayer = relevantPlayers[rnd.Next(relevantPlayers.Count)];
+                animationPlayer.PlayerPhysics.SetBodyType(rnd.Next(2) > 0 ? (disableHorses ? PlayerBodyTypes.Horse : PlayerBodyTypes.Normal) : PlayerBodyTypes.Seeker);
+                FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(eventDurations[(int)EventTypes.Animation], new Action<float>((p) => {
+                    if (p==1)
+                        animationPlayer.PlayerControl.MyPhysics.SetBodyType(disableHorses ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse);
+                })));
+                break;
+            case EventTypes.Communication:
+                int index = TheOtherRoles.rnd.Next(relevantPlayers.Count);
+                CachedPlayer firstPlayer = relevantPlayers[index];
+                relevantPlayers.RemoveAt(index);
+                string msg = firstPlayer.Data.PlayerName + " ";
+                foreach (CachedPlayer pc in relevantPlayers.ToArray()) {
+                    if (TheOtherRoles.rnd.NextSingle() < 1f / relevantPlayers.Count) {
+                        relevantPlayers.Remove(pc);
+                        msg += pc.Data.PlayerName + " ";
                     }
-                    RoomTracker tracker = FastDestroyableSingleton<HudManager>.Instance.roomTracker;
-                    string lastRoom = "";
-                    try { lastRoom = FastDestroyableSingleton<TranslationController>.Instance.GetString(tracker.LastRoom.RoomId); } catch { }
-                    msg += lastRoom != ""? lastRoom : (TheOtherRoles.rnd.Next(2) > 0 ? "sus" : "safe");
-
-                    FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(relevantPlayers[rnd.Next(relevantPlayers.Count)], $"{msg}");
-                    break;
-                case EventTypes.Invert:
-                    eventInvert = true;
-                    FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(eventDurations[(int)EventTypes.Invert], new Action<float>((p) => {
-                        if (p == 1)
-                            eventInvert = false;
-                    })));
-                    break;
-                case EventTypes.KnockKnock:
-                    SoundEffectsManager.play("knockKnock");
-                    knocked = true;
-                    break;
+                }
+                RoomTracker tracker = FastDestroyableSingleton<HudManager>.Instance.roomTracker;
+                string lastRoom = "";
+                try { lastRoom = FastDestroyableSingleton<TranslationController>.Instance.GetString(tracker.LastRoom.RoomId); } catch { }
+                msg += lastRoom != ""? lastRoom : (TheOtherRoles.rnd.Next(2) > 0 ? "sus" : "safe");
+
+                FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(relevantPlayers[rnd.Next(relevantPlayers.Count)], $"{msg}");
+                break;
+            case EventTypes.Invert:
+                eventInvert = true;
+                FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(eventDurations[(int)EventTypes.Invert], new Action<float>((p) => {
+                    if (p == 1)
+                        eventInvert = false;
+                })));
+                break;
+            case EventTypes.KnockKnock:
+                SoundEffectsManager.play("knockKnock");
+                knocked = true;
+                break;
 
-            }
         }
     }
-}
+}
\ No newline at end of file
index ae1b9649eb8c4ca899b5ef8e4219eff36f613cfc..3a2a5c34f48d972ff7a99cf7cb4b18f7556e5374 100644 (file)
@@ -4,9 +4,9 @@
     "net6.0": {
       "AmongUs.GameLibs.Steam": {
         "type": "Direct",
-        "requested": "[2023.7.11, )",
-        "resolved": "2023.7.11",
-        "contentHash": "e5zbU+4AYnp1zAQDizO5GRIh1kemkgCa8rFVAcr9bxeupkBu5dwwdHxArWv/pUybIJTZfOPjkOnicZ2qKWLX7g=="
+        "requested": "[2023.11.28, )",
+        "resolved": "2023.11.28",
+        "contentHash": "EsKrZS4tjTRrY7mtZls3tMy54zije4vleEA9s0UGUr4SF3qm2axR6wMncyIJCk4ACXlN5Bxpsd3/upDe9Gnx6A=="
       },
       "BepInEx.IL2CPP.MSBuild": {
         "type": "Direct",