using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("InstantItemSlots")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("InstantItemSlots")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("4cc5e582-3cd6-41fb-87fe-c92a18523560")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace InstantItemSlots; [BepInPlugin("kviks.instantitemslots", "Instant Item Slots", "15.04.2026")] public class InstantItemSlotsPlugin : BaseUnityPlugin { internal static ManualLogSource Log; internal static bool DebugEnabled; internal static bool ArrowEmotesOnly; internal static int ControlledSlots; internal static float CooldownSeconds; internal static float SpawnDelaySeconds; internal static bool IgnoreBusyDuringSpawnDelay; internal static bool IgnoreBusyChecksAlways; internal static bool PlayGrabSfx; internal static bool SyncStepsFallback; private static Harmony _harmony; private static ConfigEntry _debug; private static ConfigEntry _arrowEmotesOnly; private static ConfigEntry _controlledSlots; private static ConfigEntry _cooldownSeconds; private static ConfigEntry _spawnDelaySeconds; private static ConfigEntry _ignoreBusyDuringSpawnDelay; private static ConfigEntry _ignoreBusyChecksAlways; private static ConfigEntry _playGrabSfx; private static ConfigEntry _syncStepsFallback; private void Awake() { //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; _debug = ((BaseUnityPlugin)this).Config.Bind("General", "EnableDebugLogging", false, "Enables debug logging."); _arrowEmotesOnly = ((BaseUnityPlugin)this).Config.Bind("Emotes", "UseArrowEmotesOnly", true, "UpArrow = emote 1, DownArrow = emote 2. Vanilla emote input is blocked while enabled."); _controlledSlots = ((BaseUnityPlugin)this).Config.Bind("Hotbar", "DigitControlledSlots", 4, "How many slots are controlled by 1/2/3/4 (1..4)."); _cooldownSeconds = ((BaseUnityPlugin)this).Config.Bind("Hotbar", "DigitSwitchCooldownSeconds", 0f, "Minimum time since last switch allowed."); _spawnDelaySeconds = ((BaseUnityPlugin)this).Config.Bind("Spawn", "RelaxedRulesWindowSeconds", 0f, "Seconds after level load where switching rules can be relaxed."); _ignoreBusyDuringSpawnDelay = ((BaseUnityPlugin)this).Config.Bind("Spawn", "IgnoreBusyChecksInsideRelaxedWindow", true, "Ignore animation/interact/activatingItem/twoHanded during the relaxed window."); _ignoreBusyChecksAlways = ((BaseUnityPlugin)this).Config.Bind("Spawn", "IgnoreBusyChecksAlways", false, "Always ignore animation/interact/activatingItem/twoHanded checks."); _playGrabSfx = ((BaseUnityPlugin)this).Config.Bind("Audio", "PlayGrabSoundOnDigitSwitch", true, "Play grab sound when switching with digits."); _syncStepsFallback = ((BaseUnityPlugin)this).Config.Bind("Network", "UseLegacyStepRpcFallback", true, "Fallback sync for old versions without direct slot RPC."); ApplyConfig(); HookConfigChanges(); GameAccess.Init(); _harmony = new Harmony("kviks.instantitemslots"); _harmony.PatchAll(); if (DebugEnabled) { Log.LogInfo((object)"[IIS] Loaded."); } } private void ApplyConfig() { DebugEnabled = _debug.Value; ArrowEmotesOnly = _arrowEmotesOnly.Value; ControlledSlots = Mathf.Clamp(_controlledSlots.Value, 1, 4); CooldownSeconds = Mathf.Max(0f, _cooldownSeconds.Value); SpawnDelaySeconds = Mathf.Max(0f, _spawnDelaySeconds.Value); IgnoreBusyDuringSpawnDelay = _ignoreBusyDuringSpawnDelay.Value; IgnoreBusyChecksAlways = _ignoreBusyChecksAlways.Value; PlayGrabSfx = _playGrabSfx.Value; SyncStepsFallback = _syncStepsFallback.Value; } private void HookConfigChanges() { _debug.SettingChanged += delegate { ApplyConfig(); }; _arrowEmotesOnly.SettingChanged += delegate { ApplyConfig(); }; _controlledSlots.SettingChanged += delegate { ApplyConfig(); }; _cooldownSeconds.SettingChanged += delegate { ApplyConfig(); }; _spawnDelaySeconds.SettingChanged += delegate { ApplyConfig(); }; _ignoreBusyDuringSpawnDelay.SettingChanged += delegate { ApplyConfig(); }; _ignoreBusyChecksAlways.SettingChanged += delegate { ApplyConfig(); }; _playGrabSfx.SettingChanged += delegate { ApplyConfig(); }; _syncStepsFallback.SettingChanged += delegate { ApplyConfig(); }; } internal static void HandleLocalUpdate(PlayerControllerB player) { if ((Object)(object)player == (Object)null || !((NetworkBehaviour)player).IsOwner || (!player.isPlayerControlled && !player.isTestingPlayer)) { return; } Keyboard current = Keyboard.current; if (current == null) { return; } if (ArrowEmotesOnly) { if (current.upArrowKey != null && ((ButtonControl)current.upArrowKey).wasPressedThisFrame) { TryPerformArrowEmote(player, 1); return; } if (current.downArrowKey != null && ((ButtonControl)current.downArrowKey).wasPressedThisFrame) { TryPerformArrowEmote(player, 2); return; } } int num = -1; if (current.digit1Key != null && ((ButtonControl)current.digit1Key).wasPressedThisFrame) { num = 0; } else if (current.digit2Key != null && ((ButtonControl)current.digit2Key).wasPressedThisFrame) { num = 1; } else if (current.digit3Key != null && ((ButtonControl)current.digit3Key).wasPressedThisFrame) { num = 2; } else if (current.digit4Key != null && ((ButtonControl)current.digit4Key).wasPressedThisFrame) { num = 3; } if (num >= 0) { if (DebugEnabled) { Log.LogInfo((object)("[IIS] Key pressed -> slot " + (num + 1))); } TrySwitchToSlot(player, num); } } private static void TryPerformArrowEmote(PlayerControllerB player, int emoteId) { try { if (!GameAccess.EmoteReady) { if (DebugEnabled) { Log.LogWarning((object)"[IIS] Emote reflection not ready."); } } else if (((((NetworkBehaviour)player).IsOwner && player.isPlayerControlled && (!((NetworkBehaviour)player).IsServer || player.isHostPlayerObject)) || player.isTestingPlayer) && GameAccess.CheckConditionsForEmote(player) && !(GameAccess.GetTimeSinceStartingEmote(player) < 0.5f)) { GameAccess.SetTimeSinceStartingEmote(player, 0f); GameAccess.SetPerformingEmote(player, value: true); if ((Object)(object)player.playerBodyAnimator != (Object)null) { player.playerBodyAnimator.SetInteger("emoteNumber", emoteId); } GameAccess.StartPerformingEmoteServerRpc(player); } } catch (Exception ex) { Log.LogError((object)("[IIS] Emote error: " + ex)); } } private static void TrySwitchToSlot(PlayerControllerB player, int targetSlot) { try { if (!GameAccess.Ready) { if (DebugEnabled) { Log.LogWarning((object)"[IIS] Not ready: reflection cache missing."); } return; } if (player.inTerminalMenu || player.isTypingChat || player.inSpecialMenu) { if (DebugEnabled) { Log.LogInfo((object)"[IIS] Blocked: menu/terminal/chat."); } return; } bool flag = SpawnDelaySeconds > 0f && Time.timeSinceLevelLoad < SpawnDelaySeconds; if (!IgnoreBusyChecksAlways && (!flag || !IgnoreBusyDuringSpawnDelay) && (player.isGrabbingObjectAnimation || player.inSpecialInteractAnimation || player.activatingItem || player.twoHanded)) { if (DebugEnabled) { Log.LogInfo((object)"[IIS] Blocked: animation/interact/activatingItem/twoHanded."); } return; } if ((player.jetpackControls || player.disablingJetpackControls) && (Object)(object)player.currentlyHeldObjectServer != (Object)null && (Object)(object)player.currentlyHeldObjectServer.itemProperties != (Object)null && player.currentlyHeldObjectServer.itemProperties.itemId == 13) { if (DebugEnabled) { Log.LogInfo((object)"[IIS] Blocked: jetpackControls."); } return; } if ((Object)(object)player.quickMenuManager != (Object)null && player.quickMenuManager.isMenuOpen) { if (DebugEnabled) { Log.LogInfo((object)"[IIS] Blocked: quick menu."); } return; } if (GameAccess.GetThrowingObject(player)) { if (DebugEnabled) { Log.LogInfo((object)"[IIS] Blocked: throwingObject."); } return; } float timeSinceSwitchingSlots = GameAccess.GetTimeSinceSwitchingSlots(player); if (timeSinceSwitchingSlots >= 0f && timeSinceSwitchingSlots < CooldownSeconds) { if (DebugEnabled) { Log.LogInfo((object)("[IIS] Blocked: cooldown timeSinceSwitchingSlots=" + timeSinceSwitchingSlots)); } return; } GrabbableObject[] itemSlots = player.ItemSlots; if (itemSlots == null || itemSlots.Length == 0) { return; } int num = Mathf.Min(itemSlots.Length, ControlledSlots); if (num <= 0) { return; } targetSlot = Mathf.Clamp(targetSlot, 0, num - 1); if (player.currentItemSlot != targetSlot) { ShipBuildModeManager instance = ShipBuildModeManager.Instance; if ((Object)(object)instance != (Object)null && instance.InBuildMode) { instance.CancelBuildMode(true); } if ((Object)(object)player.playerBodyAnimator != (Object)null) { player.playerBodyAnimator.SetBool("GrabValidated", false); } if (GameAccess.HasDirectSlotRpc) { GameAccess.SwitchToItemSlot(player, targetSlot); GameAccess.SwitchToSlotServerRpc(player, targetSlot); } else { DoLegacyStepSwitch(player, targetSlot, num); } PlayGrabSound(player); GameAccess.SetTimeSinceSwitchingSlots(player, 0f); } } catch (Exception ex) { Log.LogError((object)("[IIS] Switch error: " + ex)); } } private static void DoLegacyStepSwitch(PlayerControllerB player, int targetSlot, int len) { if (!SyncStepsFallback) { GameAccess.SwitchToItemSlot(player, targetSlot); return; } int currentItemSlot = player.currentItemSlot; int num = (targetSlot - currentItemSlot + len) % len; int num2 = (currentItemSlot - targetSlot + len) % len; bool flag = num < num2; int num3 = (flag ? num : num2); if (num3 <= 0 && currentItemSlot != targetSlot) { flag = true; num3 = num; } for (int i = 0; i < num3; i++) { int slot = (flag ? ((player.currentItemSlot + 1) % len) : ((player.currentItemSlot == 0) ? (len - 1) : (player.currentItemSlot - 1))); GameAccess.SwitchToItemSlot(player, slot); GameAccess.SwitchItemSlotsServerRpc(player, flag); } } private static void PlayGrabSound(PlayerControllerB player) { if (!PlayGrabSfx || (Object)(object)player.currentlyHeldObjectServer == (Object)null) { return; } Item itemProperties = player.currentlyHeldObjectServer.itemProperties; if (!((Object)(object)itemProperties == (Object)null) && !((Object)(object)itemProperties.grabSFX == (Object)null)) { AudioSource component = ((Component)player.currentlyHeldObjectServer).gameObject.GetComponent(); if ((Object)(object)component != (Object)null) { component.PlayOneShot(itemProperties.grabSFX, 0.6f); } } } } internal static class GameAccess { internal static bool Ready; internal static bool EmoteReady; private static MethodInfo _switchToItemSlot; private static MethodInfo _switchToSlotServerRpc; private static MethodInfo _switchItemSlotsServerRpc; private static MethodInfo _checkConditionsForEmote; private static MethodInfo _startPerformingEmoteServerRpc; private static FieldInfo _throwingObject; private static FieldInfo _timeSinceSwitchingSlots; private static FieldInfo _timeSinceStartingEmote; private static FieldInfo _performingEmote; internal static bool HasDirectSlotRpc => _switchToSlotServerRpc != null; internal static void Init() { try { Type typeFromHandle = typeof(PlayerControllerB); _switchToItemSlot = AccessTools.Method(typeFromHandle, "SwitchToItemSlot", new Type[2] { typeof(int), typeof(GrabbableObject) }, (Type[])null); _switchToSlotServerRpc = AccessTools.Method(typeFromHandle, "SwitchToSlotServerRpc", new Type[1] { typeof(int) }, (Type[])null); _switchItemSlotsServerRpc = AccessTools.Method(typeFromHandle, "SwitchItemSlotsServerRpc", new Type[1] { typeof(bool) }, (Type[])null); _throwingObject = AccessTools.Field(typeFromHandle, "throwingObject"); _timeSinceSwitchingSlots = AccessTools.Field(typeFromHandle, "timeSinceSwitchingSlots"); _checkConditionsForEmote = AccessTools.Method(typeFromHandle, "CheckConditionsForEmote", (Type[])null, (Type[])null); _startPerformingEmoteServerRpc = AccessTools.Method(typeFromHandle, "StartPerformingEmoteServerRpc", (Type[])null, (Type[])null); _timeSinceStartingEmote = AccessTools.Field(typeFromHandle, "timeSinceStartingEmote"); _performingEmote = AccessTools.Field(typeFromHandle, "performingEmote"); Ready = _switchToItemSlot != null && _throwingObject != null && _timeSinceSwitchingSlots != null && (_switchToSlotServerRpc != null || _switchItemSlotsServerRpc != null); EmoteReady = _checkConditionsForEmote != null && _startPerformingEmoteServerRpc != null && _timeSinceStartingEmote != null && _performingEmote != null; if (InstantItemSlotsPlugin.DebugEnabled && InstantItemSlotsPlugin.Log != null) { InstantItemSlotsPlugin.Log.LogInfo((object)("[IIS] Reflection: SwitchToItemSlot=" + (_switchToItemSlot != null) + " SwitchToSlotServerRpc=" + (_switchToSlotServerRpc != null) + " SwitchItemSlotsServerRpc=" + (_switchItemSlotsServerRpc != null) + " CheckConditionsForEmote=" + (_checkConditionsForEmote != null) + " StartPerformingEmoteServerRpc=" + (_startPerformingEmoteServerRpc != null))); } } catch (Exception ex) { Ready = false; EmoteReady = false; if (InstantItemSlotsPlugin.Log != null) { InstantItemSlotsPlugin.Log.LogError((object)("[IIS] Reflection init failed: " + ex)); } } } internal static void SwitchToItemSlot(PlayerControllerB player, int slot) { _switchToItemSlot.Invoke(player, new object[2] { slot, null }); } internal static void SwitchToSlotServerRpc(PlayerControllerB player, int slot) { _switchToSlotServerRpc?.Invoke(player, new object[1] { slot }); } internal static void SwitchItemSlotsServerRpc(PlayerControllerB player, bool forward) { _switchItemSlotsServerRpc?.Invoke(player, new object[1] { forward }); } internal static bool GetThrowingObject(PlayerControllerB player) { try { return (bool)_throwingObject.GetValue(player); } catch { return false; } } internal static float GetTimeSinceSwitchingSlots(PlayerControllerB player) { try { return (float)_timeSinceSwitchingSlots.GetValue(player); } catch { return -1f; } } internal static void SetTimeSinceSwitchingSlots(PlayerControllerB player, float value) { try { _timeSinceSwitchingSlots.SetValue(player, value); } catch { } } internal static bool CheckConditionsForEmote(PlayerControllerB player) { try { return (bool)_checkConditionsForEmote.Invoke(player, null); } catch { return false; } } internal static float GetTimeSinceStartingEmote(PlayerControllerB player) { try { return (float)_timeSinceStartingEmote.GetValue(player); } catch { return -1f; } } internal static void SetTimeSinceStartingEmote(PlayerControllerB player, float value) { try { _timeSinceStartingEmote.SetValue(player, value); } catch { } } internal static void SetPerformingEmote(PlayerControllerB player, bool value) { try { _performingEmote.SetValue(player, value); } catch { } } internal static void StartPerformingEmoteServerRpc(PlayerControllerB player) { _startPerformingEmoteServerRpc?.Invoke(player, null); } } [HarmonyPatch] internal static class Patches { [HarmonyPatch(typeof(PlayerControllerB), "Update")] [HarmonyPostfix] private static void PlayerUpdatePostfix(PlayerControllerB __instance) { InstantItemSlotsPlugin.HandleLocalUpdate(__instance); } [HarmonyPatch(typeof(PlayerControllerB), "PerformEmote")] [HarmonyPrefix] private static bool PerformEmotePrefix() { return !InstantItemSlotsPlugin.ArrowEmotesOnly; } }