Decompiled source of EpicLootSacrificeRecycler v1.4.3

BepInEx/plugins/EpicLootSacrificeRecycler/EpicLootSacrificeRecycler.dll

Decompiled 4 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("EpicLootSacrificeRecycler")]
[assembly: AssemblyDescription("Low-loss Epic Loot enchanting, guaranteed tempering and equipment recycling")]
[assembly: AssemblyCompany("ArcByte Software")]
[assembly: AssemblyFileVersion("1.4.3.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.4.3.0")]
[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 ArcByte.EpicLootSacrificeRecycler
{
	internal static class ConfigMigration
	{
		internal static bool CopyIfMissing(string legacyPath, string currentPath, Action<string> warn)
		{
			if (File.Exists(currentPath) || !File.Exists(legacyPath))
			{
				return false;
			}
			try
			{
				File.Copy(legacyPath, currentPath, overwrite: false);
				return true;
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				warn("Could not import legacy Recycler settings. Original config was kept: " + ex.Message);
				return false;
			}
		}
	}
	public sealed class ResourceCosts
	{
		public int Materials = 100;

		public int Gold = 100;

		public int Runes = 100;

		public int Shards = 100;

		public int Tokens = 100;

		public string GoldPrefabs = "Coins,Gold";

		public string RunePrefabs = "RunestoneMagic,RunestoneRare,RunestoneEpic,RunestoneLegendary,RunestoneMythic,RunestoneAncient";

		public string ShardPrefabs = "ShardMagic,ShardRare,ShardEpic,ShardLegendary,ShardMythic,ShardAncient";

		public string TokenPrefabs = "ForestToken,IronBountyToken,GoldBountyToken";

		public int Percent(string prefab)
		{
			if (Contains(GoldPrefabs, prefab))
			{
				return Gold;
			}
			if (Contains(RunePrefabs, prefab))
			{
				return Runes;
			}
			if (Contains(ShardPrefabs, prefab))
			{
				return Shards;
			}
			if (Contains(TokenPrefabs, prefab))
			{
				return Tokens;
			}
			return Materials;
		}

		private static bool Contains(string names, string prefab)
		{
			if (string.IsNullOrWhiteSpace(prefab))
			{
				throw new ArgumentException("Missing cost prefab");
			}
			string[] array = (names ?? "").Split(new char[1] { ',' });
			foreach (string text in array)
			{
				if (string.Equals(text.Trim(), prefab, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			return false;
		}
	}
	public static class CostPolicy
	{
		public static int Scale(int amount, int percent)
		{
			return Scale(amount, percent, 100);
		}

		public static int Scale(int amount, int operationPercent, int resourcePercent)
		{
			if (amount < 0 || operationPercent < 0 || operationPercent > 100 || resourcePercent < 0 || resourcePercent > 100)
			{
				throw new ArgumentOutOfRangeException();
			}
			checked
			{
				return (int)unchecked(checked(unchecked((long)amount) * unchecked((long)operationPercent) * resourcePercent + 9999) / 10000);
			}
		}

		public static object CopyCosts(object source, string amountMember, int percent)
		{
			return CopyCosts(source, amountMember, percent, null, null);
		}

		public static object CopyCosts(object source, string amountMember, int percent, string prefabMember, ResourceCosts resources)
		{
			if (source == null)
			{
				return null;
			}
			Type type = source.GetType();
			Type type2 = (type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]);
			IList list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(type2));
			foreach (object item in (IEnumerable)source)
			{
				int resourcePercent = resources?.Percent(Convert.ToString(Reflect.Get(item, prefabMember))) ?? 100;
				int num = Scale(Convert.ToInt32(Reflect.Get(item, amountMember)), percent, resourcePercent);
				if (num != 0)
				{
					object obj = typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Invoke(item, null);
					Reflect.Set(obj, amountMember, num);
					list.Add(obj);
				}
			}
			if (!type.IsArray)
			{
				return list;
			}
			Array array = Array.CreateInstance(type2, list.Count);
			list.CopyTo(array, 0);
			return array;
		}
	}
	internal static class EpicLootCompatibility
	{
		internal static MethodInfo SacrificeItems(Type controller, Type listElement)
		{
			Type result = typeof(List<>).MakeGenericType(listElement);
			MethodInfo[] source = (from m in controller.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
				where m.Name == "GetSacrificeItems" && m.IsStatic && !m.IsGenericMethod && m.ReturnType == result
				select m).ToArray();
			Type category = controller.GetNestedType("SacrificeCategory", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (category != null && category.IsEnum)
			{
				MethodInfo[] array = source.Where((MethodInfo m) => m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == category).ToArray();
				if (array.Length == 1)
				{
					return array[0];
				}
			}
			MethodInfo[] array2 = source.Where((MethodInfo m) => m.GetParameters().Length == 0).ToArray();
			if (array2.Length == 1)
			{
				return array2[0];
			}
			throw new MissingMethodException(controller.FullName, "GetSacrificeItems() or GetSacrificeItems(SacrificeCategory) returning List<InventoryItemListElement>");
		}
	}
	[BepInPlugin("arcbyte.epicloot.sacrificerecycler", "EpicLootSacrificeRecycler", "1.4.3")]
	[BepInDependency("randyknapp.mods.epicloot", "0.15.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class DisenchantState
		{
			public int Depth;

			public object Item;

			public object Rows;
		}

		private struct TransactionState
		{
			public int Depth;

			public bool Refunded;
		}

		public const string Guid = "arcbyte.epicloot.sacrificerecycler";

		public const string Version = "1.4.3";

		private const string LegacyGuid = "arcbyte.epicloot.salvagerecycler";

		private static Plugin self;

		private static RefundEngine engine;

		private static ConfigEntry<bool> modEnabled;

		private static ConfigEntry<bool> hoverPreview;

		private static ConfigEntry<int> refundMultiplier;

		private static ConfigEntry<int> rerollCredits;

		private static ConfigEntry<int> temperCredits;

		private static ConfigEntry<bool> guaranteedTemper;

		private static ConfigEntry<bool> removeSlotFee;

		private static ConfigEntry<int> enchantPercent;

		private static ConfigEntry<int> augmentPercent;

		private static ConfigEntry<int> temperPercent;

		private static ConfigEntry<int> identifyPercent;

		private static ConfigEntry<int> disenchantPercent;

		private static ConfigEntry<int> runePercent;

		private static ConfigEntry<int> materialsPercent;

		private static ConfigEntry<int> goldPercent;

		private static ConfigEntry<int> runesPercent;

		private static ConfigEntry<int> shardsPercent;

		private static ConfigEntry<int> tokensPercent;

		private static ConfigEntry<string> goldPrefabs;

		private static ConfigEntry<string> runePrefabs;

		private static ConfigEntry<string> shardPrefabs;

		private static ConfigEntry<string> tokenPrefabs;

		private static Type itemType;

		private static Type elementType;

		private static MethodInfo magicItem;

		private static MethodInfo reclaimSockets;

		private static object currentUI;

		private Harmony harmony;

		private readonly List<MethodInfo> patchedTargets = new List<MethodInfo>();

		private static readonly HashSet<string> Errors = new HashSet<string>();

		[ThreadStatic]
		private static int sacrificeDepth;

		[ThreadStatic]
		private static int previewDepth;

		[ThreadStatic]
		private static int listDepth;

		[ThreadStatic]
		private static bool refundedInBatch;

		[ThreadStatic]
		private static int disenchantDepth;

		internal static RefundEngine Engine => engine;

		internal static bool InSacrifice => Active && sacrificeDepth > 0;

		internal static bool Active => engine != null && modEnabled.Value;

		internal static object GetMagic(object item)
		{
			return magicItem.Invoke(null, new object[1] { item });
		}

		internal static IEnumerable GetSockets(object item)
		{
			return Sockets(item);
		}

		internal static void Report(string message, Exception ex)
		{
			Error(message, ex);
		}

		private void Awake()
		{
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Expected O, but got Unknown
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Expected O, but got Unknown
			//IL_0194: Unknown result type (might be due to invalid IL or missing references)
			//IL_019e: Expected O, but got Unknown
			//IL_0359: Unknown result type (might be due to invalid IL or missing references)
			//IL_0363: Expected O, but got Unknown
			self = this;
			if (Chainloader.PluginInfos.ContainsKey("arcbyte.epicloot.salvagerecycler"))
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Remove EpicLootSalvageRecycler.dll before installing EpicLootSacrificeRecycler.dll. The renamed plugin is inactive to avoid running both refund implementations.");
				return;
			}
			if (Chainloader.PluginInfos.ContainsKey("arcbyte.epicloot.sacrificerefund"))
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Remove the old ArcByteSacrificeRefund.dll before installing EpicLootSacrificeRecycler. Recycler is inactive to avoid conflicting refund hooks.");
				return;
			}
			string legacyPath = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Config.ConfigFilePath), "arcbyte.epicloot.salvagerecycler.cfg");
			if (ConfigMigration.CopyIfMissing(legacyPath, ((BaseUnityPlugin)this).Config.ConfigFilePath, delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
			}))
			{
				((BaseUnityPlugin)this).Config.Reload();
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Imported existing Recycler settings into " + ((BaseUnityPlugin)this).Config.ConfigFilePath + ". Original config kept as backup."));
			}
			modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Return crafting, all quality-upgrade and current-rarity enchanting recipe costs from equipment sacrifices.");
			hoverPreview = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Hover preview", true, "Show full refund details in equipment tooltips while the Sacrifice tab is open.");
			refundMultiplier = ((BaseUnityPlugin)this).Config.Bind<int>("Refunds", "Refund multiplier", 1, new ConfigDescription("Multiply material/currency refunds, including modification credits. 1 = full recipe value; 2 or 3 = more generous recycling for short playthroughs. Socket contents are NOT multiplied.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10), Array.Empty<object>()));
			rerollCredits = ((BaseUnityPlugin)this).Config.Bind<int>("Refunds", "Reroll credits per marked effect", 1, new ConfigDescription("Standard augment-cost credits for EACH recorded augmented effect. 1 credits one attempt; higher values approximate repeated rolls. Epic Loot does not store attempt counts. 0 disables this credit.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			temperCredits = ((BaseUnityPlugin)this).Config.Bind<int>("Refunds", "Temper credits per marked effect", 1, new ConfigDescription("Current temper-cost credits for EACH effect with a surviving successful-temper marker. Failed/unmarked/erased attempts cannot be detected. Higher values are an explicit catch-up credit; 0 disables this credit.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			guaranteedTemper = ((BaseUnityPlugin)this).Config.Bind<bool>("Enchanting", "Guaranteed temper success", true, "Valid temper attempts always succeed. Native eligibility and critical success remain. Restart after changing settings.");
			removeSlotFee = ((BaseUnityPlugin)this).Config.Bind<bool>("Enchanting", "Remove additional augment fee", true, "Remove the escalating fee for augmenting additional effect slots.");
			enchantPercent = CostSetting("Enchant cost percent", 100, "Initial enchant recipe; recovered on equipment sacrifice.");
			augmentPercent = CostSetting("Augment cost percent", 50, "Reroll materials and currency. Multiplied by each resource group's percentage.");
			temperPercent = CostSetting("Temper cost percent", 0, "Tempering materials and currency.");
			identifyPercent = CostSetting("Identify cost percent", 0, "Identification materials and currency.");
			disenchantPercent = CostSetting("Disenchant cost percent", 0, "Disenchant service fee.");
			runePercent = CostSetting("Rune service cost percent", 0, "Rune extraction and etching service materials. Native source rune consumption remains.");
			ResourceCosts resourceCosts = new ResourceCosts();
			materialsPercent = ResourceSetting("Material cost percent");
			goldPercent = ResourceSetting("Gold cost percent");
			runesPercent = ResourceSetting("Rune cost percent");
			shardsPercent = ResourceSetting("Shard cost percent");
			tokensPercent = ResourceSetting("Token cost percent");
			goldPrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("Resources", "Gold prefabs", resourceCosts.GoldPrefabs, "Comma-separated exact prefab names in the gold group.");
			runePrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("Resources", "Rune prefabs", resourceCosts.RunePrefabs, "Comma-separated exact prefab names in the rune group. Service ingredients only; source rune consumption remains native.");
			shardPrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("Resources", "Shard prefabs", resourceCosts.ShardPrefabs, "Comma-separated exact prefab names in the shard group.");
			tokenPrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("Resources", "Token prefabs", resourceCosts.TokenPrefabs, "Comma-separated exact prefab names in the token group. All unlisted prefabs use the material group. Overlap precedence: gold, rune, shard, token, material.");
			harmony = new Harmony("arcbyte.epicloot.sacrificerecycler");
			try
			{
				Type type = NeedType("EpicLoot.Crafting.EnchantCostsHelper");
				itemType = NeedType("ItemDrop+ItemData");
				Type type2 = NeedType("EpicLoot_UnityLib.SacrificeUI");
				Type type3 = NeedType("EpicLoot.CraftingV2.EnchantingUIController");
				Type type4 = NeedType("EpicLoot_UnityLib.EnchantingTable");
				Type type5 = NeedType("EpicLoot.ItemDataExtensions");
				Type type6 = NeedType("EpicLoot_UnityLib.EnchantingFeature");
				elementType = NeedType("EpicLoot_UnityLib.IListElement");
				engine = new RefundEngine(type.Assembly, NeedType("ObjectDB"), itemType, delegate(string m)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)m);
				}, CurrentSettings);
				magicItem = RefundEngine.RequireMethod(type5, "GetMagicItem", itemType);
				reclaimSockets = type3.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "ReclaimSockets" && m.GetParameters().Length == 1);
				MethodInfo target = RefundEngine.RequireMethod(type, "GetSacrificeProducts", itemType);
				MethodInfo target2 = RefundEngine.RequireMethod(type2, "SacrificeItems");
				MethodInfo target3 = RefundEngine.RequireMethod(type2, "OnSelectedItemsChanged");
				MethodInfo target4 = RefundEngine.RequireMethod(type4, "GetFeatureCurrentValue", type6);
				MethodInfo target5 = type3.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetSacrificeProducts" && m.GetParameters().Length == 1);
				MethodInfo target6 = EpicLootCompatibility.SacrificeItems(type3, NeedType("EpicLoot_UnityLib.InventoryItemListElement"));
				Patch(target, null, "ProductsPostfix");
				Patch(target2, "SacrificePrefix", null, "SacrificeFinalizer");
				Patch(target4, null, "FeaturePostfix");
				Patch(target3, "SelectionPrefix", null, "SelectionFinalizer");
				Patch(target5, null, "PreviewSocketsPostfix");
				Patch(target6, "ListPrefix", null, "ListFinalizer");
				Patch(RefundEngine.RequireMethod(type2, "OnEnable"), null, "UiEnabledPostfix");
				InstallEnchantingHooks(type, type3);
				Patch(type3.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "DisenchantItem"), "DisenchantPrefix", "DisenchantPostfix", "DisenchantFinalizer");
				MethodInfo methodInfo = itemType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.IsStatic && m.Name == "GetTooltip" && m.ReturnType == typeof(string) && m.GetParameters().Length != 0 && m.GetParameters()[0].ParameterType == itemType);
				if (methodInfo != null)
				{
					Patch(methodInfo, null, "TooltipPostfix");
				}
				else
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Hover tooltip hook unavailable; use the normal selected-item returns preview.");
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)("EpicLootSacrificeRecycler enabled. Craft cost + quality steps + current-rarity enchant cost + configured modification credits. Material multiplier: " + refundMultiplier.Value + ". Double-yield disabled for refund batches."));
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Local enchanting: guaranteed temper=" + guaranteedTemper.Value + "; cost percentages enchant/augment/temper/identify/disenchant/rune=" + enchantPercent.Value + "/" + augmentPercent.Value + "/" + temperPercent.Value + "/" + identifyPercent.Value + "/" + disenchantPercent.Value + "/" + runePercent.Value + ". No server installation required for this add-on."));
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Local resource percentages material/gold/rune/shard/token=" + materialsPercent.Value + "/" + goldPercent.Value + "/" + runesPercent.Value + "/" + shardsPercent.Value + "/" + tokensPercent.Value + "."));
			}
			catch (Exception ex)
			{
				engine = null;
				((BaseUnityPlugin)this).Logger.LogError((object)("Could not install refund hooks; normal Epic Loot sacrifice remains active. " + ex));
				RemoveHooks();
			}
		}

		private static Type NeedType(string name)
		{
			return AccessTools.TypeByName(name) ?? throw new TypeLoadException(name);
		}

		private void Patch(MethodInfo target, string prefix, string postfix, string finalizer = null)
		{
			if (!patchedTargets.Contains(target))
			{
				patchedTargets.Add(target);
			}
			harmony.Patch((MethodBase)target, Hook(prefix), Hook(postfix), (HarmonyMethod)null, Hook(finalizer), (HarmonyMethod)null);
		}

		private void RemoveHooks()
		{
			if (harmony == null)
			{
				return;
			}
			foreach (MethodInfo item in patchedTargets.AsEnumerable().Reverse())
			{
				try
				{
					harmony.Unpatch((MethodBase)item, (HarmonyPatchType)0, "arcbyte.epicloot.sacrificerecycler");
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)("Could not remove Recycler hooks from " + item?.ToString() + ": " + ex));
				}
			}
			patchedTargets.Clear();
		}

		private static HarmonyMethod Hook(string name)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			return (name == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Plugin).GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
			{
				priority = 0
			};
		}

		private ConfigEntry<int> CostSetting(string name, int value, string description)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			return ((BaseUnityPlugin)this).Config.Bind<int>("Enchanting", name, value, new ConfigDescription(description + " 0 = free; 100 = native cost before resource-group scaling. Spending only; recycling always uses full native recipe value. Local to this client.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
		}

		private ConfigEntry<int> ResourceSetting(string name)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Expected O, but got Unknown
			return ((BaseUnityPlugin)this).Config.Bind<int>("Resources", name, 100, new ConfigDescription("Local ingredient multiplier for every supported enchanting service: 0 = free; 100 = unchanged. Combined with the operation percentage before rounding up once. Does not scale recycling refunds or socket objects.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
		}

		private static ResourceCosts CurrentResources()
		{
			return new ResourceCosts
			{
				Materials = materialsPercent.Value,
				Gold = goldPercent.Value,
				Runes = runesPercent.Value,
				Shards = shardsPercent.Value,
				Tokens = tokensPercent.Value,
				GoldPrefabs = goldPrefabs.Value,
				RunePrefabs = runePrefabs.Value,
				ShardPrefabs = shardPrefabs.Value,
				TokenPrefabs = tokenPrefabs.Value
			};
		}

		private void InstallEnchantingHooks(Type costs, Type controller)
		{
			string[] array = new string[4] { "GetEnchantCost", "GetAugmentCost", "GetIdentifyCosts", "GetRuneCost" };
			foreach (string name in array)
			{
				Patch(costs.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == name), null, "CostsPostfix");
			}
			Patch(costs.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetReAugmentCost"), null, "SlotFeePostfix");
			Patch(controller.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetDisenchantCost"), "DisenchantCostPrefix", "DisenchantCostPostfix");
			Patch(NeedType("EpicLoot.TemperMan").GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetRequirements"), null, "TemperCostsPostfix");
			Type type = NeedType("EpicLoot.TemperData");
			if (type.GetField("selectedValues", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) == null || type.GetField("_tempItem", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) == null || type.GetProperty("success", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetSetMethod(nonPublic: true) == null)
			{
				throw new MissingMemberException("Unsupported Epic Loot TemperData shape");
			}
			Patch(RefundEngine.RequireMethod(type, "CalculateProbability"), null, "TemperChancePostfix");
			Patch(RefundEngine.RequireMethod(type, "RollOutcome"), null, "TemperOutcomePostfix");
		}

		private static void CostsPostfix(MethodBase __originalMethod, ref object __result)
		{
			if (Active && !RefundCostScope.IsActive)
			{
				int percent = ((__originalMethod.Name == "GetEnchantCost") ? enchantPercent.Value : ((__originalMethod.Name == "GetAugmentCost") ? augmentPercent.Value : ((__originalMethod.Name == "GetIdentifyCosts") ? identifyPercent.Value : runePercent.Value)));
				__result = CostPolicy.CopyCosts(__result, "Amount", percent, "Item", CurrentResources());
			}
		}

		private static void SlotFeePostfix(ref object __result)
		{
			if (Active && !RefundCostScope.IsActive && removeSlotFee.Value)
			{
				__result = null;
			}
		}

		private static void TemperCostsPostfix(ref object __result)
		{
			if (Active && !RefundCostScope.IsActive)
			{
				__result = CostPolicy.CopyCosts(__result, "amount", temperPercent.Value, "prefab", CurrentResources());
			}
		}

		private static bool DisenchantCostPrefix(MethodBase __originalMethod, ref object __result)
		{
			if (!Active || disenchantPercent.Value != 0)
			{
				return true;
			}
			__result = Activator.CreateInstance(((MethodInfo)__originalMethod).ReturnType);
			return false;
		}

		private static void DisenchantCostPostfix(ref object __result)
		{
			if (!Active)
			{
				return;
			}
			ResourceCosts resourceCosts = CurrentResources();
			IList list = (IList)Activator.CreateInstance(__result.GetType());
			foreach (object item in (IEnumerable)__result)
			{
				object obj = Reflect.Get(item, "Item");
				object obj2 = Reflect.Get(obj, "m_dropPrefab");
				string prefab = ((obj2 == null) ? FindCostPrefab(obj) : Convert.ToString(Reflect.Get(obj2, "name")));
				int num = CostPolicy.Scale(Convert.ToInt32(Reflect.Get(obj, "m_stack")), disenchantPercent.Value, resourceCosts.Percent(prefab));
				if (num != 0)
				{
					object obj3 = RefundEngine.RequireMethod(obj.GetType(), "Clone").Invoke(obj, null);
					Reflect.Set(obj3, "m_stack", num);
					object obj4 = Activator.CreateInstance(item.GetType());
					Reflect.Set(obj4, "Item", obj3);
					list.Add(obj4);
				}
			}
			__result = list;
		}

		private static bool DisenchantPrefix(ItemData __0, ref bool __1, ref object __result, out DisenchantState __state)
		{
			__state = new DisenchantState
			{
				Depth = disenchantDepth,
				Item = __0
			};
			if (!Active)
			{
				return true;
			}
			disenchantDepth++;
			try
			{
				if (__0 != null && RecyclerAPI.CanDisenchant(__0) && GetMagic(__0) != null)
				{
					__state.Rows = RecyclerAPI.EnchantmentRows(__0, Math.Max(1, __0.m_stack));
				}
				return true;
			}
			catch (Exception ex)
			{
				__result = RecyclerAPI.NewRows();
				__1 = false;
				Error("Disenchant blocked: full enchanting refund unavailable", ex);
				return false;
			}
		}

		private static void DisenchantPostfix(ref bool __1, object __result, DisenchantState __state)
		{
			if (!Active || __state.Rows == null || GetMagic(__state.Item) != null)
			{
				return;
			}
			foreach (object item in (IEnumerable)__state.Rows)
			{
				((IList)__result).Add(item);
			}
			__1 = false;
		}

		private static Exception DisenchantFinalizer(Exception __exception, DisenchantState __state)
		{
			disenchantDepth = __state.Depth;
			return __exception;
		}

		private static string FindCostPrefab(object item)
		{
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Expected O, but got Unknown
			Type type = NeedType("ObjectDB");
			object target = Reflect.GetStatic(type, "instance");
			object obj = Reflect.Get(item, "m_shared");
			foreach (GameObject item2 in (IEnumerable)Reflect.Get(target, "m_items"))
			{
				GameObject val = item2;
				object component = val.GetComponent(itemType.DeclaringType);
				if (component != null && Reflect.Get(Reflect.Get(component, "m_itemData"), "m_shared") == obj)
				{
					return ((Object)val).name;
				}
			}
			throw new InvalidOperationException("Cannot resolve disenchant cost prefab");
		}

		private static void TemperChancePostfix(ref float __result)
		{
			if (Active && guaranteedTemper.Value)
			{
				__result = 1f;
			}
		}

		private static void TemperOutcomePostfix(object __instance)
		{
			if (Active && guaranteedTemper.Value && Reflect.Get(__instance, "selectedValues") != null && Reflect.Get(__instance, "_tempItem") != null)
			{
				Reflect.Set(__instance, "success", true);
			}
		}

		private static RefundSettings CurrentSettings()
		{
			return new RefundSettings
			{
				RefundMultiplier = refundMultiplier.Value,
				RerollCreditsPerMarkedEffect = rerollCredits.Value,
				TemperCreditsPerMarkedEffect = temperCredits.Value
			};
		}

		private void OnDestroy()
		{
			if ((Object)(object)self == (Object)(object)this)
			{
				currentUI = null;
				engine = null;
				self = null;
			}
			RemoveHooks();
		}

		private static void ProductsPostfix(object[] __args, ref object __result)
		{
			if (!Active || (sacrificeDepth == 0 && previewDepth == 0 && listDepth == 0))
			{
				return;
			}
			object item = __args[0];
			try
			{
				if (!engine.TryQuote(item, out var quote))
				{
					__result = null;
					return;
				}
				if (quote.RetainItem)
				{
					__result = null;
					return;
				}
				object obj = (__result = quote.Products);
				if (sacrificeDepth > 0 && obj != null)
				{
					refundedInBatch = true;
				}
			}
			catch (Exception ex)
			{
				__result = null;
				Error("Full refund unavailable; equipment hidden from sacrifice", ex);
			}
		}

		private static void SacrificePrefix(out TransactionState __state)
		{
			__state = new TransactionState
			{
				Depth = sacrificeDepth,
				Refunded = refundedInBatch
			};
			sacrificeDepth++;
			refundedInBatch = false;
		}

		private static Exception SacrificeFinalizer(Exception __exception, TransactionState __state)
		{
			sacrificeDepth = __state.Depth;
			refundedInBatch = __state.Refunded;
			return __exception;
		}

		private static void FeaturePostfix(object[] __args, ref Tuple<float, float> __result)
		{
			if (Active && disenchantDepth > 0 && __args[0].ToString() == "Disenchant")
			{
				__result = Tuple.Create(0f, __result.Item2);
			}
			if (Active && sacrificeDepth > 0 && refundedInBatch && __args[0].ToString() == "Sacrifice")
			{
				__result = Tuple.Create(0f, __result.Item2);
			}
		}

		private static void SelectionPrefix(object __instance, out int __state)
		{
			currentUI = __instance;
			__state = previewDepth;
			if (Reflect.Get(__instance, "_sacrificeMode").ToString() == "Sacrifice")
			{
				previewDepth++;
			}
		}

		private static void UiEnabledPostfix(object __instance)
		{
			currentUI = __instance;
		}

		private static Exception SelectionFinalizer(Exception __exception, int __state)
		{
			previewDepth = __state;
			return __exception;
		}

		private static void ListPrefix(out int __state)
		{
			__state = listDepth;
			listDepth++;
		}

		private static Exception ListFinalizer(Exception __exception, int __state)
		{
			listDepth = __state;
			return __exception;
		}

		private static bool SacrificeVisible()
		{
			object obj = currentUI;
			Component val = (Component)((obj is Component) ? obj : null);
			return (Object)(object)val != (Object)null && val.gameObject.activeInHierarchy && Reflect.Get(currentUI, "_sacrificeMode").ToString() == "Sacrifice";
		}

		private static void PreviewSocketsPostfix(object[] __args, object __result)
		{
			if (!Active || previewDepth == 0 || sacrificeDepth > 0)
			{
				return;
			}
			try
			{
				if (!SacrificeVisible())
				{
					return;
				}
				IList list = (IList)__result;
				foreach (object item2 in (IEnumerable)__args[0])
				{
					object item = Reflect.Get(item2, "Item1");
					int num = Convert.ToInt32(Reflect.Get(item2, "Item2"));
					if (num <= 0)
					{
						continue;
					}
					foreach (object item3 in Sockets(item))
					{
						object target = Reflect.Get(item3, "Item");
						Reflect.Set(target, "m_stack", checked(Convert.ToInt32(Reflect.Get(target, "m_stack")) * num));
						list.Add(item3);
					}
				}
			}
			catch (Exception ex)
			{
				Error("Socket preview unavailable; Epic Loot still handles socket returns", ex);
			}
		}

		private static IEnumerable Sockets(object item)
		{
			object obj = magicItem.Invoke(null, new object[1] { item });
			object result;
			if (obj != null)
			{
				result = (IEnumerable)reclaimSockets.Invoke(null, new object[1] { obj });
			}
			else
			{
				IEnumerable enumerable = Array.Empty<object>();
				result = enumerable;
			}
			return (IEnumerable)result;
		}

		private static void TooltipPostfix(object[] __args, ref string __result)
		{
			if (!Active || !hoverPreview.Value)
			{
				return;
			}
			try
			{
				if (!SacrificeVisible())
				{
					return;
				}
				object item = __args[0];
				if (!engine.TryCalculate(item, out var products))
				{
					return;
				}
				if (products == null)
				{
					__result += "\n\n<color=#ffcc66>Full-refund sacrifice unavailable for this item.</color>";
					return;
				}
				StringBuilder stringBuilder = new StringBuilder("\n\n<color=#ffcc66>Sacrifice returns (destroys this item)</color>");
				foreach (object item2 in (IEnumerable)products)
				{
					stringBuilder.Append("\n").Append(Reflect.Get(item2, "Amount")).Append(" × ")
						.Append(MaterialName((string)Reflect.Get(item2, "Item")));
				}
				foreach (object item3 in Sockets(item))
				{
					object target = Reflect.Get(item3, "Item");
					stringBuilder.Append("\n").Append(Reflect.Get(target, "m_stack")).Append(" × ")
						.Append(Localize((string)Reflect.Get(Reflect.Get(target, "m_shared"), "m_name")))
						.Append(" (socket)");
				}
				ModificationSummary modificationSummary = engine.GetModificationSummary(item);
				stringBuilder.Append("\nRecorded effects: ").Append(modificationSummary.AugmentedEffects).Append(" augmented; ")
					.Append(modificationSummary.TemperedEffects)
					.Append(" tempered.");
				RefundSettings refundSettings = CurrentSettings();
				stringBuilder.Append("\nCredits per marked effect: reroll ").Append(refundSettings.RerollCreditsPerMarkedEffect).Append("; temper ")
					.Append(refundSettings.TemperCreditsPerMarkedEffect)
					.Append(". Material multiplier: ×")
					.Append(refundMultiplier.Value)
					.Append('.');
				stringBuilder.Append("\nFull native recipe credits, independent of spending discounts. Not exact spending history. No double yield.");
				__result += stringBuilder.ToString();
			}
			catch (Exception ex)
			{
				Error("Refund tooltip unavailable; use the returns panel", ex);
			}
		}

		private static string MaterialName(string prefabName)
		{
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Expected O, but got Unknown
			Type type = NeedType("ObjectDB");
			object obj = Reflect.GetStatic(type, "instance");
			GameObject val = (GameObject)RefundEngine.RequireMethod(type, "GetItemPrefab", typeof(string)).Invoke(obj, new object[1] { prefabName });
			object component = val.GetComponent(NeedType("ItemDrop"));
			return Localize((string)Reflect.Get(Reflect.Get(Reflect.Get(component, "m_itemData"), "m_shared"), "m_name"));
		}

		private static string Localize(string token)
		{
			Type type = NeedType("Localization");
			object obj = Reflect.GetStatic(type, "instance");
			return (string)RefundEngine.RequireMethod(type, "Localize", typeof(string)).Invoke(obj, new object[1] { token });
		}

		private static void Error(string message, Exception ex)
		{
			string text = message + ": " + (ex.InnerException ?? ex).Message;
			if (Errors.Add(text))
			{
				((BaseUnityPlugin)self).Logger.LogWarning((object)text);
			}
		}
	}
	internal static class RecyclerAdapters
	{
		private sealed class Frame
		{
			internal object Item;

			internal IList Rows;
		}

		[ThreadStatic]
		private static Frame frame;

		internal static void Install(Harmony harmony, ManualLogSource log)
		{
			InstallOne(harmony, log, "MidgardPlus.Content.TrashAndRecycle", "Recycle", "Yield", "1.56.5", tuples: true);
			InstallOne(harmony, log, "SeneaLUI.Recycle.NativeRecycler", "Take", "Gains", "1.0.0", tuples: false);
		}

		private static HarmonyMethod Hook(string name)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Expected O, but got Unknown
			return new HarmonyMethod(typeof(RecyclerAdapters).GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
		}

		private static void InstallOne(Harmony harmony, ManualLogSource log, string name, string action, string gains, string version, bool tuples)
		{
			Type type = AccessTools.TypeByName(name);
			if (type == null)
			{
				log.LogInfo((object)("Optional recycler absent: " + name));
				return;
			}
			string text = type.Assembly.GetTypes().SelectMany((Type t) => t.GetCustomAttributes(typeof(BepInPlugin), inherit: false)).Cast<BepInPlugin>()
				.Single()
				.Version.ToString();
			if (text != version)
			{
				log.LogWarning((object)("Recycler adapter requires audited version " + version + ": " + name));
				return;
			}
			MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			MethodInfo methodInfo = methods.Single((MethodInfo m) => m.Name == action && m.GetParameters().Length == 3 && m.GetParameters()[0].ParameterType == typeof(Player));
			MethodInfo methodInfo2 = methods.Single((MethodInfo m) => m.Name == gains && m.GetParameters().Length == 3);
			Type type2 = (tuples ? methodInfo2.ReturnType : methodInfo2.GetParameters()[2].ParameterType);
			Type type3 = type2.GetGenericArguments().Single();
			if (type3.GetGenericArguments()[0] != typeof(ItemDrop) || type3.GetGenericArguments()[1] != typeof(int))
			{
				throw new InvalidOperationException("Recycler payout shape changed");
			}
			harmony.Patch((MethodBase)methodInfo, Hook(tuples ? "MidgardGuard" : "SenealGuard"), (HarmonyMethod)null, (HarmonyMethod)null, Hook("Finish"), (HarmonyMethod)null);
			harmony.Patch((MethodBase)methodInfo2, Hook(tuples ? "MidgardYield" : "SenealGains"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			log.LogInfo((object)("Prepared equipment refund adapter: " + name + " " + version + ". Native eligibility/removal/delivery retained; non-decomposable and socketed inputs protected."));
		}

		private static bool Prepare(object item, Type listType, out Frame state)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Expected O, but got Unknown
			state = frame;
			try
			{
				frame = new Frame
				{
					Item = item,
					Rows = Build((ItemData)item, listType)
				};
				return frame.Rows != null;
			}
			catch (Exception ex)
			{
				frame = state;
				Plugin.Report("Recycler preflight blocked input before native removal", ex);
				return false;
			}
		}

		private static bool MidgardGuard(object[] __args, MethodBase __originalMethod, out Frame __state)
		{
			__state = frame;
			if (!Plugin.Active)
			{
				return true;
			}
			Type returnType = __originalMethod.DeclaringType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Yield").ReturnType;
			return Prepare(__args[1], returnType, out __state);
		}

		private static bool SenealGuard(object[] __args, MethodBase __originalMethod, ref bool __result, out Frame __state)
		{
			__state = frame;
			if (!Plugin.Active)
			{
				return true;
			}
			Type parameterType = __originalMethod.DeclaringType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Gains").GetParameters()[2].ParameterType;
			__result = false;
			return Prepare(__args[1], parameterType, out __state);
		}

		private static Exception Finish(Exception __exception, Frame __state)
		{
			frame = __state;
			return __exception;
		}

		private static bool MidgardYield(object[] __args, MethodBase __originalMethod, ref object __result)
		{
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Expected O, but got Unknown
			if (!Plugin.Active)
			{
				return true;
			}
			__result = ((frame != null && frame.Item == __args[1]) ? frame.Rows : (Build((ItemData)__args[1], ((MethodInfo)__originalMethod).ReturnType) ?? Activator.CreateInstance(((MethodInfo)__originalMethod).ReturnType)));
			return false;
		}

		private static bool SenealGains(object[] __args)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Expected O, but got Unknown
			if (!Plugin.Active)
			{
				return true;
			}
			IList list = (IList)__args[2];
			IList list2 = ((frame != null && frame.Item == __args[0]) ? frame.Rows : Build((ItemData)__args[0], list.GetType()));
			list.Clear();
			if (list2 != null)
			{
				foreach (object item in list2)
				{
					list.Add(item);
				}
			}
			return false;
		}

		private static IList Build(ItemData item, Type listType)
		{
			if (item == null || item.m_stack != 1 || item.m_equipped || item.m_shared.m_questItem || RecyclerAPI.IsProtected(item))
			{
				return null;
			}
			object magic = Plugin.GetMagic(item);
			if (magic != null && ((IList)Reflect.Get(magic, "Sockets")).Count > 0)
			{
				return null;
			}
			if (!RecyclerAPI.TryGetRefund(item, 1, out var materials, out var retainItem) || retainItem)
			{
				return null;
			}
			IList list = (IList)Activator.CreateInstance(listType);
			Type type = listType.GetGenericArguments()[0];
			foreach (KeyValuePair<string, int> item2 in materials)
			{
				GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(item2.Key);
				if ((Object)(object)itemPrefab == (Object)null || (Object)(object)itemPrefab.GetComponent<ItemDrop>() == (Object)null)
				{
					throw new InvalidOperationException("Missing prepared refund prefab " + item2.Key);
				}
				list.Add(Activator.CreateInstance(type, itemPrefab.GetComponent<ItemDrop>(), item2.Value));
			}
			return (list.Count > 0) ? list : null;
		}
	}
	public static class RecyclerAPI
	{
		public static bool TryGetRefund(ItemData item, int quantity, out Dictionary<string, int> materials, out bool retainItem)
		{
			materials = null;
			retainItem = false;
			if (!Plugin.Active || item == null || quantity < 1 || quantity > item.m_stack)
			{
				return false;
			}
			try
			{
				if (!Plugin.Engine.TryQuote(item, out var quote))
				{
					return false;
				}
				materials = new Dictionary<string, int>(StringComparer.Ordinal);
				foreach (object item2 in (IEnumerable)quote.Products)
				{
					materials.Add((string)Reflect.Get(item2, "Item"), checked((int)Reflect.Get(item2, "Amount") * quantity));
				}
				retainItem = quote.RetainItem;
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Report("Recycle quote blocked", ex);
				return false;
			}
		}

		internal static bool IsProtected(ItemData item)
		{
			return (bool)RefundEngine.RequireMethod(AccessTools.TypeByName("EpicLoot.Compatibility.ItemFavorites"), "IsProtected", typeof(ItemData)).Invoke(null, new object[1] { item });
		}

		internal static bool CanDisenchant(ItemData item)
		{
			return (bool)RefundEngine.RequireMethod(AccessTools.TypeByName("EpicLoot.ItemDataExtensions"), "CanBeDisenchanted", typeof(ItemData)).Invoke(null, new object[1] { item });
		}

		internal static IList NewRows()
		{
			return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(AccessTools.TypeByName("EpicLoot_UnityLib.InventoryItemListElement")));
		}

		internal static void AddItem(IList rows, ItemData item)
		{
			object obj = Activator.CreateInstance(AccessTools.TypeByName("EpicLoot_UnityLib.InventoryItemListElement"));
			Reflect.Set(obj, "Item", item);
			rows.Add(obj);
		}

		internal static void AddMaterial(IList rows, string prefabName, int amount)
		{
			GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(prefabName);
			if ((Object)(object)itemPrefab == (Object)null || (Object)(object)itemPrefab.GetComponent<ItemDrop>() == (Object)null)
			{
				throw new InvalidOperationException("Missing refund prefab " + prefabName);
			}
			ItemData val = itemPrefab.GetComponent<ItemDrop>().m_itemData.Clone();
			val.m_dropPrefab = itemPrefab;
			val.m_stack = amount;
			AddItem(rows, val);
		}

		internal static object EnchantmentRows(ItemData item, int quantity)
		{
			IList list = NewRows();
			foreach (object item2 in (IEnumerable)Plugin.Engine.CalculateEnchantments(item))
			{
				AddMaterial(list, (string)Reflect.Get(item2, "Item"), checked((int)Reflect.Get(item2, "Amount") * quantity));
			}
			return list;
		}
	}
	public sealed class RefundSettings
	{
		public int RefundMultiplier = 1;

		public int RerollCreditsPerMarkedEffect = 1;

		public int TemperCreditsPerMarkedEffect = 1;
	}
	public struct ModificationSummary
	{
		public int AugmentedEffects;

		public int TemperedEffects;
	}
	public sealed class RecyclingQuote
	{
		public bool RetainItem { get; internal set; }

		public object Products { get; internal set; }
	}
	public sealed class RefundEngine
	{
		private readonly Type dbType;

		private readonly Type itemType;

		private readonly Type amountType;

		private readonly MethodInfo allowed;

		private readonly MethodInfo isMagic;

		private readonly MethodInfo isUnidentified;

		private readonly MethodInfo rarity;

		private readonly MethodInfo enchantCost;

		private readonly MethodInfo getMagicItem;

		private readonly MethodInfo temperRequirements;

		private readonly Type helperType;

		private readonly Func<RefundSettings> settingsProvider;

		private readonly Action<string> warn;

		private readonly HashSet<string> warnings = new HashSet<string>();

		private static readonly HashSet<string> Equipment = new HashSet<string>
		{
			"OneHandedWeapon", "TwoHandedWeapon", "TwoHandedWeaponLeft", "Bow", "Bows", "Crossbow", "Crossbows", "Shield", "Helmet", "Chest",
			"Legs", "Shoulder", "Utility", "Tool", "Torch", "Trinket"
		};

		public RefundEngine(Assembly epic, Type objectDbType, Type itemDataType, Action<string> warning, Func<RefundSettings> settings = null)
		{
			dbType = objectDbType;
			itemType = itemDataType;
			warn = warning;
			settingsProvider = settings ?? ((Func<RefundSettings>)(() => new RefundSettings()));
			Type type = epic.GetType("EpicLoot.API", throwOnError: true);
			Type type2 = epic.GetType("EpicLoot.ItemDataExtensions", throwOnError: true);
			Type type3 = (helperType = epic.GetType("EpicLoot.Crafting.EnchantCostsHelper", throwOnError: true));
			amountType = epic.GetType("EpicLoot.Crafting.ItemAmountConfig", throwOnError: true);
			allowed = RequireMethod(type, "SacrificeAllowed", itemType);
			isMagic = RequireMethod(type2, "IsMagic", itemType);
			isUnidentified = RequireMethod(type2, "IsUnidentified", itemType);
			rarity = RequireMethod(type2, "GetRarity", itemType);
			getMagicItem = RequireMethod(type2, "GetMagicItem", itemType);
			enchantCost = type3.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetEnchantCost" && m.GetParameters().Length == 2);
			temperRequirements = epic.GetType("EpicLoot.TemperMan", throwOnError: true).GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "GetRequirements" && m.GetParameters().Length == 1);
		}

		public bool IsEquipment(object item)
		{
			if (item == null)
			{
				return false;
			}
			object target = Reflect.Get(item, "m_shared");
			return Equipment.Contains(Reflect.Get(target, "m_itemType").ToString()) && Convert.ToInt32(Reflect.Get(target, "m_maxStackSize")) == 1;
		}

		public bool TryQuote(object item, out RecyclingQuote quote)
		{
			quote = null;
			if (item == null || !(bool)allowed.Invoke(null, new object[1] { item }) || (bool)isUnidentified.Invoke(null, new object[1] { item }))
			{
				return false;
			}
			object obj = Reflect.GetStatic(dbType, "instance");
			if (obj == null)
			{
				return false;
			}
			object obj2 = RequireMethod(dbType, "GetRecipe", itemType).Invoke(obj, new object[1] { item });
			bool flag = IsEquipment(item) && obj2 != null && Convert.ToInt32(Reflect.Get(obj2, "m_amount")) == 1 && !Convert.ToBoolean(Reflect.Get(obj2, "m_requireOnlyOneIngredient")) && !Convert.ToBoolean(Reflect.Get(obj2, "m_noCraftOnlyUpgrade"));
			object products;
			if (flag)
			{
				if (!TryCalculate(item, out products) || products == null)
				{
					return false;
				}
			}
			else
			{
				products = CalculateEnchantments(item);
			}
			quote = new RecyclingQuote
			{
				RetainItem = !flag,
				Products = products
			};
			return true;
		}

		public object CalculateEnchantments(object item)
		{
			if (item == null || (bool)isUnidentified.Invoke(null, new object[1] { item }))
			{
				throw new InvalidOperationException("Cannot refund unidentified or missing item enchantments");
			}
			SortedDictionary<string, int> totals = new SortedDictionary<string, int>(StringComparer.Ordinal);
			RefundSettings settings = settingsProvider();
			if ((bool)isMagic.Invoke(null, new object[1] { item }))
			{
				object tier = rarity.Invoke(null, new object[1] { item });
				if (!(RefundCostScope.ReadNative(() => enchantCost.Invoke(null, new object[2] { item, tier })) is IEnumerable enumerable))
				{
					throw new InvalidOperationException("Missing native enchant recipe");
				}
				foreach (object item2 in enumerable)
				{
					AddCredit(totals, item2, "Item", "Amount", 1);
				}
				AddModificationCredits(item, tier, settings, totals);
			}
			return BuildProducts(totals, settings);
		}

		public bool TryCalculate(object item, out object products)
		{
			products = null;
			if (!IsEquipment(item))
			{
				return false;
			}
			if (!(bool)allowed.Invoke(null, new object[1] { item }))
			{
				return true;
			}
			if ((bool)isUnidentified.Invoke(null, new object[1] { item }))
			{
				return false;
			}
			object obj = Reflect.GetStatic(dbType, "instance");
			if (obj == null)
			{
				return true;
			}
			object obj2 = RequireMethod(dbType, "GetRecipe", itemType).Invoke(obj, new object[1] { item });
			if (obj2 == null)
			{
				return false;
			}
			string text = Convert.ToString(Reflect.Get(Reflect.Get(item, "m_shared"), "m_name"));
			if (Convert.ToInt32(Reflect.Get(obj2, "m_amount")) != 1 || Convert.ToBoolean(Reflect.Get(obj2, "m_requireOnlyOneIngredient")) || Convert.ToBoolean(Reflect.Get(obj2, "m_noCraftOnlyUpgrade")))
			{
				WarnOnce(text + ": ambiguous, batch, or upgrade-only recipe; full-refund sacrifice is blocked.");
				return true;
			}
			int num = Convert.ToInt32(Reflect.Get(item, "m_quality"));
			if (num < 1 || num > 100)
			{
				throw new InvalidOperationException("Invalid quality for " + text);
			}
			RefundSettings refundSettings = settingsProvider();
			if (refundSettings == null || refundSettings.RefundMultiplier < 1 || refundSettings.RefundMultiplier > 10 || refundSettings.RerollCreditsPerMarkedEffect < 0 || refundSettings.RerollCreditsPerMarkedEffect > 100 || refundSettings.TemperCreditsPerMarkedEffect < 0 || refundSettings.TemperCreditsPerMarkedEffect > 100)
			{
				throw new InvalidOperationException("Refund settings are outside the supported ranges");
			}
			SortedDictionary<string, int> sortedDictionary = new SortedDictionary<string, int>(StringComparer.Ordinal);
			foreach (object item2 in (IEnumerable)Reflect.Get(obj2, "m_resources"))
			{
				if (item2 == null)
				{
					throw new InvalidOperationException("Null recipe requirement for " + text);
				}
				object obj3 = Reflect.Get(item2, "m_resItem");
				for (int i = 1; i <= num; i++)
				{
					int num2 = Convert.ToInt32(RequireMethod(item2.GetType(), "GetAmount", typeof(int)).Invoke(item2, new object[1] { i }));
					if (num2 < 0)
					{
						throw new InvalidOperationException("Negative recipe amount for " + text);
					}
					if (num2 != 0)
					{
						if (obj3 == null)
						{
							throw new InvalidOperationException("Missing recipe material for " + text);
						}
						Add(sortedDictionary, Convert.ToString(Reflect.Get(obj3, "name")), num2);
					}
				}
			}
			if ((bool)isMagic.Invoke(null, new object[1] { item }))
			{
				object tier = rarity.Invoke(null, new object[1] { item });
				if (!(RefundCostScope.ReadNative(() => enchantCost.Invoke(null, new object[2] { item, tier })) is IEnumerable enumerable))
				{
					throw new InvalidOperationException("No enchant recipe for " + text + " at " + tier);
				}
				foreach (object item3 in enumerable)
				{
					int num3 = Convert.ToInt32(Reflect.Get(item3, "Amount"));
					if (num3 < 0)
					{
						throw new InvalidOperationException("Negative enchant amount for " + text);
					}
					Add(sortedDictionary, Convert.ToString(Reflect.Get(item3, "Item")), num3);
				}
				AddModificationCredits(item, tier, refundSettings, sortedDictionary);
			}
			if (sortedDictionary.Count == 0)
			{
				return true;
			}
			products = BuildProducts(sortedDictionary, refundSettings);
			return true;
		}

		private object BuildProducts(IDictionary<string, int> totals, RefundSettings settings)
		{
			if (settings == null || settings.RefundMultiplier < 1 || settings.RefundMultiplier > 10 || settings.RerollCreditsPerMarkedEffect < 0 || settings.RerollCreditsPerMarkedEffect > 100 || settings.TemperCreditsPerMarkedEffect < 0 || settings.TemperCreditsPerMarkedEffect > 100)
			{
				throw new InvalidOperationException("Invalid refund settings");
			}
			object obj = Reflect.GetStatic(dbType, "instance");
			if (obj == null)
			{
				throw new InvalidOperationException("ObjectDB is not ready");
			}
			IList list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(amountType));
			foreach (KeyValuePair<string, int> total in totals)
			{
				object obj2 = RequireMethod(dbType, "GetItemPrefab", typeof(string)).Invoke(obj, new object[1] { total.Key });
				if (obj2 == null)
				{
					throw new InvalidOperationException("Missing refund prefab " + total.Key);
				}
				if (itemType.DeclaringType != null && RequireMethod(obj2.GetType(), "GetComponent", typeof(Type)).Invoke(obj2, new object[1] { itemType.DeclaringType }) == null)
				{
					throw new InvalidOperationException("Refund prefab has no ItemDrop: " + total.Key);
				}
				object obj3 = Activator.CreateInstance(amountType);
				Reflect.Set(obj3, "Item", total.Key);
				Reflect.Set(obj3, "Amount", checked(total.Value * settings.RefundMultiplier));
				list.Add(obj3);
			}
			return list;
		}

		public ModificationSummary GetModificationSummary(object item)
		{
			object obj = getMagicItem.Invoke(null, new object[1] { item });
			if (obj == null)
			{
				return default(ModificationSummary);
			}
			int count = ((IList)Reflect.Get(obj, "Effects")).Count;
			HashSet<int> hashSet = ValidIndices((IEnumerable)Reflect.Get(obj, "AugmentedEffectIndices"), count);
			int num = Convert.ToInt32(Reflect.Get(obj, "AugmentedEffectIndex"));
			if (num >= 0 && num < count)
			{
				hashSet.Add(num);
			}
			HashSet<int> hashSet2 = ValidIndices((IEnumerable)Reflect.Get(obj, "TemperedEffectIndices"), count);
			return new ModificationSummary
			{
				AugmentedEffects = hashSet.Count,
				TemperedEffects = hashSet2.Count
			};
		}

		private static HashSet<int> ValidIndices(IEnumerable values, int count)
		{
			HashSet<int> hashSet = new HashSet<int>();
			if (values != null)
			{
				foreach (object value in values)
				{
					int num = Convert.ToInt32(value);
					if (num >= 0 && num < count)
					{
						hashSet.Add(num);
					}
				}
			}
			return hashSet;
		}

		private void AddModificationCredits(object item, object tier, RefundSettings settings, IDictionary<string, int> totals)
		{
			ModificationSummary modificationSummary = GetModificationSummary(item);
			if (modificationSummary.AugmentedEffects > 0 && settings.RerollCreditsPerMarkedEffect > 0)
			{
				object target = Reflect.GetStatic(helperType, "Config");
				string item2 = Reflect.Get(Reflect.Get(item, "m_shared"), "m_itemType").ToString();
				object obj = null;
				foreach (object item3 in (IEnumerable)Reflect.Get(target, "AugmentCosts"))
				{
					if (Convert.ToInt32(Reflect.Get(item3, "Rarity")) == Convert.ToInt32(tier))
					{
						List<string> list = ((!(Reflect.Get(item3, "ItemTypes") is IEnumerable source)) ? new List<string>() : source.Cast<object>().Select(Convert.ToString).ToList());
						if (list.Count == 0 || list.Contains(item2))
						{
							obj = item3;
							break;
						}
					}
				}
				if (obj == null)
				{
					throw new InvalidOperationException("No standard augment recipe for recorded modifications");
				}
				foreach (object item4 in (IEnumerable)Reflect.Get(obj, "Cost"))
				{
					AddCredit(totals, item4, "Item", "Amount", checked(modificationSummary.AugmentedEffects * settings.RerollCreditsPerMarkedEffect));
				}
				if (Reflect.Get(target, "ReAugmentCosts") is IList { Count: >0 } list2)
				{
					for (int i = 0; i < modificationSummary.AugmentedEffects - 1; i++)
					{
						object cost = list2[Math.Min(i, list2.Count - 1)];
						AddCredit(totals, cost, "Item", "Amount", settings.RerollCreditsPerMarkedEffect);
					}
				}
			}
			if (modificationSummary.TemperedEffects <= 0 || settings.TemperCreditsPerMarkedEffect <= 0)
			{
				return;
			}
			foreach (object item5 in (IEnumerable)RefundCostScope.ReadNative(() => temperRequirements.Invoke(null, new object[1] { tier })))
			{
				AddCredit(totals, item5, "prefab", "amount", checked(modificationSummary.TemperedEffects * settings.TemperCreditsPerMarkedEffect));
			}
		}

		private static void AddCredit(IDictionary<string, int> totals, object cost, string itemField, string amountField, int copies)
		{
			int num = Convert.ToInt32(Reflect.Get(cost, amountField));
			if (num < 0)
			{
				throw new InvalidOperationException("Negative modification credit");
			}
			Add(totals, Convert.ToString(Reflect.Get(cost, itemField)), checked(num * copies));
		}

		private static void Add(IDictionary<string, int> totals, string name, int amount)
		{
			if (amount != 0)
			{
				if (string.IsNullOrWhiteSpace(name))
				{
					throw new InvalidOperationException("Empty material prefab name");
				}
				totals.TryGetValue(name, out var value);
				totals[name] = checked(value + amount);
			}
		}

		private void WarnOnce(string message)
		{
			if (warnings.Add(message))
			{
				warn(message);
			}
		}

		internal static MethodInfo RequireMethod(Type type, string name, params Type[] args)
		{
			return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, args, null) ?? throw new MissingMethodException(type.FullName, name);
		}
	}
	internal static class RefundCostScope
	{
		[ThreadStatic]
		private static int depth;

		internal static bool IsActive => depth > 0;

		internal static T ReadNative<T>(Func<T> read)
		{
			depth++;
			try
			{
				return read();
			}
			finally
			{
				depth--;
			}
		}
	}
	internal static class Reflect
	{
		internal const BindingFlags All = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

		private static readonly Dictionary<Tuple<Type, string>, MemberInfo> Members = new Dictionary<Tuple<Type, string>, MemberInfo>();

		private static MemberInfo Member(Type type, string name)
		{
			Tuple<Type, string> key = Tuple.Create(type, name);
			if (Members.TryGetValue(key, out var value))
			{
				return value;
			}
			Type type2 = type;
			while (type2 != null)
			{
				value = (MemberInfo)(((object)type2.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) ?? ((object)type2.GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)));
				if (value != null)
				{
					Members[key] = value;
					return value;
				}
				type2 = type2.BaseType;
			}
			throw new MissingMemberException(type.FullName, name);
		}

		internal static object Get(object target, string name)
		{
			return Read(Member(target.GetType(), name), target);
		}

		internal static object GetStatic(Type type, string name)
		{
			return Read(Member(type, name), null);
		}

		private static object Read(MemberInfo member, object target)
		{
			return (member is FieldInfo fieldInfo) ? fieldInfo.GetValue(target) : ((PropertyInfo)member).GetValue(target, null);
		}

		internal static void Set(object target, string name, object value)
		{
			MemberInfo memberInfo = Member(target.GetType(), name);
			if (memberInfo is FieldInfo fieldInfo)
			{
				fieldInfo.SetValue(target, value);
			}
			else
			{
				((PropertyInfo)memberInfo).SetValue(target, value, null);
			}
		}
	}
}