]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Version 2.5.1
authorEisbison <eisbison.creator@gmail.com>
Thu, 29 Apr 2021 15:48:33 +0000 (17:48 +0200)
committerEisbison <eisbison.creator@gmail.com>
Thu, 29 Apr 2021 15:48:33 +0000 (17:48 +0200)
12 files changed:
Images/Preview_2.5.0.png [deleted file]
Images/Preview_2.5.1.png [new file with mode: 0644]
README.md
Source Code/Buttons.cs
Source Code/ChatCommands.cs
Source Code/CustomHats.cs
Source Code/Garlic.cs
Source Code/Main.cs
Source Code/RPC.cs
Source Code/TasksHandler.cs
Source Code/TheOtherRoles.cs
Source Code/TheOtherRoles.csproj

diff --git a/Images/Preview_2.5.0.png b/Images/Preview_2.5.0.png
deleted file mode 100644 (file)
index 3c43d5d..0000000
Binary files a/Images/Preview_2.5.0.png and /dev/null differ
diff --git a/Images/Preview_2.5.1.png b/Images/Preview_2.5.1.png
new file mode 100644 (file)
index 0000000..c0684b3
Binary files /dev/null and b/Images/Preview_2.5.1.png differ
index 72cf98464f5caf9aeb088b7b022f731fdfee2125..df97db9ceb2dc446b0849542243bc7b8604a29c3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![Discord](https://discord.com/assets/e4923594e694a21542a489471ecffa50.svg)](https://discord.gg/77RkMJHWsM)\r
 ![eisbison banner](./Images/Banner.png)\r
-![eisbison infographic](./Images/Preview_2.5.0.png)\r
+![eisbison infographic](./Images/Preview_2.5.1.png)\r
 \r
 \r
 # The Other Roles\r
@@ -18,10 +18,11 @@ Even more roles are coming soon :)
 | [Godfather (Mafia)](#mafia) | [Lighter](#lighter) |  |\r
 | [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) |  |\r
 | [Janitor (Mafia)](#mafia)  | [Medic](#medic) |  |\r
-| [Morphling](#morphling) | [Seer](#seer) |  |\r
-| [Trickster](#trickster) | [Sheriff](#sheriff) |  |\r
-| [Vampire](#vampire) | [Shifter](#shifter) |  |\r
-| [Warlock](#warlock) | [Snitch](#snitch) |  |\r
+| [Morphling](#morphling) | [Security Guard](#security-guard) |  |\r
+| [Trickster](#trickster) | [Seer](#seer) |  |\r
+| [Vampire](#vampire) | [Sheriff](#sheriff) |  |\r
+| [Warlock](#warlock) | [Shifter](#shifter) |  |\r
+|  | [Snitch](#snitch) |  |\r
 |  | [Spy](#spy) |  |\r
 |  | [Swapper](#swapper) |  |\r
 |  | [Time Master](#time-master) |  |\r
@@ -32,6 +33,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 # Releases\r
 | Among Us - Version| Mod Version | Link |\r
 |----------|-------------|-----------------|\r
+| 2021.4.14s| v2.5.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.5.1/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.5.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.5.0/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.4.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.4.0/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.3.0/TheOtherRoles.zip)\r
@@ -56,6 +58,13 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 <details>\r
   <summary>Click to show the Changelog</summary>\r
 \r
+**Version 2.5.1**\r
+- **New Hats:** We added the support for custom hats and there are already a few hats inside the game. We can add new hats without updating the mod and we're awaiting your hat designs on our discord server.\r
+- Changed Lovers to ignore Lover's Tasks for Taskwin, while an ImpLover is alive\r
+- Fixed a bug where garlic was not visible in some places\r
+- The Security Guard can't place cameras on MiraHQ anymore\r
+- Fixed a bug on there Airship, where the view of the cameras that the Security Guard placed wasn't centered on the camera.\r
+\r
 **Version 2.5.0**\r
 - **New Role:** [Security Guard](#security-guard)\r
 - Fixed a bug where the game would stop after the first meeting\r
@@ -514,6 +523,7 @@ The Lovers never know the role of their partner, they only see who their partner
 The Lovers win if they are both alive among the last 3 players, however they can also win with their respective role.\\r
 If both Lovers are Crewmates, they can achieve a "double victory" but the Crewmates then also win.\\r
 If one Lover is an Impostor and one a Crewmate, they can only get a "Lovers solo win" where only they win (even if there is another Impostor alive).\\r
+If there's an ImpLover in the game, the tasks of the Crewmate Lover won't be counted (for a task win) as long as he's alive. If the Lover dies, his tasks will also be counted.\\r
 \\r
 **NOTE:**\r
 - If there is an equal amount of Impostors and Crewmates left in the game and one of the Lovers is an Impostor, the game is not automatically over since the Lovers can still achieve a solo win. E.g. if there are the following roles Impostor + ImpLover + Lover + Crewmate left, the game will not end and the next kill will decide if the Impostors or Lovers win.\r
@@ -876,6 +886,7 @@ The new camera will be visible after the next meeting and accessible by everyone
 The vents will be sealed after the next meeting, players can't enter or exit sealed vents, but they can still "move to them" underground.\\r
 **NOTE:**\r
 - Tickster boxes can't be sealed\r
+- The Security Guard can't place Cameras on MiraHQ\r
 - The remaining number of screws can be seen above his special button\r
 - On Skeld the four cameras will be replaced every 3 seconds (with the next four cameras). You can also navigate manually using the arrow keys.\r
 \r
index fb54c6e49f2b9c8f2931bf93253424cd5518c50c..368d18b12faf0040e54600abc3daf0e5bd122dcd 100644 (file)
@@ -682,7 +682,13 @@ namespace TheOtherRoles
             // SecurityGuard button
             securityGuardButton = new CustomButton(
                 () => {
-                    if (SecurityGuard.ventTarget == null) { // Place camera
+                    if (SecurityGuard.ventTarget != null) { // Seal vent
+                        MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SealVent, Hazel.SendOption.Reliable);
+                        writer.WritePacked(SecurityGuard.ventTarget.Id);
+                        writer.EndMessage();
+                        RPCProcedure.sealVent(SecurityGuard.ventTarget.Id);
+                        SecurityGuard.ventTarget = null;
+                    } else if (PlayerControl.GameOptions.MapId != 1) { // Place camera if there's no vent and it's not MiraHQ
                         var pos = PlayerControl.LocalPlayer.transform.position;
                         byte[] buff = new byte[sizeof(float) * 2];
                         Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0*sizeof(float), sizeof(float));
@@ -692,20 +698,17 @@ namespace TheOtherRoles
                         writer.WriteBytesAndSize(buff);
                         writer.EndMessage();
                         RPCProcedure.placeCamera(buff); 
-                    } else { // Seal vent
-                        MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SealVent, Hazel.SendOption.Reliable);
-                        writer.WritePacked(SecurityGuard.ventTarget.Id);
-                        writer.EndMessage();
-                        RPCProcedure.sealVent(SecurityGuard.ventTarget.Id);
-                        SecurityGuard.ventTarget = null;
                     }
                     securityGuardButton.Timer = securityGuardButton.MaxTimer;
                 },
                 () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
                 () => {
-                    securityGuardButton.killButtonManager.renderer.sprite = (SecurityGuard.ventTarget == null) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite(); 
+                    securityGuardButton.killButtonManager.renderer.sprite = (SecurityGuard.ventTarget == null && PlayerControl.GameOptions.MapId != 1) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite(); 
                     if (securityGuardButtonScrewsText != null) securityGuardButtonScrewsText.text = $"{SecurityGuard.remainingScrews}/{SecurityGuard.totalScrews}";
-                    return SecurityGuard.remainingScrews >= (SecurityGuard.ventTarget == null ? SecurityGuard.camPrice : SecurityGuard.ventPrice) && PlayerControl.LocalPlayer.CanMove;
+
+                    if (SecurityGuard.ventTarget != null)
+                        return SecurityGuard.remainingScrews >= SecurityGuard.ventPrice && PlayerControl.LocalPlayer.CanMove;
+                    return PlayerControl.GameOptions.MapId != 1 && SecurityGuard.remainingScrews >= SecurityGuard.camPrice && PlayerControl.LocalPlayer.CanMove;
                 },
                 () => { securityGuardButton.Timer = securityGuardButton.MaxTimer; },
                 SecurityGuard.getPlaceCameraButtonSprite(),
index 930fa85dcfd7ebc138e442cb85ad143716cf4404..98d3daf45f95f56b734c7dd5ae2879a2d450846e 100644 (file)
@@ -1,4 +1,6 @@
 using System;
+using System.Security.Cryptography;
+using System.Text;
 using BepInEx;
 using BepInEx.Configuration;
 using BepInEx.IL2CPP;
@@ -15,18 +17,22 @@ namespace TheOtherRoles {
                 string text = __instance.TextArea.text;
                 bool handled = false;
                 if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) {
-                    if (text.ToLower().Equals("i'm a panda")) {
-                        handled = true;
-                        byte colorId = (byte)CustomColors.pickableColors;
-                        SaveManager.BodyColor = (byte)colorId;
-                        if (PlayerControl.LocalPlayer)
-                            PlayerControl.LocalPlayer.CmdCheckColor(colorId);
-                    } else if (text.ToLower().Equals("nightsky")) {
-                        handled = true;
-                        byte colorId = (byte)(CustomColors.pickableColors + 1);
-                        SaveManager.BodyColor = (byte)colorId;
-                        if (PlayerControl.LocalPlayer)
-                            PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                    using(MD5 md5 = MD5.Create()) {
+                        string hash = System.BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(text.ToLower()))).Replace("-", "").ToLowerInvariant();
+                        if (hash.Equals("f92af861c8b7aa5f2d05165abc9ba04f")) {
+                            handled = true;
+                            byte colorId = (byte)CustomColors.pickableColors;
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        } else if (hash.Equals("5a2ee6de2e42d2b326556640f838b0d7")) {
+                            handled = true;
+                            byte colorId = (byte)(CustomColors.pickableColors + 1);
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        }
+                        System.Console.WriteLine(hash);
                     }
                 }
                 if (handled) {
index 2a1fb985dcf752e9de30c1734923e3e784c2465c..a5a29e9b839094f363151de48bd5cca0c68cc180 100644 (file)
@@ -1,15 +1,26 @@
+extern alias Il2CppNewtonsoft;
 using System;
 using BepInEx;
 using BepInEx.Configuration;
 using BepInEx.IL2CPP;
+using Il2CppSystem;
 using HarmonyLib;
 using UnityEngine;
-using System.Collections.Generic;
+using UnhollowerBaseLib;
 using System.IO;
 using System.Reflection;
 using System.Collections;
-using UnhollowerBaseLib;
+using System.Collections.Generic;
 using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+using Il2CppNewtonsoft::Newtonsoft.Json.Linq;
+using Il2CppNewtonsoft::Newtonsoft.Json;
 
 namespace TheOtherRoles {
     [HarmonyPatch]
@@ -17,14 +28,31 @@ namespace TheOtherRoles {
         private static bool LOADED = false;
         public static Material hatShader;
 
-        protected internal struct CustomHat { 
-            public string name;
-            public string resource;
-            public string backresource;
-            public string climbresource;
-            public bool bounce;
-            public bool adaptive;
-            public bool behind;
+        public static Dictionary<string, HatExtension> CustomHatRegistry = new Dictionary<string, HatExtension>();
+
+        public class HatExtension {
+            public string author { get; set;}
+            public string package { get; set;}
+            public string condition { get; set;}
+
+            public bool isUnlocked() {
+                if (condition == null || condition.ToLower() == "none") 
+                    return true;
+                return false;
+            }
+        }
+
+        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 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) {
@@ -44,7 +72,6 @@ namespace TheOtherRoles {
                     climbs.Add(p[0], hats[i]);
                 if (options.Contains("back"))
                     backs.Add(p[0], hats[i]);
-                
                 if (!options.Contains("back") && !options.Contains("climb")) {
                     CustomHat custom = new CustomHat { resource = hats[i] };
                     custom.name = p[0].Replace('-', ' ');
@@ -60,10 +87,16 @@ namespace TheOtherRoles {
 
             foreach (string k in fronts.Keys) {
                 CustomHat hat = fronts[k];
-                backs.TryGetValue(k, out hat.backresource);
-                climbs.TryGetValue(k, out hat.climbresource);
+                string br, cr;
+                backs.TryGetValue(k, out br);
+                climbs.TryGetValue(k, out cr);
+                if (br != null)
+                    hat.backresource = br;
+                if (cr != null)
+                    hat.climbresource = cr;
                 if (hat.backresource != null)
                     hat.behind = true;
+
                 customhats.Add(hat);
             }
 
@@ -75,11 +108,22 @@ namespace TheOtherRoles {
             return texture == null ? null : Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.8f), texture.width * 0.75f);
         }
 
-        private static HatBehaviour CreateHatBehaviour(CustomHat ch, bool fromDisk = false) {
+        private static HatBehaviour CreateHatBehaviour(CustomHat ch, bool fromDisk = false, bool testOnly = false) {
+            if (hatShader == null && DestroyableSingleton<HatManager>.InstanceExists) {
+                foreach (HatBehaviour h in DestroyableSingleton<HatManager>.Instance.AllHats) {
+                    if (h.AltShader != null) {
+                        hatShader = h.AltShader;
+                        break;
+                    }
+                }
+            }
+
             HatBehaviour hat = new HatBehaviour();
             hat.MainImage = CreateHatSprite(ch.resource, fromDisk);
-            if (ch.backresource != null)
+            if (ch.backresource != null) {
                 hat.BackImage = CreateHatSprite(ch.backresource, fromDisk);
+                ch.behind = true; // Required to view backresource
+            }
             if (ch.climbresource != null)
                 hat.ClimbImage = CreateHatSprite(ch.climbresource, fromDisk);
             hat.name = ch.name;
@@ -87,23 +131,31 @@ namespace TheOtherRoles {
             hat.ProductId = "hat_" + ch.name.Replace(' ', '_');
             hat.InFront = !ch.behind;
             hat.NoBounce = !ch.bounce;
-            hat.ChipOffset = new Vector2(0f, 0.35f);
+            hat.ChipOffset = new Vector2(0f, 0.2f);
 
             if (ch.adaptive && hatShader != null)
                 hat.AltShader = hatShader;
 
+            if (!testOnly) {
+                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";
+
+                CustomHatRegistry.Add(hat.name, extend);
+            }
+
             return hat;
         }
 
-        private static void loadHatShader() {
-            if (hatShader != null || !DestroyableSingleton<HatManager>.InstanceExists) return;
-
-            foreach (HatBehaviour hat in DestroyableSingleton<HatManager>.Instance.AllHats) {
-                if (hat.AltShader != null) {
-                    hatShader = hat.AltShader;
-                    break;
-                }
-            }
+        private static HatBehaviour CreateHatBehaviour(CustomHatLoader.CustomHatOnline chd) {
+            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;
+            return CreateHatBehaviour(chd, true);
         }
 
         [HarmonyPatch(typeof(HatManager), nameof(HatManager.GetHatById))]
@@ -111,8 +163,6 @@ namespace TheOtherRoles {
             static bool Prefix(HatManager __instance) {
                 try {
                     if (!LOADED) {
-                        loadHatShader();
-
                         Assembly assembly = Assembly.GetExecutingAssembly();
                         string hatres = $"{assembly.GetName().Name}.Resources.CustomHats";
                         string[] hats = (from r in assembly.GetManifestResourceNames()
@@ -123,17 +173,22 @@ namespace TheOtherRoles {
                         foreach (CustomHat ch in customhats)
                             __instance.AllHats.Add(CreateHatBehaviour(ch));
 
+                        while (CustomHatLoader.hatdetails.Count > 0) {
+                            __instance.AllHats.Add(CreateHatBehaviour(CustomHatLoader.hatdetails[0]));
+                            CustomHatLoader.hatdetails.RemoveAt(0);
+                        }
+
                         LOADED = true;
                     }
                     return true;
-                } catch (Exception e) {
+                } catch (System.Exception e) {
                     System.Console.WriteLine("Unable to add Custom Hats\n" + e);
                     return false;
                 }
             }
         }
 
-        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), new Type[] { typeof(uint), typeof(int) })]
+        [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), new System.Type[] { typeof(uint), typeof(int) })]
         private static class HatParentSetHatPatch {
             static void Postfix(HatParent __instance, [HarmonyArgument(0)]uint hatId, [HarmonyArgument(1)]int color) {
                 if (DestroyableSingleton<TutorialManager>.InstanceExists) {
@@ -141,17 +196,281 @@ namespace TheOtherRoles {
                         string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\Test";
                         DirectoryInfo d = new DirectoryInfo(filePath);
                         string[] filePaths = d.GetFiles("*.png").Select(x => x.FullName).ToArray(); // Getting Text files
-                        List<CustomHat> customHats = createCustomHatDetails(filePaths, true);
-                        if (customHats.Count > 0) {
-                            loadHatShader();
-                            __instance.Hat = CreateHatBehaviour(customHats[0], true);
+                        List<CustomHat> hats = createCustomHatDetails(filePaths, true);
+                        if (hats.Count > 0) {
+                            __instance.Hat = CreateHatBehaviour(hats[0], true, true);
                             __instance.SetHat(color);
                         }
-                    } catch (Exception e) {
+                    } catch (System.Exception e) {
                         System.Console.WriteLine("Unable to create test hat\n" + e);
                     }
                 }
             }     
         }
+
+        private static List<TMPro.TMP_Text> hatsTabCustomTexts = new List<TMPro.TMP_Text>();
+
+        [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<HatBehaviour, HatExtension>> hats, string packageName, float YStart, HatsTab __instance) {
+                bool isDefaultPackage = innerslothPackageName == packageName;
+                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.currentFontSize
+                    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;
+                    hatsTabCustomTexts.Add(title);
+                }
+                for (int i = 0; i < hats.Count; i++) {
+                    HatBehaviour 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 (ext != null) {
+                        Transform background = colorChip.transform.FindChild("Background");
+                        Transform foreground = colorChip.transform.FindChild("ForeGround");
+
+                        if (background != null) {
+                            background.localScale = new Vector3(1, 1.5f, 1);
+                            background.localPosition = Vector3.down * 0.243f;
+                        }
+                        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.75f, -1f);
+                            description.transform.localScale = Vector3.one * 0.7f;
+                            __instance.StartCoroutine(Effects.Lerp(0.1f, new System.Action<float>((p) => { description.SetText($"{hat.name}\nby {ext.author}"); })));
+                            hatsTabCustomTexts.Add(description);
+                        }
+
+                        if (!ext.isUnlocked()) { // Hat is locked
+                            UnityEngine.Object.Destroy(colorChip.Button);
+                            var overlay = UnityEngine.Object.Instantiate(colorChip.InUseForeground, colorChip.transform);
+                            overlay.SetActive(true);
+                        }
+                    }
+                    
+                    colorChip.transform.localPosition = new Vector3(xpos, ypos, -1f);
+                    colorChip.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => { __instance.SelectHat(hat); }));
+                    colorChip.Inner.SetHat(hat, PlayerControl.LocalPlayer.Data.ColorId);
+                    colorChip.Inner.transform.localPosition = hat.ChipOffset;
+                    colorChip.Tag = hat;
+                    __instance.ColorChips.Add(colorChip);
+                }
+                return offset - ((hats.Count - 1) / __instance.NumPerRow * (isDefaultPackage ? 1f : 1.5f) + 0.7f) * __instance.YOffset;
+            }
+
+            public static bool Prefix(HatsTab __instance) {
+                PlayerControl.SetPlayerMaterialColors(PlayerControl.LocalPlayer.Data.ColorId, __instance.DemoImage);
+                __instance.HatImage.SetHat(SaveManager.LastHat, PlayerControl.LocalPlayer.Data.ColorId);
+                PlayerControl.SetSkinImage(SaveManager.LastSkin, __instance.SkinImage);
+                PlayerControl.SetPetImage(SaveManager.LastPet, PlayerControl.LocalPlayer.Data.ColorId, __instance.PetImage);
+
+                HatBehaviour[] unlockedHats = DestroyableSingleton<HatManager>.Instance.GetUnlockedHats();
+                Dictionary<string, List<System.Tuple<HatBehaviour, HatExtension>>> packages = new Dictionary<string, List<System.Tuple<HatBehaviour, HatExtension>>>();
+                hatsTabCustomTexts = new List<TMPro.TMP_Text>();
+
+                foreach (HatBehaviour hatBehaviour in unlockedHats) {
+                    HatExtension ext = hatBehaviour.getHatExtension();
+
+                    if (ext != null) {
+                        if (!packages.ContainsKey(ext.package)) 
+                            packages[ext.package] = new List<System.Tuple<HatBehaviour, HatExtension>>();
+                        packages[ext.package].Add(new System.Tuple<HatBehaviour, HatExtension>(hatBehaviour, ext));
+                    } else {
+                        if (!packages.ContainsKey(innerslothPackageName)) 
+                            packages[innerslothPackageName] = new List<System.Tuple<HatBehaviour, HatExtension>>();
+                        packages[innerslothPackageName].Add(new System.Tuple<HatBehaviour, HatExtension>(hatBehaviour, null));
+                    }
+                }
+
+                float YOffset = __instance.YStart;
+
+                var hatButton = GameObject.Find("HatButton");
+
+                if (hatButton != null && hatButton.transform.FindChild("ButtonText_TMP") != null) {
+                    textTemplate = hatButton.transform.FindChild("ButtonText_TMP").GetComponent<TMPro.TMP_Text>();
+                }
+
+                var orderedKeys = packages.Keys.OrderBy((string x) => x == innerslothPackageName ? 1000 : 0);
+                foreach (string key in orderedKeys) {
+                    List<System.Tuple<HatBehaviour, HatExtension>> value = packages[key];
+                    YOffset += createHatPackage(value, key, YOffset, __instance);
+                }
+                __instance.scroller.YBounds.max = (YOffset - 0.6f) * -0.75f;
+                return false;
+            }
+        }
+
+        [HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
+        public class HatsTabUpdatePatch {
+            public static void Postfix(HatsTab __instance) {
+                // Manually hide all custom TMPro.TMP_Text objects that are outside the ScrollRect
+                foreach (TMPro.TMP_Text customText in hatsTabCustomTexts) {
+                    if (customText != null && customText.transform != null && customText.gameObject != null) {
+                        bool active = customText.transform.position.y <= 3.75f && customText.transform.position.y >= 0.3f;
+                        float epsilon = Mathf.Min(Mathf.Abs(customText.transform.position.y - 3.75f), Mathf.Abs(customText.transform.position.y - 0.35f));
+                        if (active != customText.gameObject.active && epsilon > 0.1f) customText.gameObject.SetActive(active);
+                    }
+                }
+            }
+        }
+    }
+
+    public class CustomHatLoader {
+        public bool running = false;
+        private const string REPO = "https://raw.githubusercontent.com/Eisbison/TheOtherHats/master";
+
+        public static List<CustomHatOnline> hatdetails = new List<CustomHatOnline>();
+
+        public void LaunchHatFetcher() {
+            if (this.running)
+                return;
+            this.running = true;
+            this.LaunchHatFetcherAsync();
+        }
+
+        private async void LaunchHatFetcherAsync() {
+            try {
+                HttpStatusCode status = await this.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);
+            }
+            this.running = false;
+        }
+
+        private string sanitizeResourcePath(string res) {
+            if (res == null || !res.EndsWith(".png")) 
+                return null;
+
+            res = res.Replace("\\", "")
+                     .Replace("/", "")
+                     .Replace("*", "")
+                     .Replace("..", "");   
+            return res;
+        }
+
+        public async Task<HttpStatusCode> FetchHats() {
+            using (HttpClient http = new HttpClient()) {
+                http.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue{ NoCache = true };
+                using (HttpRequestMessage msg = new HttpRequestMessage()) {
+                    msg.Method = HttpMethod.Get;
+                    msg.RequestUri = new System.Uri($"{REPO}/CustomHats.json");
+                    HttpResponseMessage httpResponseMessage = await http.SendAsync(msg);
+                    using (HttpResponseMessage res = httpResponseMessage) {
+                        try {
+                            if (res.Content == null) {
+                                System.Console.WriteLine("Server returned no data: " + res.StatusCode.ToString());
+                                return HttpStatusCode.ExpectationFailed;
+                            }
+                            if (res.StatusCode != HttpStatusCode.OK) 
+                                return res.StatusCode;
+
+                            string json = await res.Content.ReadAsStringAsync();
+                            // System.Console.WriteLine(json);
+
+                            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.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\";
+                            using (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);
+                                }
+                            }
+                            markedfordownload.AsParallel().ForAll(file => {
+                                System.Uri url = new System.Uri($"{REPO}/hats/{file}");
+                                // System.Console.WriteLine($"Downloading {file}");
+                                var fres = http.GetAsync(url, HttpCompletionOption.ResponseContentRead);
+                                if (fres.Result.StatusCode != HttpStatusCode.OK)
+                                    return;
+                                using (var resStream = fres.Result.Content.ReadAsStreamAsync().Result) {
+                                    using (var fileStream = File.Create($"{filePath}\\{file}")) {
+                                        resStream.CopyTo(fileStream);
+                                    }
+                                }
+                            });
+
+                            hatdetails = hatdatas;
+                        } catch (System.Exception ex) {
+                            System.Console.WriteLine(ex);
+                        }
+                    }
+                }
+            }
+            return HttpStatusCode.OK;
+        }
+
+        private 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 class CustomHatOnline : CustomHats.CustomHat { 
+            public string reshasha { get; set;}
+            public string reshashb { get; set;}
+            public string reshashc { get; set;}
+        }   
+    }
+    public static class CustomHatExtensions {
+        public static CustomHats.HatExtension getHatExtension(this HatBehaviour hat) {
+            CustomHats.HatExtension ret = null;
+            CustomHats.CustomHatRegistry.TryGetValue(hat.name, out ret);
+            return ret;
+        }
     }
-}
\ No newline at end of file
+}
index e47b3e0f895f007358d0329135738f200a57afd5..e988bd4be2c415af2ebd001210408cb0a82830b3 100644 (file)
@@ -28,10 +28,10 @@ namespace TheOtherRoles{
             garlic = new GameObject("Garlic");
             background = new GameObject("Background");
             background.transform.SetParent(garlic.transform);
-            Vector3 position = new Vector3(p.x, p.y, PlayerControl.LocalPlayer.transform.position.z + 1f);
+            Vector3 position = new Vector3(p.x, p.y, PlayerControl.LocalPlayer.transform.localPosition.z + 0.001f); // just behind player
             garlic.transform.position = position;
             garlic.transform.localPosition = position;
-            background.transform.localPosition = new Vector3(0 , 0, 0.01f);
+            background.transform.localPosition = new Vector3(0 , 0, -0.01f); // before player
 
             var garlicRenderer = garlic.AddComponent<SpriteRenderer>();
             garlicRenderer.sprite = getGarlicSprite();
index a20402a4b5e8c341009782a32c3404af24ce5900..64839406dcfd137006a600a84f4697b4a0bfc486 100644 (file)
@@ -19,10 +19,10 @@ namespace TheOtherRoles
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string Version = "2.5.0";
+        public const string Version = "2.5.1";
         public const byte Major = 2;
         public const byte Minor = 5;
-        public const byte Patch = 0;
+        public const byte Patch = 1;
 
         public Harmony Harmony { get; } = new Harmony(Id);
         public static TheOtherRolesPlugin Instance;
@@ -52,6 +52,8 @@ namespace TheOtherRoles
         }
 
         public override void Load() {
+            new CustomHatLoader().LaunchHatFetcher();
+        
             DebugMode = Config.Bind("Custom", "Enable Debug Mode", false);
             StreamerMode = Config.Bind("Custom", "Enable Streamer Mode", false);
             GhostsSeeTasks = Config.Bind("Custom", "Ghosts See Remaining Tasks", true);
index 3d622214514dd214274b788a29676a070f1040a7..6dcdfcd7949c667f0321e5d674e7d4e5add234ed 100644 (file)
@@ -614,7 +614,8 @@ namespace TheOtherRoles
 
             var camera = UnityEngine.Object.Instantiate<SurvCamera>(referenceCamera);
             camera.transform.position = new Vector3(position.x, position.y, referenceCamera.transform.position.z - 1f);
-            camera.CamName = $"Security Guard Camera {SecurityGuard.placedCameras}";
+            camera.CamName = $"Security Camera {SecurityGuard.placedCameras}";
+            camera.Offset = new Vector3(0f, 0f, camera.Offset.z);
             if (PlayerControl.GameOptions.MapId == 2 || PlayerControl.GameOptions.MapId == 4) camera.transform.localRotation = new Quaternion(0, 0, 1, 1); // Polus and Airship 
             camera.gameObject.SetActive(false);
             MapOptions.camerasToAdd.Add(camera);
index af1ebf3350547192e3feae451ea722d09b46fc1a..43d24fb03ea4fa528dd4465b0e3c2e892820d83a 100644 (file)
@@ -15,7 +15,7 @@ namespace TheOtherRoles {
                 playerInfo.Object &&
                 (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) &&
                 !playerInfo.IsImpostor &&
-                !Helpers.hasFakeTasks(playerInfo.Object)
+                !playerInfo.Object.hasFakeTasks()
                 ) {
 
                 for (int j = 0; j < playerInfo.Tasks.Count; j++) {
@@ -35,6 +35,8 @@ namespace TheOtherRoles {
                 __instance.CompletedTasks = 0;
                 for (int i = 0; i < __instance.AllPlayers.Count; i++) {
                     GameData.PlayerInfo playerInfo = __instance.AllPlayers[i];
+                    if (playerInfo.Object && playerInfo.Object.hasAliveKillingLover())
+                        continue;
                     var (playerCompleted, playerTotal) = taskInfo(playerInfo);
                     __instance.TotalTasks += playerTotal;
                     __instance.CompletedTasks += playerCompleted;
index 8c43366852713ef4f7c1ca6ee153dc203e1cc3e0..28519b9597d5ba2f202b059d2013b1e1455936cd 100644 (file)
@@ -322,6 +322,16 @@ namespace TheOtherRoles
             return !(lover1.Data.IsImpostor || lover2.Data.IsImpostor || lover1 == Jackal.jackal || lover2 == Jackal.jackal || lover1 == Sidekick.sidekick || lover2 == Sidekick.sidekick); // Not Impostor, Sidekick or Jackal
         }
 
+        public static bool hasAliveKillingLover(this PlayerControl player) {
+            if (!Lovers.existingAndAlive())
+                return false;
+            if (player != null && player != lover1 && player != lover2) // Is Player a Lover?
+                return false;
+            return (lover1 == Jackal.jackal     || lover2 == Jackal.jackal 
+                 || lover1 == Sidekick.sidekick || lover2 == Sidekick.sidekick 
+                 || lover1.Data.IsImpostor      || lover2.Data.IsImpostor); // Either of the lovers is a killer
+        }
+
         public static void clearAndReload() {
             lover1 = null;
             lover2 = null;
@@ -758,7 +768,7 @@ namespace TheOtherRoles
 
         public static void clearAndReload() {
             cleaner = null;
-            cooldown = CustomOptionHolder.janitorCooldown.getFloat();
+            cooldown = CustomOptionHolder.cleanerCooldown.getFloat();
         }
     }
 
@@ -861,4 +871,4 @@ namespace TheOtherRoles
             ventPrice = Mathf.RoundToInt(CustomOptionHolder.securityGuardVentPrice.getFloat());
         }
     }
-}
\ No newline at end of file
+}
index 88a74b4e1bc9de704720ca7c798faedfd5bbf504..4aa7a90dabd14b90b69926bfe44d42d37e2cf5db 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>netstandard2.1</TargetFramework>
-        <Version>2.5.0</Version>
+        <Version>2.5.1</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
     </PropertyGroup>
@@ -10,6 +10,7 @@
         <GameVersion>2021.4.14s</GameVersion>
         <Mappings>EoF-1141/Mappings:0.4.1</Mappings>
         <DefineConstants>$(DefineConstants);STEAM</DefineConstants>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     </PropertyGroup>
 
     <ItemGroup>
     <Target Name="Copy" AfterTargets="Reobfuscate">
         <Copy SourceFiles="$(OutputPath)reobfuscated/$(AssemblyName)-$(GameVersion).dll" DestinationFolder="$(AmongUs)/BepInEx/plugins/" />
     </Target>
-</Project>
+    <!-- il2cpp newtonsoft -->
+       <ItemGroup>
+               <Reference Include="Newtonsoft.Json, Version=3.7.1.6">
+                       <HintPath>$(AmongUs)\BepInEx\unhollowed\Newtonsoft.Json.dll</HintPath>
+                       <Aliases>Il2CppNewtonsoft</Aliases>
+               </Reference>
+       </ItemGroup>
+</Project>
\ No newline at end of file