Decompiled source of SmartSeller v2.0.0

SmartSeller.dll

Decompiled 13 hours ago
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<bool> ShouldProtectUtilityItems = () => true;

	private void Awake()
	{
		ConfigEntry<bool> protectUtilityItemsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("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<GrabbableObject> Items = new List<GrabbableObject>();

		public int Total;

		public int RawTotal;
	}

	private static List<GrabbableObject> pendingSellItems = new List<GrabbableObject>();

	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 <quota>, sell <max>, sell <value>\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 <amount>, 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<TerminalNode>();
		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<GrabbableObject> list = (from x in Object.FindObjectsOfType<GrabbableObject>()
			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<GrabbableObject> 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<GrabbableObject> list3 = new List<GrabbableObject>();
		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 <amount>, 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<DepositItemsDesk>();
		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<GrabbableObject> list = new List<GrabbableObject>(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<GrabbableObject> 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<NetworkObject>();
				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<GrabbableObject> items, int target, int total, string mode)
	{
		pendingSellItems = new List<GrabbableObject>(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<GrabbableObject> 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<GrabbableObject> FindBestCombination(List<GrabbableObject> items, int target, float multiplier, bool preferAtLeastTarget)
	{
		List<ItemValue> list = new List<ItemValue>();
		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<GrabbableObject>();
		}
		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<int, CombinationResult> dictionary = new Dictionary<int, CombinationResult>();
		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<KeyValuePair<int, CombinationResult>> 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<GrabbableObject>(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<GrabbableObject>();
		}
		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";
	}
}