using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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 TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BulkBuy")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: AssemblyProduct("BulkBuy")] [assembly: AssemblyTitle("BulkBuy")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.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; } } } namespace BulkBuy { internal static class AudioSuppressor { private static readonly HashSet BatchedKeys = new HashSet { "Buy_0", "Sell_0", "PickUp_V", "Error" }; private static readonly HashSet PlayedThisBatch = new HashSet(); private static bool _active; public static void BeginBatch() { PlayedThisBatch.Clear(); _active = true; } public static void EndBatch() { _active = false; PlayedThisBatch.Clear(); } public static bool ShouldSuppress(string cooldownKey) { if (!_active || string.IsNullOrEmpty(cooldownKey)) { return false; } if (!BatchedKeys.Contains(cooldownKey)) { return false; } return !PlayedThisBatch.Add(cooldownKey); } } internal static class BaitToastBatcher { private const float TailSeconds = 1f; private const float SlidingWindowSeconds = 0.25f; private static bool _batchActive; private static float _windowEnd; private static int _accumulated; private static bool _emitting; public static void BeginBatch() { _batchActive = true; } public static void EndBatch() { _batchActive = false; _windowEnd = Mathf.Max(_windowEnd, Time.time + 1f); } public static bool TrySwallow(int amount, bool increased) { if (_emitting || !increased) { return false; } if (!_batchActive && Time.time > _windowEnd) { return false; } _accumulated += amount; _windowEnd = Mathf.Max(_windowEnd, Time.time + 0.25f); return true; } public static void Tick() { if (!_batchActive && _accumulated > 0 && !(Time.time <= _windowEnd)) { int accumulated = _accumulated; _accumulated = 0; _windowEnd = 0f; _emitting = true; try { PlayerUI.OnBaitChange(accumulated, true); } finally { _emitting = false; } Plugin.Log.LogDebug((object)$"Collapsed bait toasts into a single +{accumulated}."); } } public static void Reset() { _batchActive = false; _accumulated = 0; _windowEnd = 0f; _emitting = false; } } internal sealed class BulkBuyConfig { public const int HardCeiling = 50; private const string QuantitiesDefault = "1,5,10,25,50"; public ConfigEntry CycleKey { get; } public ConfigEntry ReverseModifier { get; } public ConfigEntry Quantities { get; } public ConfigEntry MaxQuantity { get; } public ConfigEntry ShowIndicator { get; } public ConfigEntry ResetOnShopClose { get; } public IReadOnlyList Ladder { get; private set; } public BulkBuyConfig(ConfigFile file) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Expected O, but got Unknown CycleKey = file.Bind("Keys", "CycleKey", new KeyboardShortcut((KeyCode)116, Array.Empty()), "Cycles the bulk-buy quantity forward. Only active while looking at a bait vendor. Default is T because the game already binds Q to 'drop held item'."); ReverseModifier = file.Bind("Keys", "ReverseModifier", new KeyboardShortcut((KeyCode)116, (KeyCode[])(object)new KeyCode[1] { (KeyCode)304 }), "Cycles the bulk-buy quantity backward. Checked before CycleKey."); Quantities = file.Bind("Quantity", "Quantities", "1,5,10,25,50", "Comma-separated quantity ladder to cycle through. Values above MaxQuantity (and above the hard ceiling of " + 50 + ") are clamped on load. Duplicates and junk entries are dropped."); MaxQuantity = file.Bind("Quantity", "MaxQuantity", 50, new ConfigDescription("Hard ceiling for a single batch. Cannot exceed " + 50 + ".", (AcceptableValueBase)(object)new AcceptableValueRange(1, 50), Array.Empty())); ShowIndicator = file.Bind("UI", "ShowIndicator", true, "Show the 'Bulk: xN' indicator while looking at a bait vendor."); ResetOnShopClose = file.Bind("UI", "ResetOnShopClose", false, "If true, the quantity resets to the first rung of the ladder when you look away from a vendor. If false, it persists."); Ladder = ParseLadder(Quantities.Value, MaxQuantity.Value); Quantities.SettingChanged += delegate { RebuildLadder(); }; MaxQuantity.SettingChanged += delegate { RebuildLadder(); }; } private void RebuildLadder() { Ladder = ParseLadder(Quantities.Value, MaxQuantity.Value); Plugin.Log.LogInfo((object)("Quantity ladder reloaded: " + string.Join(", ", Ladder))); } public static IReadOnlyList ParseLadder(string raw, int maxQuantity) { int val = Mathf.Clamp(maxQuantity, 1, 50); List list = new List(); if (!string.IsNullOrEmpty(raw)) { string[] array = raw.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { Plugin.Log.LogWarning((object)("Ignoring unparseable quantity '" + text + "' in Quantities.")); continue; } if (result < 1) { Plugin.Log.LogWarning((object)$"Ignoring non-positive quantity '{result}' in Quantities."); continue; } int num = Math.Min(result, val); if (num != result) { Plugin.Log.LogInfo((object)$"Clamped quantity {result} to {num}."); } if (!list.Contains(num)) { list.Add(num); } } } if (list.Count == 0) { Plugin.Log.LogWarning((object)"Quantities produced no usable values; falling back to '1,5,10,25,50'."); string[] array = "1,5,10,25,50".Split(','); for (int i = 0; i < array.Length; i++) { int item = Math.Min(int.Parse(array[i], CultureInfo.InvariantCulture), val); if (!list.Contains(item)) { list.Add(item); } } } return list; } } internal static class BulkBuyState { private const float HoverGraceSeconds = 0.5f; private static int _ladderIndex; private static bool _wasHovering; private static Purchasable _lastSeen; private static float _lastSeenTime = float.NegativeInfinity; public static Purchasable Hovered { get { Player localPlayer = Player.LocalPlayer; PlayerHolding val = (((Object)(object)localPlayer == (Object)null) ? null : localPlayer.Holding); Purchasable val2 = (Purchasable)(((Object)(object)val == (Object)null) ? null : /*isinst with value type is only supported in some contexts*/); if ((Object)(object)val2 != (Object)null) { _lastSeen = val2; _lastSeenTime = Time.time; return val2; } if ((Object)(object)_lastSeen != (Object)null && Time.time - _lastSeenTime <= 0.5f) { return _lastSeen; } _lastSeen = null; return null; } } public static int Quantity { get { IReadOnlyList ladder = Plugin.Settings.Ladder; if (ladder.Count == 0) { return 1; } return ladder[Mathf.Clamp(_ladderIndex, 0, ladder.Count - 1)]; } } public static void NoteHoverState(bool hovering) { if (_wasHovering && !hovering && Plugin.Settings.ResetOnShopClose.Value && _ladderIndex != 0) { _ladderIndex = 0; Plugin.Log.LogInfo((object)$"Vendor closed - quantity reset to x{Quantity}."); } _wasHovering = hovering; } public static void Cycle(bool forward) { IReadOnlyList ladder = Plugin.Settings.Ladder; if (ladder.Count != 0) { int count = ladder.Count; _ladderIndex = Mathf.Clamp(_ladderIndex, 0, count - 1); _ladderIndex = (forward ? ((_ladderIndex + 1) % count) : ((_ladderIndex - 1 + count) % count)); Plugin.Log.LogInfo((object)$"Bulk quantity -> x{Quantity}"); } } } internal static class BulkPurchaseRunner { private enum Confirm { None, HeldItem, VendorState } private readonly struct VendorSnapshot { private readonly Item _held; private readonly int _cost; private readonly bool _canBuy; private VendorSnapshot(Item held, int cost, bool canBuy) { _held = held; _cost = cost; _canBuy = canBuy; } public static VendorSnapshot Capture(Purchasable purchasable, Player player) { PlayerHolding val = (((Object)(object)player == (Object)null) ? null : player.Holding); return new VendorSnapshot(((Object)(object)val == (Object)null) ? null : val.HeldItem, purchasable._customCost, purchasable._customCanBuy); } public bool Matches(VendorSnapshot other, Confirm confirm) { switch (confirm) { case Confirm.HeldItem: return _held == other._held; case Confirm.VendorState: if (_cost == other._cost) { return _canBuy == other._canBuy; } return false; default: return true; } } } private const int StallTimeoutFrames = 90; public static bool Reentrant { get; private set; } public static bool IsRunning { get; private set; } public static int Progress { get; private set; } public static int Target { get; private set; } public static IEnumerator Run(Purchasable purchasable, Player player, int total) { IsRunning = true; Progress = 1; Target = total; AudioSuppressor.BeginBatch(); BaitToastBatcher.BeginBatch(); Confirm confirm = ConfirmFor(purchasable); int baseline = MoneyManager.Money; int committed = purchasable._customCost; int bought = 1; string stopReason = null; VendorSnapshot pending = VendorSnapshot.Capture(purchasable, player); Plugin.Log.LogInfo((object)($"Batch start: {((object)purchasable).GetType().Name} x{total} " + $"(first unit ${committed}, have ${baseline}, confirm by {confirm}).")); for (int i = 1; i < total; i++) { if (confirm == Confirm.None) { yield return (object)new WaitForFixedUpdate(); } else { bool advanced = false; for (int f = 0; f < 90; f++) { if (advanced) { break; } yield return (object)new WaitForFixedUpdate(); if ((Object)(object)purchasable == (Object)null || (Object)(object)player == (Object)null) { break; } TryRefresh(purchasable, ref stopReason); advanced = !VendorSnapshot.Capture(purchasable, player).Matches(pending, confirm); } if (!advanced) { stopReason = "the previous purchase did not land"; break; } } if ((Object)(object)purchasable == (Object)null) { stopReason = "the vendor went away"; break; } if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.LocalPlayer) { stopReason = "the player went away"; break; } if (!TryRefresh(purchasable, ref stopReason)) { break; } if (!purchasable._customCanBuy) { stopReason = "nothing left to buy here"; break; } int customCost = purchasable._customCost; if (customCost > 0 && baseline - committed < customCost) { stopReason = "out of money"; break; } pending = VendorSnapshot.Capture(purchasable, player); try { Reentrant = true; ((Interactable)purchasable).Interact(player); } catch (Exception arg) { stopReason = "an error occurred"; Plugin.Log.LogError((object)$"Batch aborted after {bought}/{total}: {arg}"); break; } finally { Reentrant = false; } committed += customCost; bought = (Progress = bought + 1); } RestoreHoverState(purchasable); AudioSuppressor.EndBatch(); BaitToastBatcher.EndBatch(); IsRunning = false; Progress = 0; Target = 0; yield return (object)new WaitForSeconds(0.5f); int num2 = baseline - MoneyManager.Money; if (stopReason == null) { Plugin.Log.LogInfo((object)$"Batch result: bought {bought} of {total}, spent ${num2}."); yield break; } Plugin.Log.LogInfo((object)$"Batch result: bought {bought} of {total}, spent ${num2} - {stopReason}."); } private static Confirm ConfirmFor(Purchasable purchasable) { if (purchasable is BaitPurchasable) { return Confirm.None; } if (purchasable is ItemPurchasable) { return Confirm.HeldItem; } return Confirm.VendorState; } private static bool TryRefresh(Purchasable purchasable, ref string stopReason) { try { ((Interactable)purchasable).Hover(); return true; } catch (Exception ex) { stopReason = "the vendor stopped responding"; Plugin.Log.LogDebug((object)("Hover refresh failed: " + ex.Message)); return false; } } private static void RestoreHoverState(Purchasable purchasable) { try { if (!((Object)(object)purchasable == (Object)null)) { Player localPlayer = Player.LocalPlayer; PlayerHolding val = (((Object)(object)localPlayer == (Object)null) ? null : localPlayer.Holding); if (!((Object)(object)val != (Object)null) || (object)val._interactable != purchasable) { ((Interactable)purchasable).UnHover(); } } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Could not restore hover state: " + ex.Message)); } } } internal sealed class Indicator { private const float FontSize = 26f; private static readonly Vector2 AnchoredPosition = new Vector2(0f, -120f); private TextMeshProUGUI _text; private string _shownText; private bool _loggedCreateFailure; private bool Ready => (Object)(object)_text != (Object)null; private bool EnsureCreated() { if (Ready) { return true; } try { return Create(); } catch (Exception ex) { if (!_loggedCreateFailure) { _loggedCreateFailure = true; Plugin.Log.LogDebug((object)("Indicator not ready yet, will retry: " + ex.Message)); } return false; } } private bool Create() { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI canvasTextPrefab = PlayerUI.CanvasTextPrefab; Transform fXCanvasTrans = PlayerUI.FXCanvasTrans; if ((Object)(object)canvasTextPrefab == (Object)null || (Object)(object)fXCanvasTrans == (Object)null) { return false; } _text = Object.Instantiate(canvasTextPrefab, fXCanvasTrans); ((Object)_text).name = "BulkBuyIndicator"; ((TMP_Text)_text).fontSize = 26f; ((TMP_Text)_text).alignment = (TextAlignmentOptions)514; ((Graphic)_text).color = GameInfo.GreenColor; ((Graphic)_text).raycastTarget = false; ((TMP_Text)_text).text = string.Empty; RectTransform rectTransform = ((TMP_Text)_text).rectTransform; ((Transform)rectTransform).localScale = Vector3.one; ((Transform)rectTransform).localRotation = Quaternion.identity; rectTransform.anchorMin = new Vector2(0.5f, 0.5f); rectTransform.anchorMax = new Vector2(0.5f, 0.5f); rectTransform.pivot = new Vector2(0.5f, 0.5f); rectTransform.anchoredPosition = AnchoredPosition; ((Component)_text).gameObject.SetActive(false); _loggedCreateFailure = false; Plugin.Log.LogDebug((object)"Indicator created."); return true; } public void Refresh(bool visible, int quantity, int progress, int target) { if (!visible) { if (Ready && ((Component)_text).gameObject.activeSelf) { ((Component)_text).gameObject.SetActive(false); _shownText = null; } } else if (EnsureCreated()) { string text = ((target > 0) ? $"Bulk: {progress}/{target}" : $"Bulk: x{quantity}"); if (_shownText != text) { ((TMP_Text)_text).text = text; _shownText = text; } if (!((Component)_text).gameObject.activeSelf) { ((Component)_text).gameObject.SetActive(true); } } } public void Destroy() { if ((Object)(object)_text != (Object)null) { Object.Destroy((Object)(object)((Component)_text).gameObject); _text = null; } _shownText = null; } } [HarmonyPatch] internal static class PurchasableInteractPatch { private static IEnumerable TargetMethods() { List list = new List(); Type[] types = typeof(Purchasable).Assembly.GetTypes(); foreach (Type type in types) { if (!type.IsAbstract && typeof(Purchasable).IsAssignableFrom(type)) { MethodInfo methodInfo = AccessTools.DeclaredMethod(type, "Interact", new Type[1] { typeof(Player) }, (Type[])null); if (methodInfo != null) { list.Add(methodInfo); } } } return list; } private static void Postfix(Purchasable __instance, Player player) { try { if (!BulkPurchaseRunner.Reentrant && !BulkPurchaseRunner.IsRunning) { int quantity = BulkBuyState.Quantity; if (quantity > 1 && !((Object)(object)__instance == (Object)null) && !((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.LocalPlayer) && !((Object)(object)Server.Instance == (Object)null) && __instance._customCanBuy && MoneyManager.CanAfford(__instance._customCost)) { Plugin.Instance.StartBatch(__instance, player, quantity); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Bulk purchase failed to start: {arg}"); } } } [HarmonyPatch(typeof(AudioManager), "PlayClip", new Type[] { typeof(string), typeof(Vector3), typeof(float), typeof(float), typeof(bool), typeof(AudioDistance), typeof(bool), typeof(string) })] internal static class AudioManagerPlayClipPatch { [HarmonyPrefix] private static bool Prefix(string clip, string cooldownKey) { try { return !AudioSuppressor.ShouldSuppress(string.IsNullOrEmpty(cooldownKey) ? clip : cooldownKey); } catch (Exception arg) { Plugin.Log.LogError((object)$"Audio suppression failed, passing through: {arg}"); return true; } } } [HarmonyPatch(typeof(PlayerUI), "OnBaitChange", new Type[] { typeof(int), typeof(bool) })] internal static class PlayerUIOnBaitChangePatch { [HarmonyPrefix] private static bool Prefix(int amount, bool increased) { try { return !BaitToastBatcher.TrySwallow(amount, increased); } catch (Exception arg) { Plugin.Log.LogError((object)$"Bait toast batching failed, passing through: {arg}"); return true; } } } [BepInPlugin("com.zoe.howtofish.bulkbuy", "BulkBuy", "1.2.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.zoe.howtofish.bulkbuy"; public const string PluginName = "BulkBuy"; public const string PluginVersion = "1.2.0"; private Harmony _harmony; private Indicator _indicator; private bool _loggedUpdateFailure; internal static ManualLogSource Log { get; private set; } internal static BulkBuyConfig Settings { get; private set; } internal static Plugin Instance { get; private set; } private void Awake() { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Settings = new BulkBuyConfig(((BaseUnityPlugin)this).Config); _indicator = new Indicator(); Log.LogInfo((object)("Quantity ladder: " + string.Join(", ", Settings.Ladder))); Log.LogInfo((object)($"Cycle key: {Settings.CycleKey.Value} (reverse: {Settings.ReverseModifier.Value}). " + "Q is left alone - the game binds it to 'drop held item'.")); _harmony = new Harmony("com.zoe.howtofish.bulkbuy"); _harmony.PatchAll(typeof(Plugin).Assembly); SceneManager.sceneUnloaded += OnSceneUnloaded; Log.LogInfo((object)"BulkBuy 1.2.0 loaded."); } private void OnDestroy() { SceneManager.sceneUnloaded -= OnSceneUnloaded; _indicator?.Destroy(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } Instance = null; } private void OnSceneUnloaded(Scene scene) { ((MonoBehaviour)this).StopAllCoroutines(); _indicator?.Destroy(); BaitToastBatcher.Reset(); AudioSuppressor.EndBatch(); } internal void StartBatch(Purchasable purchasable, Player player, int quantity) { ((MonoBehaviour)this).StartCoroutine(BulkPurchaseRunner.Run(purchasable, player, quantity)); } private void Update() { try { Tick(); } catch (Exception arg) { if (!_loggedUpdateFailure) { _loggedUpdateFailure = true; Log.LogError((object)$"Update failed (further occurrences suppressed): {arg}"); } } } private void Tick() { BaitToastBatcher.Tick(); bool num = (Object)(object)BulkBuyState.Hovered != (Object)null && CanTakeInput(); BulkBuyState.NoteHoverState(num); if (num && !BulkPurchaseRunner.IsRunning) { HandleCycleInput(); } bool visible = (num || BulkPurchaseRunner.IsRunning) && Settings.ShowIndicator.Value; _indicator.Refresh(visible, BulkBuyState.Quantity, BulkPurchaseRunner.IsRunning ? BulkPurchaseRunner.Progress : 0, BulkPurchaseRunner.IsRunning ? BulkPurchaseRunner.Target : 0); } private static bool CanTakeInput() { Player localPlayer = Player.LocalPlayer; if ((Object)(object)localPlayer != (Object)null && Player.LocalPlayerEnabled && !localPlayer.BlockInputs) { return !PauseManager.IsPaused; } return false; } private static void HandleCycleInput() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = Settings.ReverseModifier.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { BulkBuyState.Cycle(forward: false); return; } value = Settings.CycleKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { BulkBuyState.Cycle(forward: true); } } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }