Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SneakyMasks v1.6.0
SneakyMasks.dll
Decompiled 3 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using SneakyMasks.Patches; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SneakyMasks")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.6.0.0")] [assembly: AssemblyInformationalVersion("1.6.0+99dda9d28835cb214527045eb23a45c96f67cd60")] [assembly: AssemblyProduct("SneakyMasks")] [assembly: AssemblyTitle("SneakyMasks")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.6.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SneakyMasks { internal sealed class MaskProcess { private static readonly ConditionalWeakTable<HauntedMaskItem, MaskProcess> States = new ConditionalWeakTable<HauntedMaskItem, MaskProcess>(); public float Delay; public float StartAt = float.MaxValue; public bool AutoStarted; public float TakeoverDelay; public float TakeoverAt = float.MaxValue; public bool ClimbImmediately; public int EscalationStep; public float AttachStartedAt; public static MaskProcess For(HauntedMaskItem mask) { return States.GetOrCreateValue(mask); } public void Restart(float delay) { Delay = delay; StartAt = Time.realtimeSinceStartup + delay; AutoStarted = false; TakeoverAt = float.MaxValue; ClimbImmediately = false; } public void Pocket(float takeoverDelay) { Delay = 0f; StartAt = float.MaxValue; AutoStarted = false; Reschedule(takeoverDelay); } public void Reschedule(float takeoverDelay) { TakeoverDelay = takeoverDelay; TakeoverAt = Time.realtimeSinceStartup + takeoverDelay; } public void Hold() { StartAt = Time.realtimeSinceStartup + Delay; } public void HoldTakeover() { TakeoverAt = Time.realtimeSinceStartup + TakeoverDelay; } public void Clear() { Delay = 0f; StartAt = float.MaxValue; AutoStarted = false; TakeoverDelay = 0f; TakeoverAt = float.MaxValue; ClimbImmediately = false; EscalationStep = 0; } } [BepInPlugin("Timofey.SneakyMasks", "SneakyMasks", "1.6.0")] public class Plugin : BaseUnityPlugin { private Harmony _harmony; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static PluginConfig Cfg { get; private set; } private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Cfg = new PluginConfig(((BaseUnityPlugin)this).Config); _harmony = new Harmony("Timofey.SneakyMasks"); _harmony.PatchAll(typeof(MaskPatches)); _harmony.PatchAll(typeof(StartOfRoundPatches)); Log.LogInfo((object)("SneakyMasks v1.6.0 loaded. " + (Cfg.Enabled.Value ? ($"Masks go active {Cfg.MinDelay.Value:0.##}-{Cfg.MaxDelay.Value:0.##}s after " + $"reaching the hand, then stay harmless for {Cfg.ProtectionTime.Value:0.##}s.") : "Disabled in the config — masks are left to the game."))); } internal static void DebugLog(string message) { if (Cfg != null && Cfg.DebugMode.Value) { Log.LogInfo((object)("[Debug] " + message)); } } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal sealed class PluginConfig { public const string ClimbSection = "Climb Speed"; public readonly ConfigEntry<bool> Enabled; public readonly ConfigEntry<float> MinDelay; public readonly ConfigEntry<float> MaxDelay; public readonly ConfigEntry<float> ProtectionTime; public readonly ConfigEntry<bool> PreventDropping; public readonly ConfigEntry<bool> DebugMode; public readonly ConfigEntry<bool> TakeoverEnabled; public readonly ConfigEntry<float> TakeoverMinDelay; public readonly ConfigEntry<float> TakeoverMaxDelay; public readonly ConfigEntry<float> TakeoverClimbChance; public readonly ConfigEntry<bool> EscalationEnabled; public readonly ConfigEntry<float> EscalationPerTakeover; public readonly ConfigEntry<float> EscalationMaxSpeed; private readonly ConfigFile _file; private readonly Dictionary<string, ConfigEntry<float>> _byMask = new Dictionary<string, ConfigEntry<float>>(); public PluginConfig(ConfigFile file) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Expected O, but got Unknown //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Expected O, but got Unknown //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Expected O, but got Unknown //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Expected O, but got Unknown //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_022e: Expected O, but got Unknown //IL_025d: Unknown result type (might be due to invalid IL or missing references) //IL_0267: Expected O, but got Unknown _file = file; Enabled = file.Bind<bool>("General", "Enabled", true, "Switches the whole mod off without uninstalling it.\n\n true masks put themselves on, as described below\n false masks go back to only working while you hold the use key, exactly as if\n the mod were not installed. Nothing else in this file has any effect."); MinDelay = file.Bind<float>("General", "MinDelay", 1f, new ConfigDescription("The shortest a mask will wait, in seconds, between reaching your hand and\nlifting itself to your face. Nothing is shown or heard during this wait, so\nthe only thing that tells you a mask is in your hand is that you picked it up.\n\n 0 masks go to the face the instant you take them out\n 1 the default; barely enough time to notice what you grabbed\n 10 masks are safe to carry briefly, and only bite if you keep them out\n\nThe actual wait is a fresh random number between this and MaxDelay, drawn\neach time the mask comes back into the hand, so it is never the same twice.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 120f), Array.Empty<object>())); MaxDelay = file.Bind<float>("General", "MaxDelay", 5f, new ConfigDescription("The longest that same wait can be, in seconds. Set this equal to MinDelay for\na fixed, learnable delay; leave a gap for a mask you cannot time.\n\nIf this ends up below MinDelay the mod uses MinDelay for both, so a bad pair\nof numbers will not break anything.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 120f), Array.Empty<object>())); ProtectionTime = file.Bind<float>("General", "ProtectionTime", 2f, new ConfigDescription("How long the mask is harmless, in seconds, once it has reached your face.\nDuring this window the game's roll to grab you is not allowed to happen at\nall, so putting the mask away here is always safe.\n\n 2 the default; a short, deliberate window to react in\n 10 what the base game gives you, which is where this number comes from\n 0 no grace at all, and a mask can start climbing the moment it goes on\n\nNote this replaces the base game's own 10 seconds rather than adding to it, so\nanything under 10 makes masks more dangerous than vanilla, not less. After the\nwindow closes the game takes over untouched: a 65% roll every 5 seconds, and\non a hit the mask starts climbing your face.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 120f), Array.Empty<object>())); PreventDropping = file.Bind<bool>("General", "PreventDropping", false, "Whether the mask holding the use key locks your hands the way holding it yourself\nwould.\n\nThe base game stops you dropping an item or switching slots while the use key is\ndown. That is fair when it is your finger on the button, because you can let go —\nbut a mask never will.\n\n false the lock is lifted, so you can always put the mask away or drop it. This\n is what makes the reaction window mean anything, and it is the default\n true the lock stands. Once a mask takes the key you cannot drop it, cannot\n switch slots, and the only thing left is whether the climb reaches your\n face. Picking up a mask becomes very close to a death sentence\n\nNothing else changes between the two: the delay, the protection window and the\nrolls are the same either way."); DebugMode = file.Bind<bool>("General", "DebugMode", false, "How much the mod writes to the BepInEx console.\n\n false nothing beyond the one line saying the mod loaded\n true logs the delay rolled for each pickup, the moment a mask takes the use\n key, every kill refused for a mask that was not fully on, and the one\n that goes through. Turn this on if a mask is not behaving as you set it."); TakeoverEnabled = file.Bind<bool>("Takeover", "Enabled", true, "Whether a mask left in a pocket can pull itself back into your hand.\n\n true every so often a pocketed mask switches your held item to itself, as if\n you had scrolled to it. From there it behaves as any mask in the hand:\n the hidden delay, the key, the climb. Carrying one becomes something you\n have to keep half an eye on\n false a mask in a pocket stays in the pocket, and only what you take out\n yourself can hurt you"); TakeoverMinDelay = file.Bind<float>("Takeover", "MinDelay", 20f, new ConfigDescription("The shortest a pocketed mask will wait, in seconds, before trying to come out.\nThe wait is rolled fresh each time the mask goes into a pocket, between this and\nMaxDelay, so you cannot learn it.\n\n 5 barely time to switch to something else before it is back\n 20 the default\n 120 a mask you can carry for a couple of minutes before it stirs\n\nA two-handed item in the hands is no protection. If the mask's moment comes\nwhile you are carrying one, the heavy thing is knocked to the floor first —\nthe same drop as pressing the drop key — and the mask takes the hand after.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 600f), Array.Empty<object>())); TakeoverMaxDelay = file.Bind<float>("Takeover", "MaxDelay", 90f, new ConfigDescription("The longest that wait can be, in seconds. Set equal to MinDelay for a fixed,\npredictable interval. Below MinDelay, MinDelay is used for both.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 600f), Array.Empty<object>())); TakeoverClimbChance = file.Bind<float>("Takeover", "ClimbAtOnceChance", 50f, new ConfigDescription("Out of 100, how often a mask that has just pulled itself into your hand is\nalready climbing your face as it arrives — skipping the hidden delay, the\nprotection window and the rolls entirely.\n\n 0 never; a mask that comes out on its own behaves like one you took out\n 50 a coin flip, the default. Half the time you have the usual run-up to\n notice it, half the time all you have is the climb itself\n 100 every takeover is an attack, and the climb speed is your whole warning\n\nThe climb can still be cancelled the usual way, by dropping or stowing the mask\nbefore it seats. Only the run-up is skipped, not the way out.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); EscalationEnabled = file.Bind<bool>("Escalation", "Enabled", true, "Whether a mask gets faster the longer you keep hold of it.\n\n true every time the mask has had to pull itself out of your pocket, the next\n climb is quicker than the last. Keep one long enough and the climb is over\n before you have seen it start\n false every climb is the same speed, however long you have carried the mask\n\nOnly throwing the mask on the floor puts it back to the start. Stowing it does not:\nas far as the mask is concerned it is still yours, and it is still counting."); EscalationPerTakeover = file.Bind<float>("Escalation", "SpeedPerTakeover", 0.5f, new ConfigDescription("How much faster each climb gets, as a share of the mask's base speed, for every\ntime it has had to come out of the pocket by itself.\n\n 0.25 a slow build; it takes a long carry before it bites\n 0.5 the default. Second takeover 1.5x the base, third 2x, fifth 3x\n 1 doubles on the first takeover, and gets out of hand quickly\n\nCounted per mask, and only takeovers count — taking the mask out yourself does\nnot raise it, though it does not lower it either.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>())); EscalationMaxSpeed = file.Bind<float>("Escalation", "MaxSpeed", 8f, new ConfigDescription("The ceiling on all of this: the fastest any climb may ever play, as a multiplier\non the animation the game ships with.\n\n 2 a mask that gets dangerous but stays watchable\n 8 the default, and fast enough to read as instant\n\nThe kill still happens at the top of the climb, never before it — a climb this\nfast is simply over almost as soon as it starts.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 20f), Array.Empty<object>())); } public bool RollClimbAtOnce() { return Random.Range(0f, 100f) < TakeoverClimbChance.Value; } public float RollTakeoverDelay() { float num = Mathf.Max(1f, TakeoverMinDelay.Value); float num2 = Mathf.Max(num, TakeoverMaxDelay.Value); return Random.Range(num, num2); } public float RollDelay() { float num = Mathf.Max(0f, MinDelay.Value); float num2 = Mathf.Max(num, MaxDelay.Value); return Random.Range(num, num2); } public void BindAllMasks(AllItemsList items) { if (items?.itemsList == null) { return; } foreach (Item items2 in items.itemsList) { if (!((Object)(object)items2?.spawnPrefab == (Object)null)) { HauntedMaskItem component = items2.spawnPrefab.GetComponent<HauntedMaskItem>(); if ((Object)(object)component != (Object)null && component.maskIsHaunted) { BindMask(items2.itemName); } } } _file.Save(); } public ConfigEntry<float> BindMask(string maskName) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown if (string.IsNullOrEmpty(maskName)) { return null; } if (_byMask.TryGetValue(maskName, out var value)) { return value; } ConfigEntry<float> val = _file.Bind<float>("Climb Speed", maskName, DefaultClimbSpeedFor(maskName), new ConfigDescription(MaskDescription(maskName), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 10f), Array.Empty<object>())); _byMask[maskName] = val; return val; } public float ClimbSpeedFor(string maskName) { ConfigEntry<float> val = BindMask(maskName); if (val != null) { return Mathf.Max(0.1f, val.Value); } return 1f; } public float ClimbSpeedFor(string maskName, int escalationStep) { float num = ClimbSpeedFor(maskName); if (!EscalationEnabled.Value || escalationStep <= 0) { return num; } num *= 1f + (float)escalationStep * Mathf.Max(0f, EscalationPerTakeover.Value); return Mathf.Min(num, Mathf.Max(1f, EscalationMaxSpeed.Value)); } private static float DefaultClimbSpeedFor(string maskName) { string text = maskName.ToLowerInvariant(); if (text.Contains("tragedy")) { return 1.6f; } if (text.Contains("comedy")) { return 0.7f; } return 1f; } private static string MaskDescription(string maskName) { return "How fast " + maskName + " climbs onto your face once it has started, as a multiplier\non the animation the game ships with. Since the mask only kills at the top of\nthat climb, this is the same thing as how long you have to get it off.\n\n 0.5 half speed, twice as long to react\n 1 the speed the base game plays it at\n 2 twice as fast, and about half the reaction time\n\nThe base game plays both masks at the same speed. The defaults here do not:\nTragedy is quicker than vanilla and Comedy is slower, so which one you picked\nup is worth knowing. Set both to 1 to put that back the way the game has it.\n\nTurn on DebugMode to have the console print how long each climb actually took,\nin seconds, which is the easy way to tune this."; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "Timofey.SneakyMasks"; public const string PLUGIN_NAME = "SneakyMasks"; public const string PLUGIN_VERSION = "1.6.0"; } } namespace SneakyMasks.Patches { [HarmonyPatch(typeof(HauntedMaskItem))] internal static class MaskPatches { [HarmonyPostfix] [HarmonyPatch("EquipItem")] private static void EquipItem_Postfix(HauntedMaskItem __instance) { PluginConfig cfg = Plugin.Cfg; if (cfg != null && cfg.Enabled.Value && __instance.maskIsHaunted && ((NetworkBehaviour)__instance).IsOwner) { float num = cfg.RollDelay(); MaskProcess.For(__instance).Restart(num); Plugin.DebugLog($"Mask picked up (type {__instance.maskTypeId}); pressing use in {num:0.00}s."); } } [HarmonyPostfix] [HarmonyPatch("Update")] private static void Update_Postfix(HauntedMaskItem __instance) { PluginConfig cfg = Plugin.Cfg; if (cfg == null || !cfg.Enabled.Value || !__instance.maskIsHaunted || !((NetworkBehaviour)__instance).IsOwner) { return; } if (((GrabbableObject)__instance).isPocketed) { TryTakeover(__instance, cfg); } else { if ((Object)(object)__instance.previousPlayerHeldBy == (Object)null || !__instance.holdingLastFrame || __instance.finishedAttaching || __instance.attaching) { return; } MaskProcess maskProcess = MaskProcess.For(__instance); if (__instance.maskOn) { if (!maskProcess.AutoStarted) { maskProcess.Hold(); } else { FreePlayerHands(__instance, cfg); } } else if (!RoundIsLive()) { maskProcess.Hold(); } else if (maskProcess.ClimbImmediately) { maskProcess.ClimbImmediately = false; maskProcess.AutoStarted = true; HoldUseKey(__instance, cfg); __instance.BeginAttachment(); Plugin.DebugLog($"Mask (type {__instance.maskTypeId}) began climbing on arrival."); } else if (maskProcess.AutoStarted || !(Time.realtimeSinceStartup < maskProcess.StartAt)) { bool num = !maskProcess.AutoStarted; maskProcess.AutoStarted = true; HoldUseKey(__instance, cfg); if (num) { __instance.lastIntervalCheck = Time.realtimeSinceStartup + cfg.ProtectionTime.Value; Plugin.DebugLog($"Mask (type {__instance.maskTypeId}) took the use key; " + $"still for {cfg.ProtectionTime.Value:0.00}s, then 65% every 5s to start climbing."); } } } } [HarmonyPostfix] [HarmonyPatch("AttachToPlayerOnLocalClient")] private static void AttachToPlayerOnLocalClient_Postfix(HauntedMaskItem __instance) { PluginConfig cfg = Plugin.Cfg; if (cfg != null && cfg.Enabled.Value) { if ((Object)(object)__instance.maskAnimator == (Object)null) { Plugin.DebugLog("Mask has no maskAnimator; climb speed cannot be applied."); return; } MaskProcess maskProcess = MaskProcess.For(__instance); string text = (((Object)(object)((GrabbableObject)__instance).itemProperties != (Object)null) ? ((GrabbableObject)__instance).itemProperties.itemName : null); float num = cfg.ClimbSpeedFor(text, maskProcess.EscalationStep); __instance.maskAnimator.speed = num; float attachTimer = __instance.attachTimer; __instance.attachTimer = attachTimer / num; maskProcess.AttachStartedAt = Time.realtimeSinceStartup; Plugin.DebugLog($"{text} attaching at {num:0.00}x " + $"(base {cfg.ClimbSpeedFor(text):0.00}x, escalation step {maskProcess.EscalationStep}); " + $"suffocation {attachTimer:0.00}s -> {__instance.attachTimer:0.00}s."); } } [HarmonyPostfix] [HarmonyPatch("CancelAttachToPlayerOnLocalClient")] private static void CancelAttachToPlayerOnLocalClient_Postfix(HauntedMaskItem __instance) { if ((Object)(object)__instance.maskAnimator != (Object)null) { __instance.maskAnimator.speed = 1f; } } [HarmonyPostfix] [HarmonyPatch("MaskClampToHeadAnimationEvent")] private static void MaskClampToHeadAnimationEvent_Postfix(HauntedMaskItem __instance) { PluginConfig cfg = Plugin.Cfg; if (cfg != null && cfg.Enabled.Value && __instance.clampedToHead) { float num = Time.realtimeSinceStartup - MaskProcess.For(__instance).AttachStartedAt; Plugin.DebugLog($"Mask latched onto the face after {num:0.00}s; " + $"{__instance.attachTimer:0.00}s of suffocation left before it kills."); } } [HarmonyPrefix] [HarmonyPatch("FinishAttaching")] private static bool FinishAttaching_Prefix(HauntedMaskItem __instance) { PluginConfig cfg = Plugin.Cfg; if (cfg == null || !cfg.Enabled.Value) { return true; } if (__instance.clampedToHead) { return true; } Plugin.DebugLog("A mask that is not seated tried to kill; refused."); return false; } [HarmonyPostfix] [HarmonyPatch("PocketItem")] private static void PocketItem_Postfix(HauntedMaskItem __instance) { MaskProcess maskProcess = MaskProcess.For(__instance); PluginConfig cfg = Plugin.Cfg; if (cfg == null || !cfg.Enabled.Value || !cfg.TakeoverEnabled.Value || !__instance.maskIsHaunted || !((NetworkBehaviour)__instance).IsOwner || !IsLocalPlayer(((GrabbableObject)__instance).playerHeldBy)) { maskProcess.Clear(); return; } float num = cfg.RollTakeoverDelay(); maskProcess.Pocket(num); Plugin.DebugLog($"Mask (type {__instance.maskTypeId}) pocketed; will try to come out in {num:0.0}s."); } [HarmonyPostfix] [HarmonyPatch("DiscardItem")] private static void DiscardItem_Postfix(HauntedMaskItem __instance) { MaskProcess.For(__instance).Clear(); } private static void TryTakeover(HauntedMaskItem mask, PluginConfig cfg) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!cfg.TakeoverEnabled.Value) { return; } MaskProcess maskProcess = MaskProcess.For(mask); if (maskProcess.TakeoverAt == float.MaxValue) { return; } PlayerControllerB playerHeldBy = ((GrabbableObject)mask).playerHeldBy; if (!IsLocalPlayer(playerHeldBy) || playerHeldBy.isPlayerDead) { return; } if (!RoundIsLive()) { maskProcess.HoldTakeover(); } else { if (Time.realtimeSinceStartup < maskProcess.TakeoverAt) { return; } if (playerHeldBy.twoHanded) { if (CanDrop(playerHeldBy)) { playerHeldBy.DiscardHeldObject(false, (NetworkObject)null, default(Vector3), true); Plugin.DebugLog("Mask knocked the two-handed item out of the hand; taking the hand next."); } } else { if (!CanSwitchSlots(playerHeldBy)) { return; } int num = Array.IndexOf(playerHeldBy.ItemSlots, (GrabbableObject)(object)mask); if (num < 0 || num == playerHeldBy.currentItemSlot) { maskProcess.TakeoverAt = float.MaxValue; return; } ShipBuildModeManager.Instance.CancelBuildMode(true); playerHeldBy.playerBodyAnimator.SetBool("GrabValidated", false); playerHeldBy.SwitchToItemSlot(num, (GrabbableObject)null); playerHeldBy.SwitchToSlotServerRpc(num); if ((Object)(object)playerHeldBy.currentlyHeldObjectServer != (Object)null) { ((Component)playerHeldBy.currentlyHeldObjectServer).gameObject.GetComponent<AudioSource>().PlayOneShot(playerHeldBy.currentlyHeldObjectServer.itemProperties.grabSFX, 0.6f); } playerHeldBy.timeSinceSwitchingSlots = 0f; maskProcess.EscalationStep++; Plugin.DebugLog($"Mask (type {mask.maskTypeId}) came out of slot {num} on its own " + $"(takeover #{maskProcess.EscalationStep})."); if (cfg.RollClimbAtOnce()) { maskProcess.ClimbImmediately = true; Plugin.DebugLog("...and drew the flip to climb on arrival."); } } } } private static bool CanDrop(PlayerControllerB player) { if (!player.isPlayerControlled || player.inTerminalMenu) { return false; } if (player.timeSinceSwitchingSlots < 0.2f || player.isGrabbingObjectAnimation || ((Object)(object)player.quickMenuManager != (Object)null && player.quickMenuManager.isMenuOpen) || player.inSpecialInteractAnimation || player.isTypingChat || player.activatingItem || player.throwingObject || !player.isHoldingObject || (Object)(object)player.currentlyHeldObjectServer == (Object)null) { return false; } if ((player.jetpackControls || player.disablingJetpackControls) && player.currentlyHeldObjectServer.itemProperties.itemId == 13) { return false; } return true; } private static bool CanSwitchSlots(PlayerControllerB player) { if (!player.isPlayerControlled || player.inTerminalMenu) { return false; } if (player.timeSinceSwitchingSlots < 0.3f || player.isGrabbingObjectAnimation || ((Object)(object)player.quickMenuManager != (Object)null && player.quickMenuManager.isMenuOpen) || player.inSpecialInteractAnimation || player.throwingObject || player.isTypingChat || player.activatingItem) { return false; } if ((player.jetpackControls || player.disablingJetpackControls) && (Object)(object)player.currentlyHeldObjectServer != (Object)null && player.currentlyHeldObjectServer.itemProperties.itemId == 13) { return false; } return true; } private static bool IsLocalPlayer(PlayerControllerB player) { if ((Object)(object)player != (Object)null && (Object)(object)GameNetworkManager.Instance != (Object)null) { return (Object)(object)player == (Object)(object)GameNetworkManager.Instance.localPlayerController; } return false; } private static void HoldUseKey(HauntedMaskItem mask, PluginConfig cfg) { ((GrabbableObject)mask).UseItemOnClient(true); if (!mask.maskOn) { mask.maskOn = true; } FreePlayerHands(mask, cfg); } private static void FreePlayerHands(HauntedMaskItem mask, PluginConfig cfg) { if (!cfg.PreventDropping.Value) { PlayerControllerB previousPlayerHeldBy = mask.previousPlayerHeldBy; if ((Object)(object)previousPlayerHeldBy != (Object)null && previousPlayerHeldBy.activatingItem) { previousPlayerHeldBy.activatingItem = false; } } } private static bool RoundIsLive() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } if (!instance.shipIsLeaving) { if (instance.inShipPhase) { return (Object)(object)instance.testRoom != (Object)null; } return true; } return false; } } [HarmonyPatch(typeof(StartOfRound))] internal static class StartOfRoundPatches { [HarmonyPostfix] [HarmonyPatch("Start")] private static void Start_Postfix(StartOfRound __instance) { if (Plugin.Cfg != null) { Plugin.Cfg.BindAllMasks(__instance.allItemsList); } } } }