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<bool> _debug;
private static ConfigEntry<bool> _arrowEmotesOnly;
private static ConfigEntry<int> _controlledSlots;
private static ConfigEntry<float> _cooldownSeconds;
private static ConfigEntry<float> _spawnDelaySeconds;
private static ConfigEntry<bool> _ignoreBusyDuringSpawnDelay;
private static ConfigEntry<bool> _ignoreBusyChecksAlways;
private static ConfigEntry<bool> _playGrabSfx;
private static ConfigEntry<bool> _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<bool>("General", "EnableDebugLogging", false, "Enables debug logging.");
_arrowEmotesOnly = ((BaseUnityPlugin)this).Config.Bind<bool>("Emotes", "UseArrowEmotesOnly", true, "UpArrow = emote 1, DownArrow = emote 2. Vanilla emote input is blocked while enabled.");
_controlledSlots = ((BaseUnityPlugin)this).Config.Bind<int>("Hotbar", "DigitControlledSlots", 4, "How many slots are controlled by 1/2/3/4 (1..4).");
_cooldownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Hotbar", "DigitSwitchCooldownSeconds", 0f, "Minimum time since last switch allowed.");
_spawnDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Spawn", "RelaxedRulesWindowSeconds", 0f, "Seconds after level load where switching rules can be relaxed.");
_ignoreBusyDuringSpawnDelay = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawn", "IgnoreBusyChecksInsideRelaxedWindow", true, "Ignore animation/interact/activatingItem/twoHanded during the relaxed window.");
_ignoreBusyChecksAlways = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawn", "IgnoreBusyChecksAlways", false, "Always ignore animation/interact/activatingItem/twoHanded checks.");
_playGrabSfx = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "PlayGrabSoundOnDigitSwitch", true, "Play grab sound when switching with digits.");
_syncStepsFallback = ((BaseUnityPlugin)this).Config.Bind<bool>("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<AudioSource>();
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;
}
}