using System; using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace BigCartFix; [BepInPlugin("bigcart.fix.runtime", "Big Cart Fix", "1.0.5")] public sealed class Plugin : BaseUnityPlugin { internal const string PluginGuid = "bigcart.fix.runtime"; internal const string PluginName = "Big Cart Fix"; internal const string PluginVersion = "1.0.5"; private void Awake() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) ModLog.Source = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); new Harmony("bigcart.fix.runtime").PatchAll(); CartPurchaseSettings.Apply(); ModLog.Info("Patched Big Cart display, null guard, and shop purchase limits."); } } internal static class ModConfig { private const string DisplaySection = "Display"; private const string ShopSection = "Shop"; private const string GeneralSection = "General"; private static ConfigEntry debugLogging; private static ConfigEntry maxBigCartsForPurchase; private static ConfigEntry valueTextPath; public static bool DebugLogging { get { if (debugLogging != null) { return debugLogging.Value; } return false; } } public static int MaxBigCartsForPurchase { get { if (maxBigCartsForPurchase == null) { return 4; } return Mathf.Clamp(maxBigCartsForPurchase.Value, 1, 20); } } public static string ValueTextPath { get { if (valueTextPath == null || valueTextPath.Value == null) { return string.Empty; } return valueTextPath.Value.Trim(); } } public static void Bind(ConfigFile config) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown debugLogging = config.Bind("General", "DebugLogging", false, "Enable extra logs for Big Cart Fix."); valueTextPath = config.Bind("Display", "ValueTextPath", string.Empty, "Optional Transform path to the Big Cart TMP value text. Leave empty to use the built-in paths."); maxBigCartsForPurchase = config.Bind("Shop", "MaxBigCartsForPurchase", 4, new ConfigDescription("Maximum amount of Big Carts that can be offered/purchased during a run.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 20), new object[0])); } } internal static class ModLog { public static ManualLogSource Source; public static void Info(string message) { if (Source != null) { Source.LogInfo((object)message); } } public static void Warning(string message) { if (Source != null) { Source.LogWarning((object)message); } else { Debug.LogWarning((object)("[Big Cart Fix] " + message)); } } public static void DebugInfo(string message) { if (ModConfig.DebugLogging && Source != null) { Source.LogInfo((object)("[Debug] " + message)); } } } internal static class CartValueDisplay { private static readonly FieldInfo HaulCurrentField = AccessTools.Field(typeof(PhysGrabCart), "haulCurrent"); private static readonly FieldInfo OriginalColorField = AccessTools.Field(typeof(ValueScreen), "originalColor"); private static readonly FieldInfo ValuePreviousField = AccessTools.Field(typeof(ValueScreen), "valuePrevious"); private static readonly FieldInfo ValueCurrentField = AccessTools.Field(typeof(ValueScreen), "valueCurrent"); private static readonly FieldInfo ResetTextField = AccessTools.Field(typeof(ValueScreen), "resetText"); private static readonly FieldInfo SoundSourceField = AccessTools.Field(typeof(Sound), "Source"); private static readonly FieldInfo[] SoundFields = typeof(Sound).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly string[] ValueTextPathCandidates = new string[12] { "Value Screen/Value Text", "Value Screen/Text", "Value Screen/Text (TMP)", "ValueScreen/ValueText", "ValueScreen/Text", "Screen/Value Text", "Screen/Text", "Canvas/Value Text", "Canvas/Text", "Canvas/Text (TMP)", "Value Text", "Text (TMP)" }; private static readonly Dictionary FallbackTexts = new Dictionary(); private static readonly HashSet BoundScreens = new HashSet(); private static readonly HashSet MissingDisplayWarnings = new HashSet(); private static readonly HashSet LoggedTextPaths = new HashSet(); private static Sound sourceIncreaseSound; private static Sound sourceDecreaseSound; private static bool loggedSoundAttached; private static bool loggedSoundMissing; public static void EnsureBound(PhysGrabCart cart) { if ((Object)(object)cart == (Object)null) { return; } int instanceID = ((Object)cart).GetInstanceID(); if (FallbackTexts.TryGetValue(instanceID, out var value) && (Object)(object)value != (Object)null) { return; } LogTextPaths(cart); if ((Object)(object)cart.valueScreen != (Object)null && (Object)(object)cart.valueScreen.displayText != (Object)null) { BindScreen(cart, cart.valueScreen, cart.valueScreen.displayText); return; } ValueScreen componentInChildren = ((Component)cart).GetComponentInChildren(true); TMP_Text val = FindDisplayText(cart, componentInChildren); TextMeshPro val2 = (TextMeshPro)(object)((val is TextMeshPro) ? val : null); if ((Object)(object)componentInChildren != (Object)null) { if ((Object)(object)componentInChildren.displayText == (Object)null && (Object)(object)val2 != (Object)null) { componentInChildren.displayText = val2; } if ((Object)(object)componentInChildren.displayText != (Object)null) { BindScreen(cart, componentInChildren, componentInChildren.displayText); return; } } if ((Object)(object)val2 != (Object)null) { ValueScreen val3 = ((Component)val2).gameObject.GetComponent(); if ((Object)(object)val3 == (Object)null) { val3 = ((Component)val2).gameObject.AddComponent(); } BindScreen(cart, val3, val2); } else if ((Object)(object)val != (Object)null) { FallbackTexts[instanceID] = val; SetText(val, ReadHaul(cart)); } else if (MissingDisplayWarnings.Add(instanceID)) { ModLog.Warning("Could not find a value text display on this cart prefab."); } } private static void BindScreen(PhysGrabCart cart, ValueScreen screen, TextMeshPro displayText) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)cart == (Object)null) && !((Object)(object)screen == (Object)null) && !((Object)(object)displayText == (Object)null)) { int instanceID = ((Object)cart).GetInstanceID(); screen.displayText = displayText; cart.valueScreen = screen; OriginalColorField.SetValue(screen, ((Graphic)displayText).color); TryAttachSounds(screen); if (BoundScreens.Add(instanceID)) { SetScreenBaseline(screen, ReadHaul(cart)); } } } private static bool TryAttachSounds(ValueScreen screen) { if ((Object)(object)screen == (Object)null) { return false; } CacheSourceSounds(screen); if (!HasPlayableSound(screen.soundValueIncrease) && HasPlayableSound(sourceIncreaseSound)) { screen.soundValueIncrease = CloneSound(sourceIncreaseSound); } if (!HasPlayableSound(screen.soundValueDecrease) && HasPlayableSound(sourceDecreaseSound)) { screen.soundValueDecrease = CloneSound(sourceDecreaseSound); } bool flag = HasPlayableSound(screen.soundValueIncrease) && HasPlayableSound(screen.soundValueDecrease); if (flag && !loggedSoundAttached) { ModLog.Info("Attached vanilla cart value sounds to Big Cart value display."); loggedSoundAttached = true; } return flag; } private static void CacheSourceSounds(ValueScreen skipScreen) { if (HasPlayableSound(sourceIncreaseSound) && HasPlayableSound(sourceDecreaseSound)) { return; } ValueScreen[] array = Resources.FindObjectsOfTypeAll(); ValueScreen[] array2 = array; foreach (ValueScreen val in array2) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)skipScreen) && HasPlayableSound(val.soundValueIncrease) && HasPlayableSound(val.soundValueDecrease)) { sourceIncreaseSound = val.soundValueIncrease; sourceDecreaseSound = val.soundValueDecrease; break; } } } private static bool HasPlayableSound(Sound sound) { if (sound != null && sound.Sounds != null) { return sound.Sounds.Length > 0; } return false; } private static Sound CloneSound(Sound source) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown if (source == null) { return null; } Sound val = new Sound(); FieldInfo[] soundFields = SoundFields; foreach (FieldInfo fieldInfo in soundFields) { if (!fieldInfo.IsInitOnly && !(fieldInfo.Name == "Source") && !(fieldInfo.Name == "LowPassLogic") && !(fieldInfo.Name == "HasLowPassLogic")) { fieldInfo.SetValue(val, fieldInfo.GetValue(source)); } } SoundSourceField.SetValue(val, null); return val; } private static void SetScreenBaseline(ValueScreen screen, int value) { int num = Mathf.Max(0, value); ValuePreviousField.SetValue(screen, num); ValueCurrentField.SetValue(screen, num); ResetTextField.SetValue(screen, true); SetText((TMP_Text)(object)screen.displayText, num); } public static void Refresh(PhysGrabCart cart) { EnsureBound(cart); if ((Object)(object)cart != (Object)null && FallbackTexts.TryGetValue(((Object)cart).GetInstanceID(), out var value) && (Object)(object)value != (Object)null) { SetText(value, ReadHaul(cart)); } } public static bool TryHandleValueScreenUpdate(ValueScreen screen, int value) { if ((Object)(object)screen == (Object)null) { return false; } if ((Object)(object)screen.displayText == (Object)null) { return false; } if (TryAttachSounds(screen)) { return true; } if (!loggedSoundMissing) { ModLog.Warning("No playable vanilla cart value sounds found yet; updating Big Cart value display without audio."); loggedSoundMissing = true; } SetText((TMP_Text)(object)screen.displayText, value); return false; } private static int ReadHaul(PhysGrabCart cart) { object value = HaulCurrentField.GetValue(cart); if (value is int) { return (int)value; } return 0; } private static TMP_Text FindDisplayText(PhysGrabCart cart, ValueScreen existingScreen) { if ((Object)(object)existingScreen != (Object)null && (Object)(object)existingScreen.displayText != (Object)null) { return (TMP_Text)(object)existingScreen.displayText; } TMP_Text val = FindTextAtPath(cart, ModConfig.ValueTextPath); if ((Object)(object)val != (Object)null) { ModLog.DebugInfo("Found value text from config path: " + GetPath(((Component)cart).transform, val.transform)); return val; } string[] valueTextPathCandidates = ValueTextPathCandidates; foreach (string path in valueTextPathCandidates) { TMP_Text val2 = FindTextAtPath(cart, path); if ((Object)(object)val2 != (Object)null) { ModLog.DebugInfo("Found value text from built-in path: " + GetPath(((Component)cart).transform, val2.transform)); return val2; } } TMP_Text val3 = FindTextByInitialValue(cart); if ((Object)(object)val3 != (Object)null) { ModLog.DebugInfo("Found value text by initial value: " + GetPath(((Component)cart).transform, val3.transform)); return val3; } return FindOnlyWorldText(cart); } private static TMP_Text FindTextAtPath(PhysGrabCart cart, string path) { if ((Object)(object)cart == (Object)null || string.IsNullOrEmpty(path)) { return null; } Transform val = FindTransformByPath(((Component)cart).transform, path); if ((Object)(object)val == (Object)null) { return null; } TMP_Text component = ((Component)val).GetComponent(); if ((Object)(object)component != (Object)null) { return component; } return ((Component)val).GetComponentInChildren(true); } private static Transform FindTransformByPath(Transform root, string path) { if ((Object)(object)root == (Object)null || string.IsNullOrEmpty(path)) { return null; } string text = path.Trim().Replace('\\', '/').Trim(new char[1] { '/' }); if (text.Length == 0) { return null; } if (string.Equals(text, ((Object)root).name, StringComparison.OrdinalIgnoreCase)) { return root; } string text2 = ((Object)root).name + "/"; if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { text = text.Substring(text2.Length); } Transform val = root.Find(text); if ((Object)(object)val != (Object)null) { return val; } return FindTransformByPathIgnoreCase(root, text); } private static Transform FindTransformByPathIgnoreCase(Transform root, string path) { string[] array = path.Split(new char[1] { '/' }); Transform val = root; string[] array2 = array; foreach (string text in array2) { string text2 = text.Trim(); if (text2.Length == 0) { continue; } Transform val2 = null; for (int j = 0; j < val.childCount; j++) { Transform child = val.GetChild(j); if (string.Equals(((Object)child).name, text2, StringComparison.OrdinalIgnoreCase)) { val2 = child; break; } } if ((Object)(object)val2 == (Object)null) { return null; } val = val2; } return val; } private static TMP_Text FindTextByInitialValue(PhysGrabCart cart) { TMP_Text[] componentsInChildren = ((Component)cart).GetComponentsInChildren(true); TMP_Text[] array = componentsInChildren; foreach (TMP_Text val in array) { if (!((Object)(object)val == (Object)null) && val.text != null && val.text.Contains("100000")) { return val; } } return null; } private static TMP_Text FindOnlyWorldText(PhysGrabCart cart) { TMP_Text[] componentsInChildren = ((Component)cart).GetComponentsInChildren(true); TMP_Text val = null; int num = 0; TMP_Text[] array = componentsInChildren; foreach (TMP_Text val2 in array) { if (val2 is TextMeshPro) { val = val2; num++; } } if (num == 1) { ModLog.DebugInfo("Found the only world TMP text: " + GetPath(((Component)cart).transform, val.transform)); return val; } return null; } private static void LogTextPaths(PhysGrabCart cart) { if (!ModConfig.DebugLogging || (Object)(object)cart == (Object)null) { return; } int instanceID = ((Object)cart).GetInstanceID(); if (!LoggedTextPaths.Add(instanceID)) { return; } TMP_Text[] componentsInChildren = ((Component)cart).GetComponentsInChildren(true); TMP_Text[] array = componentsInChildren; foreach (TMP_Text val in array) { if (!((Object)(object)val == (Object)null)) { string text = ((val.text == null) ? string.Empty : val.text); ModLog.DebugInfo("TMP path: " + GetPath(((Component)cart).transform, val.transform) + " | text: " + text); } } } private static string GetPath(Transform root, Transform target) { if ((Object)(object)root == (Object)null || (Object)(object)target == (Object)null) { return string.Empty; } List list = new List(); Transform val = target; while ((Object)(object)val != (Object)null) { list.Insert(0, ((Object)val).name); if ((Object)(object)val == (Object)(object)root) { break; } val = val.parent; } return string.Join("/", list.ToArray()); } private static void SetText(TMP_Text text, int value) { int num = Mathf.Max(0, value); text.text = "$" + SemiFunc.DollarGetString(num); } } internal static class CartPurchaseSettings { private static readonly string[] BigCartItemNames = new string[4] { "Cart Big", "Big Cart", "B.I.G. C.A.R.T.", "Big_Cart" }; private static readonly HashSet AppliedItems = new HashSet(); public static void Apply() { if ((Object)(object)StatsManager.instance == (Object)null || StatsManager.instance.itemDictionary == null) { return; } foreach (KeyValuePair item in StatsManager.instance.itemDictionary) { if (IsBigCartItem(item.Key, item.Value)) { ApplyToItem(item.Value); } } } private static bool IsBigCartItem(string dictionaryKey, Item item) { if ((Object)(object)item == (Object)null) { return false; } if (!MatchesKnownName(dictionaryKey) && !MatchesKnownName(((Object)item).name)) { return MatchesKnownName(item.itemName); } return true; } private static bool MatchesKnownName(string value) { if (string.IsNullOrEmpty(value)) { return false; } string a = value.Replace("(Clone)", string.Empty).Trim(); string[] bigCartItemNames = BigCartItemNames; foreach (string b in bigCartItemNames) { if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void ApplyToItem(Item item) { int num = (item.maxPurchaseAmount = (item.maxAmountInShop = (item.maxAmount = ModConfig.MaxBigCartsForPurchase))); if (AppliedItems.Add(((Object)item).GetInstanceID())) { ModLog.Info("Big Cart shop limit set to " + num + "."); } else { ModLog.DebugInfo("Big Cart shop limit refreshed to " + num + "."); } } } [HarmonyPatch(typeof(StatsManager), "LoadItemsFromFolder")] internal static class StatsManagerLoadItemsFromFolderPatch { private static void Postfix() { CartPurchaseSettings.Apply(); } } [HarmonyPatch(typeof(ShopManager), "GetAllItemsFromStatsManager")] internal static class ShopManagerGetAllItemsFromStatsManagerPatch { private static void Prefix() { CartPurchaseSettings.Apply(); } } [HarmonyPatch(typeof(ItemManager), "GetPurchasedItems")] internal static class ItemManagerGetPurchasedItemsPatch { private static void Prefix() { CartPurchaseSettings.Apply(); } } [HarmonyPatch(typeof(PhysGrabCart), "Start")] internal static class PhysGrabCartStartPatch { private static void Postfix(PhysGrabCart __instance) { CartValueDisplay.EnsureBound(__instance); } } [HarmonyPatch(typeof(PhysGrabCart), "ObjectsInCart")] internal static class PhysGrabCartObjectsInCartPatch { private static readonly FieldInfo InCartField = AccessTools.Field(typeof(PhysGrabCart), "inCart"); private static readonly HashSet MissingInCartWarnings = new HashSet(); private static bool Prefix(PhysGrabCart __instance) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown CartValueDisplay.EnsureBound(__instance); Transform val = (Transform)InCartField.GetValue(__instance); if ((Object)(object)val != (Object)null) { return true; } int instanceID = ((Object)__instance).GetInstanceID(); if (MissingInCartWarnings.Add(instanceID)) { ModLog.Warning("Skipping cart item scan because this cart prefab has no 'In Cart' transform."); } return false; } private static void Postfix(PhysGrabCart __instance) { CartValueDisplay.Refresh(__instance); } private static Exception Finalizer(Exception __exception) { if (__exception is NullReferenceException) { return null; } return __exception; } private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) { FieldInfo valueScreenField = AccessTools.Field(typeof(PhysGrabCart), "valueScreen"); FieldInfo haulCurrentField = AccessTools.Field(typeof(PhysGrabCart), "haulCurrent"); MethodInfo updateValueMethod = AccessTools.Method(typeof(ValueScreen), "UpdateValue", new Type[1] { typeof(int) }, (Type[])null); List codes = new List(instructions); bool patched = false; for (int i = 0; i < codes.Count; i++) { CodeInstruction code = codes[i]; if (!patched && CodeInstructionExtensions.LoadsField(code, valueScreenField, false) && i + 3 < codes.Count && codes[i + 1].opcode == OpCodes.Ldarg_0 && CodeInstructionExtensions.LoadsField(codes[i + 2], haulCurrentField, false) && CodeInstructionExtensions.Calls(codes[i + 3], updateValueMethod)) { Label hasValueScreenLabel = generator.DefineLabel(); codes[i + 1].labels.Add(hasValueScreenLabel); yield return code; yield return new CodeInstruction(OpCodes.Dup, (object)null); yield return new CodeInstruction(OpCodes.Brtrue_S, (object)hasValueScreenLabel); yield return new CodeInstruction(OpCodes.Pop, (object)null); yield return new CodeInstruction(OpCodes.Ret, (object)null); patched = true; } else { yield return code; } } if (!patched) { ModLog.Warning("Could not find ValueScreen.UpdateValue in PhysGrabCart.ObjectsInCart."); } } } [HarmonyPatch(typeof(ValueScreen), "UpdateValue")] internal static class ValueScreenUpdateValuePatch { private static bool Prefix(ValueScreen __instance, int __0) { return CartValueDisplay.TryHandleValueScreenUpdate(__instance, __0); } }