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.2.0
SneakyMasks.dll
Decompiled 3 hours 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.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: AssemblyProduct("SneakyMasks")] [assembly: AssemblyTitle("SneakyMasks")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.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 AttachStartedAt; public static MaskProcess For(HauntedMaskItem mask) { return States.GetOrCreateValue(mask); } public void Restart(float delay) { Delay = delay; StartAt = Time.realtimeSinceStartup + delay; AutoStarted = false; } public void Hold() { StartAt = Time.realtimeSinceStartup + Delay; } public void Clear() { Delay = 0f; StartAt = float.MaxValue; AutoStarted = false; } } [BepInPlugin("Timofey.SneakyMasks", "SneakyMasks", "1.2.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.2.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; 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 _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."); } 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; } 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.2.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 || (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.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 && !((Object)(object)__instance.maskAnimator == (Object)null)) { string text = (((Object)(object)((GrabbableObject)__instance).itemProperties != (Object)null) ? ((GrabbableObject)__instance).itemProperties.itemName : null); float num = cfg.ClimbSpeedFor(text); __instance.maskAnimator.speed = num; MaskProcess.For(__instance).AttachStartedAt = Time.realtimeSinceStartup; Plugin.DebugLog($"{text} started climbing at {num:0.00}x speed."); } } [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 reached the end of its climb after {num:0.00}s; attacking."); __instance.FinishAttaching(); } } [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.For(__instance).Clear(); } [HarmonyPostfix] [HarmonyPatch("DiscardItem")] private static void DiscardItem_Postfix(HauntedMaskItem __instance) { MaskProcess.For(__instance).Clear(); } 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); } } } }