using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using TMPro; using Unity.Netcode; using UnityEngine; [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("SmartSeller")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("SmartSeller")] [assembly: AssemblyTitle("SmartSeller")] [assembly: AssemblyVersion("1.0.0.0")] namespace SmartSeller; [BepInPlugin("lucas.smartseller", "Smart Seller", "2.0.0")] public class SmartSellerPlugin : BaseUnityPlugin { private readonly Harmony harmony = new Harmony("lucas.smartseller"); internal static Func ShouldProtectUtilityItems = () => true; private void Awake() { ConfigEntry protectUtilityItemsConfig = ((BaseUnityPlugin)this).Config.Bind("Whitelist", "WhitelistUtilityItems", true, "When enabled, Smart Seller will ignore kitchen knives, shotguns, shotgun shells, and keys."); ShouldProtectUtilityItems = () => protectUtilityItemsConfig.Value; harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Smart Seller 1.0.10 loaded."); } } [HarmonyPatch(typeof(Terminal), "QuitTerminal")] public class TerminalQuitPatch { private static void Postfix() { TerminalPatch.CancelPendingSellFromTerminalExit(); } } [HarmonyPatch(typeof(Terminal), "LoadNewNode")] public class TerminalHelpPatch { private static void Prefix(TerminalNode __0) { TerminalPatch.TryAppendSmartSellerHelpToNode(__0); } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] public class TerminalPatch { private class ItemValue { public GrabbableObject Item; public int EffectiveValue; public int RawValue; } private class CombinationResult { public List Items = new List(); public int Total; public int RawTotal; } private static List pendingSellItems = new List(); private static int pendingSellTarget = 0; private static int pendingSellTotal = 0; private static string pendingSellMode = ""; private static bool Prefix(Terminal __instance, ref TerminalNode __result, int ___textAdded, TMP_InputField ___screenText) { string text; try { if (___screenText == null) { return true; } if (___textAdded <= 0) { return true; } int num = ___screenText.text.Length - ___textAdded; if (num < 0) { return true; } text = ___screenText.text.Substring(num).Trim().ToLower(); } catch { return true; } if (HasPendingSell()) { if (text == "c" || text == "confirm") { __result = CreateTerminalNode(ConfirmPendingSell()); return false; } if (text == "d" || text == "deny" || text == "cancel") { __result = CreateTerminalNode(DenyPendingSell()); return false; } __result = CreateTerminalNode("Pending sell active.\nPress C to confirm sell or D to deny sell.\n\n"); return false; } if (text == "sell" || text.StartsWith("sell ")) { string message = HandleSellCommand(text); __result = CreateTerminalNode(message); return false; } return true; } public static void TryAppendSmartSellerHelpToNode(TerminalNode node) { try { if (node != null && !string.IsNullOrEmpty(node.displayText)) { string displayText = node.displayText; string text = displayText.ToLower(); if (!text.Contains("smart seller") && text.Contains("store") && text.Contains("moons") && text.Contains("bestiary") && text.Contains("storage")) { string text2 = "\n\n>SMART SELLER\nsell , sell , sell \nExamples: sell quota, sell max, sell 500\n"; node.displayText = displayText.TrimEnd() + text2; } } } catch { } } public static void CancelPendingSellFromTerminalExit() { if (HasPendingSell()) { ClearPendingSell(); } } private static string HandleSellCommand(string text) { if (!IsAtCompany()) { return "You must be at the Company Building.\n\n"; } string[] array = text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2) { return "Usage: sell , sell quota, sell min, or sell max\n\n"; } string argument = array[1].ToLower(); return PrepareSell(argument); } private static TerminalNode CreateTerminalNode(string message) { TerminalNode val = ScriptableObject.CreateInstance(); val.displayText = message; val.clearPreviousText = true; return val; } private static bool IsAtCompany() { try { if ((Object)(object)StartOfRound.Instance == (Object)null) { return false; } if ((Object)(object)StartOfRound.Instance.currentLevel == (Object)null) { return false; } SelectableLevel currentLevel = StartOfRound.Instance.currentLevel; if (currentLevel.levelID == 3) { return true; } string planetName = currentLevel.PlanetName; if (!string.IsNullOrEmpty(planetName)) { string text = planetName.ToLower(); if (text.Contains("company")) { return true; } if (text.Contains("gordion")) { return true; } } return false; } catch { return false; } } private static string PrepareSell(string argument) { float currentSellMultiplier = GetCurrentSellMultiplier(); List list = (from x in Object.FindObjectsOfType() where (Object)(object)x != (Object)null && x.isInShipRoom && x.scrapValue > 0 && !x.isHeld select x).ToList(); if (list.Count == 0) { return "No scrap found.\n\n"; } List list2 = list.Where((GrabbableObject x) => !IsProtectedUtilityItem(x)).ToList(); if (list2.Count == 0) { if (SmartSellerPlugin.ShouldProtectUtilityItems()) { return "No eligible scrap found.\nUtility item whitelist is enabled, so knives, shotguns, shells, and keys were ignored.\n\n"; } return "No eligible scrap found.\n\n"; } int result = 0; List list3 = new List(); if (argument == "quota" || argument == "min") { result = GetQuotaRemaining(); if (result <= 0) { return "Quota is already fulfilled.\n\n"; } list3 = FindBestCombination(list2, result, currentSellMultiplier, preferAtLeastTarget: true); } else if (argument == "max") { list3 = list2; result = CalculateTotal(list3, currentSellMultiplier); } else { if (!int.TryParse(argument, out result)) { return "Invalid command. Use: sell , sell quota, sell min, or sell max\n\n"; } if (result <= 0) { return "Sell amount must be higher than 0.\n\n"; } list3 = FindBestCombination(list2, result, currentSellMultiplier, preferAtLeastTarget: false); } if (list3.Count == 0) { return "Could not find any matching scrap.\n\n"; } int num = CalculateTotal(list3, currentSellMultiplier); SetPendingSell(list3, result, num, argument); string text = ""; foreach (GrabbableObject item in list3) { int num2 = Mathf.RoundToInt((float)item.scrapValue * currentSellMultiplier); string itemName = GetItemName(item); text = text + "Selected: " + itemName + " ($" + num2 + ")\n"; } text = text + "\nTarget: $" + result + " | Selected total: $" + num + "\n"; text += BuildProfitPreview(num); return text + "\nC to confirm sell | D to deny sell\n\n"; } private static string ConfirmPendingSell() { if (!HasPendingSell()) { return "No pending sell to confirm.\n\n"; } if (!IsAtCompany()) { ClearPendingSell(); return "Sell cancelled. You are no longer at the Company Building.\n\n"; } if ((Object)(object)NetworkManager.Singleton == (Object)null) { return "Could not access NetworkManager. Sell was not confirmed.\n\n"; } if (!NetworkManager.Singleton.IsHost && !NetworkManager.Singleton.IsServer) { return "Only the host can confirm automatic selling in this version.\n\n"; } DepositItemsDesk val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { return "Could not find the Company sell desk. Sell was not confirmed.\n\n"; } pendingSellItems.RemoveAll((GrabbableObject x) => (Object)(object)x == (Object)null || x.scrapValue <= 0 || x.isHeld || IsProtectedUtilityItem(x)); if (pendingSellItems.Count == 0) { ClearPendingSell(); return "Pending sell expired. No valid scrap left to sell.\n\n"; } try { if (val.itemsOnCounter == null) { return "Could not access the Company counter list. Sell was not confirmed.\n\n"; } val.itemsOnCounter.RemoveAll((GrabbableObject x) => (Object)(object)x == (Object)null); if (val.itemsOnCounter.Count > 0) { return "The Company counter already has items on it. Clear the counter first, then confirm again.\n\n"; } for (int num = 0; num < pendingSellItems.Count; num++) { GrabbableObject item = pendingSellItems[num]; PrepareItemForCompanyDesk(item, val, num); if (!val.itemsOnCounter.Contains(item)) { val.itemsOnCounter.Add(item); } } List list = new List(pendingSellItems); int sellValue = CalculateTotal(list, GetCurrentSellMultiplier()); int count = list.Count; string text = BuildProfitPreview(sellValue); val.SellItemsOnServer(); RemoveSoldItemsFromWorld(list); val.itemsOnCounter.RemoveAll((GrabbableObject x) => (Object)(object)x == (Object)null); ClearPendingSell(); return "Confirmed sell.\nSold " + count + " selected item(s).\nTotal value: $" + sellValue + "\n" + text + "\n"; } catch (Exception ex) { return "Sell failed before confirmation finished.\nError: " + ex.Message + "\n\n"; } } private static string DenyPendingSell() { ClearPendingSell(); return "Sell denied. Returning to terminal.\n\n"; } private static void PrepareItemForCompanyDesk(GrabbableObject item, DepositItemsDesk desk, int index) { if (!((Object)(object)item == (Object)null)) { item.isInShipRoom = false; item.isInElevator = false; item.hasHitGround = true; ((Component)item).gameObject.SetActive(true); } } private static void RemoveSoldItemsFromWorld(List soldItems) { if (soldItems == null) { return; } for (int i = 0; i < soldItems.Count; i++) { GrabbableObject val = soldItems[i]; if ((Object)(object)val == (Object)null) { continue; } try { val.isInShipRoom = false; val.isInElevator = false; val.scrapValue = 0; val.SetScrapValue(0); } catch { } try { NetworkObject component = ((Component)val).GetComponent(); if ((Object)(object)component != (Object)null && component.IsSpawned) { component.Despawn(true); continue; } } catch { } try { ((Component)val).gameObject.SetActive(false); Object.Destroy((Object)(object)((Component)val).gameObject); } catch { } } } private static bool HasPendingSell() { return pendingSellItems != null && pendingSellItems.Count > 0; } private static void SetPendingSell(List items, int target, int total, string mode) { pendingSellItems = new List(items); pendingSellTarget = target; pendingSellTotal = total; pendingSellMode = mode; } private static void ClearPendingSell() { pendingSellItems.Clear(); pendingSellTarget = 0; pendingSellTotal = 0; pendingSellMode = ""; } private static int GetQuotaRemaining() { try { int num = TimeOfDay.Instance.profitQuota - TimeOfDay.Instance.quotaFulfilled; if (num < 0) { num = 0; } return num; } catch { return 0; } } private static int GetQuotaFulfilled() { try { return TimeOfDay.Instance.quotaFulfilled; } catch { return 0; } } private static int GetProfitQuota() { try { return TimeOfDay.Instance.profitQuota; } catch { return 0; } } private static int GetDaysUntilDeadline() { try { return TimeOfDay.Instance.daysUntilDeadline; } catch { return 0; } } private static string BuildProfitPreview(int sellValue) { try { int quotaFulfilled = GetQuotaFulfilled(); int profitQuota = GetProfitQuota(); int daysUntilDeadline = GetDaysUntilDeadline(); if (profitQuota <= 0) { return "Profit bonus preview unavailable.\n"; } int num = quotaFulfilled + sellValue; int num2 = CalculateProfitBonus(quotaFulfilled, profitQuota, daysUntilDeadline); int num3 = CalculateProfitBonus(num, profitQuota, daysUntilDeadline); int num4 = num3 - num2; if (num4 < 0) { num4 = 0; } int num5 = sellValue + num4; int num6 = num + num3; string text = ""; text += "\nProfit quota preview:\n"; text = text + "Quota after sell: $" + num + " / $" + profitQuota + "\n"; if (num < profitQuota) { text = text + "Still needed for quota: $" + (profitQuota - num) + "\n"; text += "Profit bonus after sell: $0\n"; return text + "Expected value from this sell: $" + sellValue + "\n"; } int num7 = num - profitQuota; int value = 15 * daysUntilDeadline; text = text + "Over quota by: $" + num7 + "\n"; text = text + "Day efficiency modifier: " + FormatSignedMoney(value) + "\n"; text = text + "Profit bonus after sell: $" + num3 + "\n"; text = text + "Bonus added by this sell: +" + num4 + "\n"; text = text + "Expected value from this sell: $" + sellValue + " + $" + num4 + " = $" + num5 + "\n"; return text + "Total quota profit after sell: $" + num6 + "\n"; } catch { return "Profit bonus preview unavailable.\n"; } } private static string FormatSignedMoney(int value) { if (value > 0) { return "+$" + value; } if (value < 0) { return "-$" + Math.Abs(value); } return "$0"; } private static int CalculateProfitBonus(int quotaFulfilledValue, int profitQuota, int daysUntilDeadline) { if (quotaFulfilledValue < profitQuota) { return 0; } int num = quotaFulfilledValue - profitQuota; float num2 = (float)num / 5f + 15f * (float)daysUntilDeadline; int num3 = Mathf.FloorToInt(num2); if (num3 < 0) { num3 = 0; } return num3; } private static float GetCurrentSellMultiplier() { try { if ((Object)(object)StartOfRound.Instance != (Object)null) { return StartOfRound.Instance.companyBuyingRate; } } catch { } try { return TimeOfDay.Instance.daysUntilDeadline switch { 0 => 1f, 1 => 0.77f, 2 => 0.55f, _ => 0.33f, }; } catch { return 1f; } } private static int CalculateTotal(List items, float multiplier) { int num = 0; for (int i = 0; i < items.Count; i++) { if (!((Object)(object)items[i] == (Object)null)) { num += Mathf.RoundToInt((float)items[i].scrapValue * multiplier); } } return num; } private static List FindBestCombination(List items, int target, float multiplier, bool preferAtLeastTarget) { List list = new List(); for (int i = 0; i < items.Count; i++) { GrabbableObject val = items[i]; if (!((Object)(object)val == (Object)null)) { int num = Mathf.RoundToInt((float)val.scrapValue * multiplier); if (num > 0) { ItemValue itemValue = new ItemValue(); itemValue.Item = val; itemValue.EffectiveValue = num; itemValue.RawValue = val.scrapValue; list.Add(itemValue); } } } list = (from x in list orderby x.EffectiveValue, GetItemName(x.Item) select x).ToList(); if (list.Count == 0) { return new List(); } int num2 = 0; int num3 = 0; for (int num4 = 0; num4 < list.Count; num4++) { num2 += list[num4].EffectiveValue; if (list[num4].EffectiveValue > num3) { num3 = list[num4].EffectiveValue; } } int num5 = num2; if (target > 0 && num3 > 0) { num5 = Math.Min(num2, target + num3); } Dictionary dictionary = new Dictionary(); CombinationResult combinationResult = new CombinationResult(); combinationResult.Total = 0; combinationResult.RawTotal = 0; dictionary[0] = combinationResult; for (int num6 = 0; num6 < list.Count; num6++) { ItemValue itemValue2 = list[num6]; List> list2 = dictionary.ToList(); for (int num7 = 0; num7 < list2.Count; num7++) { int key = list2[num7].Key; CombinationResult value = list2[num7].Value; int num8 = key + itemValue2.EffectiveValue; if (num8 <= num5) { CombinationResult combinationResult2 = new CombinationResult(); combinationResult2.Total = num8; combinationResult2.RawTotal = value.RawTotal + itemValue2.RawValue; combinationResult2.Items = new List(value.Items); combinationResult2.Items.Add(itemValue2.Item); if (!dictionary.TryGetValue(num8, out var value2)) { dictionary[num8] = combinationResult2; } else if (IsBetterSameTotalCombination(combinationResult2, value2)) { dictionary[num8] = combinationResult2; } } } } CombinationResult combinationResult3 = null; foreach (CombinationResult value3 in dictionary.Values) { if (value3 != null && value3.Total > 0 && (combinationResult3 == null || IsBetterCandidate(value3, combinationResult3, target, preferAtLeastTarget))) { combinationResult3 = value3; } } if (combinationResult3 == null) { return new List(); } return combinationResult3.Items; } private static bool IsBetterSameTotalCombination(CombinationResult challenger, CombinationResult current) { if (challenger.Items.Count != current.Items.Count) { return challenger.Items.Count < current.Items.Count; } if (challenger.RawTotal != current.RawTotal) { return challenger.RawTotal < current.RawTotal; } return false; } private static bool IsBetterCandidate(CombinationResult challenger, CombinationResult current, int target, bool preferAtLeastTarget) { if (preferAtLeastTarget) { bool flag = challenger.Total >= target; bool flag2 = current.Total >= target; if (flag && !flag2) { return true; } if (!flag && flag2) { return false; } if (flag && flag2) { if (challenger.Total != current.Total) { return challenger.Total < current.Total; } } else if (challenger.Total != current.Total) { return challenger.Total > current.Total; } return IsBetterSameTotalCombination(challenger, current); } int num = Math.Abs(target - challenger.Total); int num2 = Math.Abs(target - current.Total); if (num != num2) { return num < num2; } if (challenger.Total != current.Total) { return challenger.Total < current.Total; } return IsBetterSameTotalCombination(challenger, current); } private static bool IsProtectedUtilityItem(GrabbableObject item) { try { if (!SmartSellerPlugin.ShouldProtectUtilityItems()) { return false; } string text = GetItemName(item).ToLower().Replace("-", " ").Replace("_", " ") .Trim(); if (string.IsNullOrEmpty(text)) { return false; } if (text.Contains("kitchen") && text.Contains("knife")) { return true; } if (text == "knife" || text.Contains(" knife") || text.Contains("knife ")) { return true; } if (text.Contains("shotgun")) { return true; } if (text.Contains("shell") && text.Contains("shotgun")) { return true; } if (text == "key" || text.EndsWith(" key") || text.Contains(" key ")) { return true; } return false; } catch { return false; } } private static string GetItemName(GrabbableObject item) { try { if ((Object)(object)item != (Object)null && (Object)(object)item.itemProperties != (Object)null && !string.IsNullOrEmpty(item.itemProperties.itemName)) { return item.itemProperties.itemName; } } catch { } return "Unknown item"; } }