Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of EpicLootSacrificeRecycler v1.4.3
BepInEx/plugins/EpicLootSacrificeRecycler/EpicLootSacrificeRecycler.dll
Decompiled 4 days agousing 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); } } } }