using System; using System.Diagnostics; using System.IO; 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 HarmonyLib; using Microsoft.CodeAnalysis; using Pigeon.Movement; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Interactions; [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("Sparroh")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: AssemblyProduct("GrenadeMacros")] [assembly: AssemblyTitle("GrenadeMacros")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.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; } } } public static class ConfigManager { public enum ActivationMode { None, Hold, Toggle } private const float DebounceSeconds = 0.25f; private static ConfigFile config; private static ManualLogSource logger; private static FileSystemWatcher configWatcher; private static volatile bool reloadPending; private static float lastReloadTime; public static ConfigEntry IncendiaryMode { get; private set; } public static ConfigEntry VoltaicMode { get; private set; } public static ConfigEntry AcidMode { get; private set; } public static ConfigEntry DebugThrowInput { get; private set; } public static bool DebugEnabled { get { if (DebugThrowInput != null) { return DebugThrowInput.Value; } return false; } } public static void Initialize(ConfigFile configFile, ManualLogSource log) { config = configFile; logger = log; IncendiaryMode = config.Bind("General", "Incendiary Mode", ActivationMode.None, "Activation mode for incendiary grenades. Hold: prevent auto-activation, Toggle: toggle on/off, None: default"); VoltaicMode = config.Bind("General", "Shock Mode", ActivationMode.Toggle, "Activation mode for voltaic (shock) grenades. Hold: prevent auto-activation, Toggle: toggle on/off, None: default"); AcidMode = config.Bind("General", "Acid Mode", ActivationMode.None, "Activation mode for acid grenades. Hold: prevent auto-activation, Toggle: toggle on/off, None: default"); DebugThrowInput = config.Bind("Debug", "Debug Throw Input", true, "Log throw input / toggle decisions to BepInEx log (Info). Turn off when done debugging."); IncendiaryMode.SettingChanged += OnSettingChanged; VoltaicMode.SettingChanged += OnSettingChanged; AcidMode.SettingChanged += OnSettingChanged; try { SetupFileWatcher(); } catch (Exception ex) { logger.LogError((object)("Error setting up config file watcher: " + ex.Message)); } } public static void Tick() { if (!reloadPending || Time.unscaledTime - lastReloadTime < 0.25f) { return; } reloadPending = false; lastReloadTime = Time.unscaledTime; try { config.Reload(); ThrowablePatches.ResetToggles(); logger.LogInfo((object)"Config reloaded from disk."); } catch (Exception ex) { logger.LogError((object)("Error reloading config: " + ex.Message)); } } public static void Dispose() { if (IncendiaryMode != null) { IncendiaryMode.SettingChanged -= OnSettingChanged; } if (VoltaicMode != null) { VoltaicMode.SettingChanged -= OnSettingChanged; } if (AcidMode != null) { AcidMode.SettingChanged -= OnSettingChanged; } if (configWatcher != null) { configWatcher.EnableRaisingEvents = false; configWatcher.Changed -= OnConfigFileChanged; configWatcher.Created -= OnConfigFileChanged; configWatcher.Renamed -= OnConfigFileChanged; configWatcher.Dispose(); configWatcher = null; } } private static void SetupFileWatcher() { configWatcher = new FileSystemWatcher(Paths.ConfigPath, "sparroh.grenademacros.cfg"); configWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite; configWatcher.Changed += OnConfigFileChanged; configWatcher.Created += OnConfigFileChanged; configWatcher.Renamed += OnConfigFileChanged; configWatcher.EnableRaisingEvents = true; } private static void OnConfigFileChanged(object sender, FileSystemEventArgs e) { reloadPending = true; } private static void OnSettingChanged(object sender, EventArgs e) { ThrowablePatches.ResetToggles(); } } [BepInPlugin("sparroh.grenademacros", "GrenadeMacros", "1.1.0")] [MycoMod(/*Could not decode attribute arguments.*/)] public class GrenadeMacrosPlugin : BaseUnityPlugin { public const string PluginGUID = "sparroh.grenademacros"; public const string PluginName = "GrenadeMacros"; public const string PluginVersion = "1.1.0"; internal static ManualLogSource Logger; private Harmony harmony; private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown Logger = ((BaseUnityPlugin)this).Logger; ConfigManager.Initialize(((BaseUnityPlugin)this).Config, Logger); harmony = new Harmony("sparroh.grenademacros"); try { harmony.PatchAll(typeof(ThrowablePatches)); Logger.LogInfo((object)"Harmony patches applied."); } catch (Exception ex) { Logger.LogError((object)("Error applying patches: " + ex.Message)); } Logger.LogInfo((object)"GrenadeMacros v1.1.0 loaded successfully."); } private void Update() { ConfigManager.Tick(); } private void OnDestroy() { ConfigManager.Dispose(); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } } public static class ThrowablePatches { public static bool incendiaryToggle; public static bool voltaicToggle; public static bool acidToggle; private static bool isAutoThrow; private static bool togglePressPending; private static float togglePressStartTime; private static Player togglePressPlayer; private static object togglePressEquipped; private const float ToggleTapMaxDuration = 0.35f; private static readonly FieldInfo CooldownDataField = AccessTools.Field(typeof(Throwable), "cooldownData"); private static readonly FieldInfo PlayerField = AccessTools.Field(typeof(Throwable), "player"); private static readonly MethodInfo TryThrowMethod = AccessTools.Method(typeof(Player), "TryThrow", (Type[])null, (Type[])null); private static readonly FieldInfo OnThrowHeldField = AccessTools.Field(typeof(Player), "OnThrowHeld"); public static void ResetToggles() { incendiaryToggle = false; voltaicToggle = false; acidToggle = false; ClearTogglePress("ResetToggles"); } private static void LogDebug(string message) { if (ConfigManager.DebugEnabled) { GrenadeMacrosPlugin.Logger.LogInfo((object)("[GrenadeMacros] " + message)); } } private static string InteractionName(CallbackContext context) { if (((CallbackContext)(ref context)).interaction == null) { return "null"; } if (((CallbackContext)(ref context)).interaction is TapInteraction) { return "Tap"; } if (((CallbackContext)(ref context)).interaction is HoldInteraction) { return "Hold"; } if (((CallbackContext)(ref context)).interaction is PressInteraction) { return "Press"; } return ((object)((CallbackContext)(ref context)).interaction).GetType().Name; } private static string PhaseName(CallbackContext context) { if (((CallbackContext)(ref context)).started) { return "Started"; } if (((CallbackContext)(ref context)).performed) { return "Performed"; } if (((CallbackContext)(ref context)).canceled) { return "Canceled"; } return "Unknown"; } private static string ToggleSummary() { return $"I={incendiaryToggle} V={voltaicToggle} A={acidToggle}"; } private static void ClearTogglePress(string reason) { if (togglePressPending) { LogDebug($"ClearPending reason={reason} age={Time.unscaledTime - togglePressStartTime:F3}s"); } togglePressPending = false; togglePressPlayer = null; togglePressEquipped = null; togglePressStartTime = 0f; } private static bool TryGetGrenadeMode(object equippedThrow, out ConfigManager.ActivationMode mode) { mode = ConfigManager.ActivationMode.None; if (equippedThrow == null) { return false; } switch (equippedThrow.GetType().Name) { case "IncendiaryGrenade": mode = ConfigManager.IncendiaryMode.Value; return true; case "VoltaicGrenade": mode = ConfigManager.VoltaicMode.Value; return true; case "AcidGrenade": mode = ConfigManager.AcidMode.Value; return true; default: return false; } } private static bool TryGetToggleState(string typeName, out bool toggleState) { switch (typeName) { case "IncendiaryGrenade": toggleState = incendiaryToggle; return true; case "VoltaicGrenade": toggleState = voltaicToggle; return true; case "AcidGrenade": toggleState = acidToggle; return true; default: toggleState = false; return false; } } private static void FlipToggle(object equippedThrow, string reason) { if (equippedThrow == null) { LogDebug("FlipToggle skipped (equipped=null) reason=" + reason); return; } string name = equippedThrow.GetType().Name; switch (name) { case "IncendiaryGrenade": incendiaryToggle = !incendiaryToggle; LogDebug($"FlipToggle {name} -> {incendiaryToggle} reason={reason}"); break; case "VoltaicGrenade": voltaicToggle = !voltaicToggle; LogDebug($"FlipToggle {name} -> {voltaicToggle} reason={reason}"); break; case "AcidGrenade": acidToggle = !acidToggle; LogDebug($"FlipToggle {name} -> {acidToggle} reason={reason}"); break; default: LogDebug("FlipToggle unknown type=" + name + " reason=" + reason); break; } } private static IGear GetEquippedThrowable(Player player) { IGear[] gear = player.Gear; if (gear == null || gear.Length <= 3) { return null; } return gear[3]; } private static void InvokeTryThrow(Player player, string reason) { if (TryThrowMethod == null) { LogDebug("InvokeTryThrow aborted (method null) reason=" + reason); return; } LogDebug("InvokeTryThrow reason=" + reason); isAutoThrow = true; try { TryThrowMethod.Invoke(player, null); } finally { isAutoThrow = false; } } private static void AutoThrow(Player player, Throwable throwable, string reason) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) if (!(CooldownDataField == null) && !(TryThrowMethod == null)) { CooldownData val = (CooldownData)CooldownDataField.GetValue(throwable); if (!((CooldownData)(ref val)).IsCharged) { LogDebug("AutoThrow skip not-charged type=" + ((object)throwable).GetType().Name + " reason=" + reason); return; } LogDebug($"AutoThrow type={((object)throwable).GetType().Name} charge={val.charge:F2} reason={reason}"); InvokeTryThrow(player, reason); } } private static bool HasOnThrowHeldSubscriber(Player player) { if (OnThrowHeldField == null) { return false; } return OnThrowHeldField.GetValue(player) != null; } private static void BeginTogglePress(Player player, object equipped) { togglePressPending = true; togglePressStartTime = Time.unscaledTime; togglePressPlayer = player; togglePressEquipped = equipped; LogDebug("BeginPending equipped=" + (equipped?.GetType().Name ?? "null") + " " + $"threshold={0.35f:F2}s {ToggleSummary()}"); } private static void CompleteToggleTap(string reason) { if (!togglePressPending) { LogDebug("CompleteToggleTap skipped (no pending) reason=" + reason); return; } object equippedThrow = togglePressEquipped; float num = Time.unscaledTime - togglePressStartTime; ClearTogglePress("complete:" + reason); LogDebug($"CompleteToggleTap age={num:F3}s reason={reason}"); FlipToggle(equippedThrow, reason); } private static void ResolveTogglePressOnRelease(Player player) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) if (!togglePressPending || (Object)(object)togglePressPlayer != (Object)(object)player) { return; } PlayerActions player2 = PlayerInput.Controls.Player; bool num = ((PlayerActions)(ref player2)).Throw.IsPressed(); float num2 = Time.unscaledTime - togglePressStartTime; if (!num) { if (num2 <= 0.35f) { LogDebug($"PendingRelease FLIP age={num2:F3}s isPressed=false"); CompleteToggleTap("release-before-hold-threshold"); } else { LogDebug($"PendingRelease CLEAR (too long) age={num2:F3}s threshold={0.35f:F2}s"); ClearTogglePress("release-after-threshold"); } } } [HarmonyPatch(typeof(Player), "OnThrowPressed")] [HarmonyPrefix] private static bool OnThrowPressedPrefix(Player __instance, CallbackContext context) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) try { if (!((NetworkBehaviour)__instance).IsOwner) { LogDebug("OnThrowPressed skip not-owner"); return true; } if (isAutoThrow) { LogDebug("OnThrowPressed skip isAutoThrow"); return true; } IGear equippedThrowable = GetEquippedThrowable(__instance); ConfigManager.ActivationMode mode; bool flag = TryGetGrenadeMode(equippedThrowable, out mode); PlayerActions player = PlayerInput.Controls.Player; bool flag2 = ((PlayerActions)(ref player)).Throw.IsPressed(); float num = (togglePressPending ? (Time.unscaledTime - togglePressStartTime) : 0f); LogDebug($"OnThrowPressed phase={PhaseName(context)} performed={((CallbackContext)(ref context)).performed} canceled={((CallbackContext)(ref context)).canceled} " + $"started={((CallbackContext)(ref context)).started} interaction={InteractionName(context)} duration={((CallbackContext)(ref context)).duration:F3} " + string.Format("throwIsPressed={0} equipped={1} ", flag2, ((object)equippedThrowable)?.GetType().Name ?? "null") + $"hasMode={flag} mode={mode} pending={togglePressPending} pendingAge={num:F3} " + $"onThrowHeld={HasOnThrowHeldSubscriber(__instance)} {ToggleSummary()}"); if (flag) { switch (mode) { case ConfigManager.ActivationMode.None: break; case ConfigManager.ActivationMode.Hold: ClearTogglePress("enter-hold-mode-input"); LogDebug("decision=hold-mode-block"); return false; default: LogDebug("decision=pass-vanilla (not toggle)"); return true; case ConfigManager.ActivationMode.Toggle: switch ((((CallbackContext)(ref context)).interaction is TapInteraction) ? 1 : (((CallbackContext)(ref context)).performed ? 2 : 0)) { case 1: if (!((CallbackContext)(ref context)).performed) { ClearTogglePress("tap-started"); LogDebug("decision=ignore (Tap started, wait for performed)"); return false; } ClearTogglePress("tap-performed"); FlipToggle(equippedThrowable, "tap-performed"); LogDebug("decision=flip-toggle (Tap performed)"); return false; case 2: ClearTogglePress("hold-performed"); if (HasOnThrowHeldSubscriber(__instance)) { LogDebug("decision=pass-OnThrowHeld"); return true; } InvokeTryThrow(__instance, "hold-manual"); LogDebug("decision=manual-throw (Hold performed)"); return false; default: ClearTogglePress("non-tap-started"); LogDebug("decision=block-started (kind=0)"); return false; } } } LogDebug("decision=pass-vanilla (mode none/unknown)"); return true; } catch (Exception ex) { GrenadeMacrosPlugin.Logger.LogError((object)("Error in OnThrowPressedPrefix: " + ex.Message)); return true; } } [HarmonyPatch(typeof(Player), "TryThrow")] [HarmonyPrefix] private static bool TryThrowPrefix(Player __instance) { try { if (isAutoThrow) { LogDebug("TryThrow allow isAutoThrow"); return true; } IGear equippedThrowable = GetEquippedThrowable(__instance); if (!TryGetGrenadeMode(equippedThrowable, out var mode)) { LogDebug("TryThrow allow unknown-equipped=" + (((object)equippedThrowable)?.GetType().Name ?? "null")); return true; } if (mode == ConfigManager.ActivationMode.Toggle || mode == ConfigManager.ActivationMode.Hold) { LogDebug($"TryThrow BLOCK mode={mode} equipped={((object)equippedThrowable)?.GetType().Name} {ToggleSummary()}"); return false; } LogDebug($"TryThrow allow mode={mode}"); return true; } catch (Exception ex) { GrenadeMacrosPlugin.Logger.LogError((object)("Error in TryThrowPrefix: " + ex.Message)); return true; } } [HarmonyPatch(typeof(Player), "Update")] [HarmonyPostfix] private static void PlayerUpdatePostfix(Player __instance) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { if (!((NetworkBehaviour)__instance).IsOwner || isAutoThrow) { return; } if (togglePressPending && (Object)(object)togglePressPlayer == (Object)(object)__instance) { ResolveTogglePressOnRelease(__instance); } PlayerActions player = PlayerInput.Controls.Player; if (!((PlayerActions)(ref player)).Throw.IsPressed()) { return; } IGear equippedThrowable = GetEquippedThrowable(__instance); if (TryGetGrenadeMode(equippedThrowable, out var mode) && mode == ConfigManager.ActivationMode.Hold) { Throwable val = (Throwable)(object)((equippedThrowable is Throwable) ? equippedThrowable : null); if (val != null) { AutoThrow(__instance, val, "hold-mode-update"); } } } catch (Exception ex) { GrenadeMacrosPlugin.Logger.LogError((object)("Error in PlayerUpdatePostfix: " + ex.Message)); } } [HarmonyPatch(typeof(Throwable), "HandleCooldown")] [HarmonyPrefix] private static bool HandleCooldownPrefix(Throwable __instance, float charge) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) try { string name = ((object)__instance).GetType().Name; if (!TryGetGrenadeMode(__instance, out var mode) || mode == ConfigManager.ActivationMode.None) { return true; } if (CooldownDataField == null) { return true; } switch (mode) { case ConfigManager.ActivationMode.Hold: return true; default: return true; case ConfigManager.ActivationMode.Toggle: { CooldownData val = (CooldownData)CooldownDataField.GetValue(__instance); if (!((CooldownData)(ref val)).IsCharged) { return true; } if (!TryGetToggleState(name, out var toggleState) || !toggleState) { return true; } if (PlayerField == null) { return true; } object? value = PlayerField.GetValue(__instance); Player val2 = (Player)((value is Player) ? value : null); if ((Object)(object)val2 == (Object)null || !((NetworkBehaviour)val2).IsOwner) { return true; } if ((object)GetEquippedThrowable(val2) != __instance) { return true; } AutoThrow(val2, __instance, "toggle-handle-cooldown"); return false; } } } catch (Exception ex) { GrenadeMacrosPlugin.Logger.LogError((object)("Error in HandleCooldownPrefix: " + ex.Message)); return true; } } } namespace GrenadeMacros { public static class MyPluginInfo { public const string PLUGIN_GUID = "GrenadeMacros"; public const string PLUGIN_NAME = "GrenadeMacros"; public const string PLUGIN_VERSION = "1.1.0"; } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }