using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SoloPossible")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("SoloPossible")] [assembly: AssemblyTitle("SoloPossible")] [assembly: AssemblyVersion("1.0.0.0")] namespace SoloPossible; [BepInPlugin("dottore.solopossible", "SoloPossible", "1.0.1")] public class SoloPossiblePlugin : BaseUnityPlugin { public const string PluginGuid = "dottore.solopossible"; public const string PluginName = "SoloPossible"; public const string PluginVersion = "1.0.1"; internal static ManualLogSource Log; internal static ConfigEntry EnableMod; internal static ConfigEntry ItemWeightMultiplier; internal static ConfigEntry SprintSpeedMultiplier; internal static ConfigEntry StaminaRegenMultiplier; internal static ConfigEntry MaxHealth; internal static ConfigEntry ShipRegenAmount; internal static ConfigEntry ShipRegenInterval; internal static ConfigEntry InventorySlots; private Harmony harmony; private bool patchesApplied; private void Awake() { //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; EnableMod = ((BaseUnityPlugin)this).Config.Bind("General", "EnableMod", true, "Enables SoloPossible."); ItemWeightMultiplier = ((BaseUnityPlugin)this).Config.Bind("Balance", "ItemWeightMultiplier", 0.85f, "Item weight multiplier. 0.85 = 15% lighter."); SprintSpeedMultiplier = ((BaseUnityPlugin)this).Config.Bind("Balance", "SprintSpeedMultiplier", 1.03f, "Sprint speed multiplier. 1.03 = 3% faster."); StaminaRegenMultiplier = ((BaseUnityPlugin)this).Config.Bind("Balance", "StaminaRegenMultiplier", 1.03f, "Stamina recovery multiplier. 1.03 = 3% faster recovery."); MaxHealth = ((BaseUnityPlugin)this).Config.Bind("Health", "MaxHealth", 150, "Health given at the start of each solo life."); ShipRegenAmount = ((BaseUnityPlugin)this).Config.Bind("Health", "ShipRegenAmount", 5, "HP regenerated per interval while inside the ship."); ShipRegenInterval = ((BaseUnityPlugin)this).Config.Bind("Health", "ShipRegenInterval", 5f, "Seconds between ship-only healing ticks."); InventorySlots = ((BaseUnityPlugin)this).Config.Bind("Inventory", "InventorySlots", 5, "Total inventory slots when playing solo. Vanilla is 4. SoloPossible default is 5."); harmony = new Harmony("dottore.solopossible"); if (!EnableMod.Value) { Log.LogInfo((object)"SoloPossible 1.0.1 loaded but disabled by config. No patches applied."); return; } harmony.PatchAll(); patchesApplied = true; Log.LogInfo((object)"SoloPossible 1.0.1 loaded and patched."); } private void OnDestroy() { try { InventoryManager.ForceVanillaInventoryAndHud(); WeightManager.RestoreAllWeights(); } catch { } if (patchesApplied && harmony != null) { harmony.UnpatchSelf(); } } internal static bool EffectsAllowed() { return EnableMod.Value && IsSoloGame(); } internal static bool IsSoloGame() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } return instance.connectedPlayersAmount <= 0; } internal static bool IsLocalPlayer(PlayerControllerB player) { return (Object)(object)player != (Object)null && (Object)(object)StartOfRound.Instance != (Object)null && (Object)(object)StartOfRound.Instance.localPlayerController == (Object)(object)player; } internal static bool IsLocalPlayerAllowed(PlayerControllerB player) { return IsLocalPlayer(player) && EffectsAllowed(); } internal static int WantedInventorySlots() { if (!EffectsAllowed()) { return 4; } return Mathf.Max(4, InventorySlots.Value); } } internal static class InventoryManager { private static readonly MethodInfo SwitchToItemSlotMethod = AccessTools.Method(typeof(PlayerControllerB), "SwitchToItemSlot", (Type[])null, (Type[])null); private static float nextHudMaintenanceTime; internal static void TickHudMaintenance() { if (!(Time.realtimeSinceStartup < nextHudMaintenanceTime)) { nextHudMaintenanceTime = Time.realtimeSinceStartup + 0.5f; ResizeAllPlayerInventories(); if ((Object)(object)HUDManager.Instance != (Object)null) { ResizeHudInventory(HUDManager.Instance); } } } internal static void ForceVanillaInventoryAndHud() { ResizeAllPlayerInventoriesToSize(4); if ((Object)(object)HUDManager.Instance != (Object)null) { ResizeHudInventoryToSize(HUDManager.Instance, 4); } } internal static void ResizePlayerInventory(PlayerControllerB player) { ResizePlayerInventoryToSize(player, SoloPossiblePlugin.WantedInventorySlots()); } internal static void ResizeAllPlayerInventories() { ResizeAllPlayerInventoriesToSize(SoloPossiblePlugin.WantedInventorySlots()); } private static void ResizeAllPlayerInventoriesToSize(int wantedSlots) { PlayerControllerB[] array = Object.FindObjectsOfType(); PlayerControllerB[] array2 = array; foreach (PlayerControllerB player in array2) { ResizePlayerInventoryToSize(player, wantedSlots); } } private static void ResizePlayerInventoryToSize(PlayerControllerB player, int wantedSlots) { if ((Object)(object)player == (Object)null) { return; } wantedSlots = Mathf.Max(4, wantedSlots); if (player.ItemSlots == null || player.ItemSlots.Length != wantedSlots) { GrabbableObject[] array = (GrabbableObject[])(((object)player.ItemSlots) ?? ((object)new GrabbableObject[4])); GrabbableObject[] array2 = (GrabbableObject[])(object)new GrabbableObject[wantedSlots]; int num = Mathf.Min(array.Length, array2.Length); for (int i = 0; i < num; i++) { array2[i] = array[i]; } player.ItemSlots = array2; } } internal static void ResizeHudInventory(HUDManager hud) { ResizeHudInventoryToSize(hud, SoloPossiblePlugin.WantedInventorySlots()); } private static void ResizeHudInventoryToSize(HUDManager hud, int wantedSlots) { //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_021f: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_022d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)hud == (Object)null) { return; } wantedSlots = Mathf.Max(4, wantedSlots); GameObject val = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD/Inventory"); if ((Object)(object)val == (Object)null) { SoloPossiblePlugin.Log.LogWarning((object)"Could not find inventory HUD root."); return; } if (hud.itemSlotIcons == null || hud.itemSlotIconFrames == null) { SoloPossiblePlugin.Log.LogWarning((object)"HUD inventory icon arrays were null."); return; } if (hud.itemSlotIcons.Length < 4 || hud.itemSlotIconFrames.Length < 4) { SoloPossiblePlugin.Log.LogWarning((object)"HUD inventory icon arrays were smaller than expected."); return; } CleanupExtraHudSlots(val.transform, wantedSlots); if (hud.itemSlotIcons.Length == wantedSlots && hud.itemSlotIconFrames.Length == wantedSlots) { CenterHudSlots(val.transform, wantedSlots); return; } Image[] array = (Image[])(object)new Image[wantedSlots]; Image[] array2 = (Image[])(object)new Image[wantedSlots]; int num = Mathf.Min(4, Mathf.Min(hud.itemSlotIcons.Length, hud.itemSlotIconFrames.Length)); for (int i = 0; i < num; i++) { array[i] = hud.itemSlotIcons[i]; array2[i] = hud.itemSlotIconFrames[i]; } if (wantedSlots > 4) { GameObject val2 = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD/Inventory/Slot3"); if ((Object)(object)val2 == (Object)null) { SoloPossiblePlugin.Log.LogWarning((object)"Could not find Slot3 HUD template."); return; } Transform transform = val2.transform; for (int j = 4; j < wantedSlots; j++) { GameObject val3 = GameObject.Find($"Systems/UI/Canvas/IngamePlayerHUD/Inventory/Slot{j}"); GameObject val4; if ((Object)(object)val3 != (Object)null) { val4 = val3; val4.SetActive(true); } else { val4 = Object.Instantiate(val2, val.transform); ((Object)val4).name = $"Slot{j}"; } Vector3 localPosition = transform.localPosition; val4.transform.SetLocalPositionAndRotation(new Vector3(localPosition.x + 50f, localPosition.y, localPosition.z), transform.localRotation); transform = val4.transform; array2[j] = val4.GetComponent(); if (val4.transform.childCount > 0) { array[j] = ((Component)val4.transform.GetChild(0)).GetComponent(); } } } hud.itemSlotIcons = array; hud.itemSlotIconFrames = array2; CenterHudSlots(val.transform, wantedSlots); SoloPossiblePlugin.Log.LogInfo((object)$"Inventory HUD resized to {wantedSlots} slots."); } private static void CleanupExtraHudSlots(Transform inventoryRoot, int wantedSlots) { if ((Object)(object)inventoryRoot == (Object)null) { return; } for (int num = inventoryRoot.childCount - 1; num >= 0; num--) { Transform child = inventoryRoot.GetChild(num); if (((Object)child).name.StartsWith("Slot")) { string s = ((Object)child).name.Replace("Slot", ""); if (int.TryParse(s, out var result) && result >= wantedSlots && result >= 4) { ((Component)child).gameObject.SetActive(false); Object.Destroy((Object)(object)((Component)child).gameObject); } } } } private static void CenterHudSlots(Transform inventoryRoot, int slotCount) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)inventoryRoot == (Object)null) { return; } float num = 50f; float num2 = (float)(slotCount - 1) / 2f; for (int i = 0; i < slotCount; i++) { Transform val = inventoryRoot.Find($"Slot{i}"); if ((Object)(object)val == (Object)null && i < inventoryRoot.childCount) { val = inventoryRoot.GetChild(i); } if (!((Object)(object)val == (Object)null)) { Vector3 localPosition = val.localPosition; val.localPosition = new Vector3(num * ((float)i - num2), localPosition.y, localPosition.z); } } } internal static void HandleFifthSlotHotkey(PlayerControllerB player) { if (SoloPossiblePlugin.IsLocalPlayerAllowed(player) && !player.isPlayerDead && player.ItemSlots != null && player.ItemSlots.Length >= 5 && Keyboard.current != null && (((ButtonControl)Keyboard.current.digit5Key).wasPressedThisFrame || ((ButtonControl)Keyboard.current.numpad5Key).wasPressedThisFrame)) { SwitchToSlot(player, 4); } } private static void SwitchToSlot(PlayerControllerB player, int slot) { if (SwitchToItemSlotMethod == null) { SoloPossiblePlugin.Log.LogWarning((object)"Could not find PlayerControllerB.SwitchToItemSlot."); return; } ParameterInfo[] parameters = SwitchToItemSlotMethod.GetParameters(); try { if (parameters.Length == 1) { SwitchToItemSlotMethod.Invoke(player, new object[1] { slot }); } else { SwitchToItemSlotMethod.Invoke(player, new object[2] { slot, null }); } } catch { SoloPossiblePlugin.Log.LogWarning((object)$"Failed to switch to inventory slot {slot}."); } } } internal static class WeightManager { private static readonly Dictionary OriginalWeights = new Dictionary(); private static bool lastAllowedState = false; internal static void RegisterAndApply(GrabbableObject grabbable) { if (!((Object)(object)grabbable == (Object)null) && !((Object)(object)grabbable.itemProperties == (Object)null)) { Item itemProperties = grabbable.itemProperties; if (!OriginalWeights.ContainsKey(itemProperties)) { OriginalWeights[itemProperties] = itemProperties.weight; } ApplyToItem(itemProperties); } } internal static void RestoreAllWeights() { foreach (KeyValuePair originalWeight in OriginalWeights) { if ((Object)(object)originalWeight.Key != (Object)null) { originalWeight.Key.weight = originalWeight.Value; } } lastAllowedState = false; } internal static void RefreshAll() { lastAllowedState = SoloPossiblePlugin.EffectsAllowed(); foreach (Item key in OriginalWeights.Keys) { ApplyToItem(key); } } internal static void RefreshAllIfNeeded() { bool flag = SoloPossiblePlugin.EffectsAllowed(); if (flag == lastAllowedState) { return; } lastAllowedState = flag; foreach (Item key in OriginalWeights.Keys) { ApplyToItem(key); } } private static void ApplyToItem(Item item) { if (!((Object)(object)item == (Object)null) && OriginalWeights.ContainsKey(item)) { float num = OriginalWeights[item]; if (!SoloPossiblePlugin.EffectsAllowed()) { item.weight = num; return; } float num2 = Mathf.Max(0f, num - 1f); item.weight = 1f + num2 * SoloPossiblePlugin.ItemWeightMultiplier.Value; } } } internal static class HealthManager { private static readonly HashSet InitializedPlayers = new HashSet(); private static float regenTimer; internal static void ResetLifeTracking() { InitializedPlayers.Clear(); regenTimer = 0f; } internal static void Tick(PlayerControllerB player) { if (SoloPossiblePlugin.IsLocalPlayerAllowed(player) && player.isPlayerControlled && !player.isPlayerDead) { EnsureSoloPossibleHealth(player); TickShipRegen(player); } } private static void EnsureSoloPossibleHealth(PlayerControllerB player) { if (!InitializedPlayers.Contains(player.playerClientId) && player.health > 0) { int num = Mathf.Max(1, SoloPossiblePlugin.MaxHealth.Value); if (player.health < num) { player.health = num; } InitializedPlayers.Add(player.playerClientId); UpdateHealthUi(player.health); } } private static void TickShipRegen(PlayerControllerB player) { int num = Mathf.Max(1, SoloPossiblePlugin.MaxHealth.Value); if (player.health >= num) { regenTimer = 0f; return; } if (!player.isInHangarShipRoom) { regenTimer = 0f; return; } regenTimer += Time.deltaTime; if (!(regenTimer < Mathf.Max(0.1f, SoloPossiblePlugin.ShipRegenInterval.Value))) { regenTimer = 0f; int num2 = Mathf.Max(0, SoloPossiblePlugin.ShipRegenAmount.Value); player.health = Mathf.Clamp(player.health + num2, 0, num); UpdateHealthUi(player.health); } } private static void UpdateHealthUi(int health) { if (!((Object)(object)HUDManager.Instance == (Object)null)) { HUDManager.Instance.UpdateHealthUI(health, false); } } } [HarmonyPatch(typeof(HUDManager), "Awake")] internal static class HudManagerAwakePatch { private static void Postfix(HUDManager __instance) { InventoryManager.ResizeHudInventory(__instance); } } [HarmonyPatch(typeof(PlayerControllerB), "Awake")] internal static class PlayerControllerAwakePatch { private static void Postfix(PlayerControllerB __instance) { InventoryManager.ResizePlayerInventory(__instance); } } [HarmonyPatch(typeof(GrabbableObject), "Start")] internal static class GrabbableObjectStartPatch { private static void Postfix(GrabbableObject __instance) { WeightManager.RegisterAndApply(__instance); } } [HarmonyPatch(typeof(StartOfRound), "ReviveDeadPlayers")] internal static class ReviveDeadPlayersPatch { private static void Postfix() { HealthManager.ResetLifeTracking(); InventoryManager.ResizeAllPlayerInventories(); if ((Object)(object)HUDManager.Instance != (Object)null) { InventoryManager.ResizeHudInventory(HUDManager.Instance); } WeightManager.RefreshAll(); } } [HarmonyPatch(typeof(StartOfRound), "Start")] internal static class StartOfRoundStartPatch { private static void Postfix() { HealthManager.ResetLifeTracking(); InventoryManager.ResizeAllPlayerInventories(); if ((Object)(object)HUDManager.Instance != (Object)null) { InventoryManager.ResizeHudInventory(HUDManager.Instance); } WeightManager.RefreshAll(); } } [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] internal static class ConnectClientToPlayerObjectPatch { private static void Postfix(PlayerControllerB __instance) { InventoryManager.ResizeAllPlayerInventories(); if ((Object)(object)HUDManager.Instance != (Object)null) { InventoryManager.ResizeHudInventory(HUDManager.Instance); } if (SoloPossiblePlugin.IsLocalPlayerAllowed(__instance)) { HealthManager.Tick(__instance); } WeightManager.RefreshAll(); } } [HarmonyPatch(typeof(PlayerControllerB), "Update")] internal static class PlayerUpdatePatch { private static readonly Dictionary PreviousMovementSpeeds = new Dictionary(); private static void Prefix(PlayerControllerB __instance, ref bool ___isPlayerControlled, ref bool ___isSprinting, ref float ___movementSpeed) { PreviousMovementSpeeds[__instance] = ___movementSpeed; if (___isPlayerControlled && SoloPossiblePlugin.IsLocalPlayerAllowed(__instance) && ___isSprinting) { ___movementSpeed *= SoloPossiblePlugin.SprintSpeedMultiplier.Value; } } private static void Postfix(PlayerControllerB __instance, ref float ___movementSpeed) { if (PreviousMovementSpeeds.ContainsKey(__instance)) { ___movementSpeed = PreviousMovementSpeeds[__instance]; } if (SoloPossiblePlugin.IsLocalPlayer(__instance)) { InventoryManager.TickHudMaintenance(); InventoryManager.HandleFifthSlotHotkey(__instance); WeightManager.RefreshAllIfNeeded(); HealthManager.Tick(__instance); } } } [HarmonyPatch(typeof(PlayerControllerB), "LateUpdate")] internal static class PlayerStaminaPatch { private static float previousSprintMeter = -1f; private static void Prefix(PlayerControllerB __instance, ref bool ___isPlayerControlled, ref float ___sprintMeter) { if (!___isPlayerControlled || !SoloPossiblePlugin.IsLocalPlayerAllowed(__instance)) { previousSprintMeter = -1f; } else { previousSprintMeter = ___sprintMeter; } } private static void Postfix(PlayerControllerB __instance, ref bool ___isPlayerControlled, ref float ___sprintMeter) { if (!(previousSprintMeter < 0f) && ___isPlayerControlled && SoloPossiblePlugin.IsLocalPlayerAllowed(__instance)) { float num = ___sprintMeter - previousSprintMeter; if (num > 0f) { ___sprintMeter = Mathf.Clamp(previousSprintMeter + num * SoloPossiblePlugin.StaminaRegenMultiplier.Value, 0f, 1f); } } } }