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 Epic Loot Luck Finds v1.5.2
plugins/EpicLootLuckyFinds/EpicLootLuckyFinds.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using EpicLoot; using EpicLoot.Adventure; using EpicLoot.Adventure.Feature; using EpicLoot.GatedItemType; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.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 LocalEpicLootLuckyFinds { internal static class ApiContract { internal static readonly string[] Entries = new string[84] { "E|EpicLoot.Adventure.BountyState|Abandoned|4", "E|EpicLoot.Adventure.BountyState|Available|0", "E|EpicLoot.Adventure.BountyState|Claimed|3", "E|EpicLoot.Adventure.BountyState|Complete|2", "E|EpicLoot.Adventure.BountyState|InProgress|1", "E|EpicLoot.GatedItemType.GatedItemTypeMode|BossKillUnlocksCurrentBiomeItems|1", "E|EpicLoot.GatedItemType.GatedItemTypeMode|BossKillUnlocksNextBiomeItems|2", "E|EpicLoot.GatedItemType.GatedItemTypeMode|PlayerMustHaveCraftedItem|4", "E|EpicLoot.GatedItemType.GatedItemTypeMode|PlayerMustKnowRecipe|3", "E|EpicLoot.GatedItemType.GatedItemTypeMode|Unlimited|0", "F|EpicLoot.Adventure.AdventureDataManager|Gamble|True|True|EpicLoot.Adventure.Feature.GambleAdventureFeature", "F|EpicLoot.Adventure.AdventureSaveData|WorldID|False|True|System.Int64", "F|EpicLoot.Adventure.BountyInfo|Biome|False|True|Heightmap/Biome", "F|EpicLoot.Adventure.BountyInfo|PlayerID|False|True|System.Int64", "F|EpicLoot.Adventure.BountyInfo|RewardCoins|False|True|System.Int32", "F|EpicLoot.Adventure.BountyInfo|State|False|True|EpicLoot.Adventure.BountyState", "F|EpicLoot.Adventure.BountyListElement|NameText|False|True|UnityEngine.UI.Text", "F|EpicLoot.Adventure.BountyListElement|Tooltip|False|True|UITooltip", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|Cost|False|True|EpicLoot.Adventure.Currencies", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|GuaranteedRarity|False|True|System.Boolean", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|IsGamble|False|True|System.Boolean", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|Item|False|True|ItemDrop/ItemData", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|ItemID|False|True|System.String", "F|EpicLoot.Adventure.Feature.SecretStashItemInfo|Rarity|False|True|EpicLoot.ItemRarity", "F|EpicLoot.CreatureDecision|Biome|False|True|System.String", "F|EpicLoot.CreatureDecision|Name|False|True|System.String", "F|EpicLoot.CreatureSorterRunner|LastDecisions|True|False|System.Collections.Generic.List`1<EpicLoot.CreatureDecision>", "F|EpicLoot.GatedItemType.GatedItemTypeHelper|AllItemsWithDetails|True|True|System.Collections.Generic.Dictionary`2<System.String,EpicLoot.GatedItemType.GatedItemDetails>", "F|EpicLoot.GatedItemType.GatedItemTypeHelper|BiomesInOrder|True|True|System.Collections.Generic.List`1<Heightmap/Biome>", "F|EpicLoot.GatedItemType.GatedItemTypeHelper|BiomesToBossKeys|True|True|System.Collections.Generic.Dictionary`2<Heightmap/Biome,System.Collections.Generic.List`1<System.String>>", "F|EpicLoot.LootDrop|Item|False|True|System.String", "M|EpicLoot.Adventure.AdventureSaveData|GetBountyInfoByID|False|True|EpicLoot.Adventure.BountyInfo|System.String", "M|EpicLoot.Adventure.BountyInfo|get_ID|False|True|System.String|", "M|EpicLoot.Adventure.BountyListElement|SetItem|False|True|System.Void|EpicLoot.Adventure.BountyInfo", "M|EpicLoot.Adventure.Currencies|Clone|False|True|EpicLoot.Adventure.Currencies|", "M|EpicLoot.Adventure.Feature.AdventureFeature|GetCurrentInterval|False|True|System.Int32|", "M|EpicLoot.Adventure.Feature.BountiesAdventureFeature|GetAvailableBounties|False|True|System.Collections.Generic.List`1<EpicLoot.Adventure.BountyInfo>|System.Int32,System.Boolean,System.Collections.Generic.List`1<EpicLoot.Adventure.Feature.BountiesAdventureFeature/BountyFilterNote>", "M|EpicLoot.Adventure.Feature.GambleAdventureFeature|GetGambleID|True|True|System.String|EpicLoot.Adventure.Feature.SecretStashItemInfo", "M|EpicLoot.Adventure.Feature.GambleAdventureFeature|GetGambleItems|False|True|System.Collections.Generic.List`1<EpicLoot.Adventure.Feature.SecretStashItemInfo>|", "M|EpicLoot.Adventure.Feature.SecretStashItemInfo|.ctor|False|True|System.Void|System.String,ItemDrop/ItemData,EpicLoot.Adventure.Currencies,System.Boolean", "M|EpicLoot.Adventure.MerchantPanel|OnEnable|False|True|System.Void|", "M|EpicLoot.Adventure.PlayerExtensions_Adventure|GetAdventureSaveData|True|True|EpicLoot.Adventure.AdventureSaveData|Player", "M|EpicLoot.EpicLoot|CanBeMagicItem|True|True|System.Boolean|ItemDrop/ItemData", "M|EpicLoot.EpicLoot|GetGatedItemTypeMode|True|True|EpicLoot.GatedItemType.GatedItemTypeMode|", "M|EpicLoot.GatedItemType.GatedItemDetails|get_RequiredBosses|False|True|System.Collections.Generic.List`1<System.String>|", "M|EpicLoot.GatedItemType.GatedItemDetails|get_Type|False|True|System.String|", "M|EpicLoot.GatedItemType.GatedItemTypeHelper|GetGatedItemNameFromItemOrType|True|True|System.String|System.String,EpicLoot.GatedItemType.GatedItemTypeMode", "M|EpicLoot.LootDenyList|IsDenied|True|True|System.Boolean|System.String", "M|EpicLoot.LootRoller|AnyItemSpawnCheatsActive|True|True|System.Boolean|", "M|EpicLoot.LootRoller|RollLootTableInternal|True|False|System.Void|EpicLoot.LootTable,System.Int32,System.String,UnityEngine.Vector3,EpicLoot.LootRoller/LootOutput", "M|EpicLoot.LootRoller|SpawnNormalItem|True|False|System.Void|EpicLoot.LootDrop,System.String,UnityEngine.Vector3,System.Single,EpicLoot.LootRoller/LootOutput", "M|EpicLoot.LootRoller|TrySpawnUnidentified|True|False|System.Boolean|EpicLoot.LootDrop,UnityEngine.Vector3&,System.Single,EpicLoot.LootRoller/LootOutput", "P|EpicLoot.Adventure.BountyListElement|SetItem|info", "P|EpicLoot.Adventure.Feature.BountiesAdventureFeature|GetAvailableBounties|interval,removeAcceptedBounties,diagnostics", "P|EpicLoot.Adventure.Feature.GambleAdventureFeature|GetGambleID|itemInfo", "P|EpicLoot.Adventure.Feature.GambleAdventureFeature|GetGambleItems|", "P|EpicLoot.Adventure.MerchantPanel|OnEnable|", "P|EpicLoot.EpicLoot|GetGatedItemTypeMode|", "P|EpicLoot.LootRoller|RollLootTableInternal|lootTable,level,objectName,dropPoint,output", "P|EpicLoot.LootRoller|SpawnNormalItem|lootDrop,objectName,dropPoint,luckFactor,output", "P|EpicLoot.LootRoller|TrySpawnUnidentified|lootDrop,dropPoint,luckFactor,output", "T|EpicLoot.Adventure.AdventureDataManager", "T|EpicLoot.Adventure.AdventureSaveData", "T|EpicLoot.Adventure.BountyInfo", "T|EpicLoot.Adventure.BountyListElement", "T|EpicLoot.Adventure.BountyState", "T|EpicLoot.Adventure.Currencies", "T|EpicLoot.Adventure.Feature.AdventureFeature", "T|EpicLoot.Adventure.Feature.BountiesAdventureFeature", "T|EpicLoot.Adventure.Feature.GambleAdventureFeature", "T|EpicLoot.Adventure.Feature.SecretStashItemInfo", "T|EpicLoot.Adventure.MerchantPanel", "T|EpicLoot.Adventure.PlayerExtensions_Adventure", "T|EpicLoot.CreatureDecision", "T|EpicLoot.CreatureSorterRunner", "T|EpicLoot.EpicLoot", "T|EpicLoot.GatedItemType.GatedItemDetails", "T|EpicLoot.GatedItemType.GatedItemTypeHelper", "T|EpicLoot.GatedItemType.GatedItemTypeMode", "T|EpicLoot.ItemRarity", "T|EpicLoot.LootDenyList", "T|EpicLoot.LootDrop", "T|EpicLoot.LootRoller", "T|EpicLoot.LootTable" }; } [BepInPlugin("local.epicloot.luckyfinds", "Epic Loot: Luck Finds", "1.5.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Bootstrap : BaseUnityPlugin { private void Awake() { try { if (!Chainloader.PluginInfos.TryGetValue("randyknapp.mods.epicloot", out var value)) { ((BaseUnityPlugin)this).Logger.LogError((object)"Luck Finds inactive: Epic Loot is missing."); return; } List<string> list = Compatibility.Check(((object)value.Instance).GetType().Assembly, ApiContract.Entries); if (list.Count > 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Luck Finds inactive on Epic Loot " + value.Metadata.Version?.ToString() + "; no patches applied. " + string.Join("; ", list.ToArray()))); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("Epic Loot " + value.Metadata.Version?.ToString() + " passed Luck Finds API checks. This checks API compatibility, not all in-game behavior.")); StartEngine(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Luck Finds inactive: compatibility/startup check failed: " + ex)); } } [MethodImpl(MethodImplOptions.NoInlining)] private void StartEngine() { Plugin.StartConfig = ((BaseUnityPlugin)this).Config; Plugin.StartLogger = ((BaseUnityPlugin)this).Logger; ((Component)this).gameObject.AddComponent<Plugin>(); } } public sealed class BountyRewardEntry { public int BaseCoins; public int Coins; public bool Lucky; } internal static class BountyRules { internal static BountyRewardEntry Decide(int coins, double sample) { bool flag = coins > 0 && coins <= 715827882 && Policy.Wins(sample, 20.0); return new BountyRewardEntry { BaseCoins = coins, Coins = (flag ? (coins * 3) : coins), Lucky = flag }; } internal static bool DecideOnce(Dictionary<string, BountyRewardEntry> history, string key, int coins, double sample) { if (history.ContainsKey(key)) { return false; } history.Add(key, Decide(coins, sample)); return true; } internal static bool Valid(BountyRewardEntry e) { if (e != null) { if (!e.Lucky) { return e.Coins == e.BaseCoins; } if (e.BaseCoins > 0 && e.BaseCoins <= 715827882) { return e.Coins == e.BaseCoins * 3; } return false; } return false; } } internal static class Compatibility { private const BindingFlags Flags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; internal static string TypeName(Type t) { if (t.IsGenericParameter) { return "!" + t.GenericParameterPosition; } if (t.IsByRef) { return TypeName(t.GetElementType()) + "&"; } if (t.IsArray) { return TypeName(t.GetElementType()) + "[]"; } if (t.IsGenericType) { return t.GetGenericTypeDefinition().FullName.Replace('+', '/') + "<" + string.Join(",", t.GetGenericArguments().Select(TypeName).ToArray()) + ">"; } return t.FullName.Replace('+', '/'); } internal static string MethodKey(MethodBase m) { return "M|" + m.DeclaringType.FullName.Replace('+', '/') + "|" + m.Name + "|" + m.IsStatic + "|" + m.IsPublic + "|" + ((m is MethodInfo) ? TypeName(((MethodInfo)m).ReturnType) : "System.Void") + "|" + string.Join(",", (from p in m.GetParameters() select TypeName(p.ParameterType)).ToArray()); } internal static string FieldKey(FieldInfo f) { return "F|" + f.DeclaringType.FullName.Replace('+', '/') + "|" + f.Name + "|" + f.IsStatic + "|" + f.IsPublic + "|" + TypeName(f.FieldType); } internal static List<string> Check(Assembly assembly, IEnumerable<string> contract) { List<string> list = new List<string>(); foreach (string entry in contract) { try { string[] parts = entry.Split('|'); Type type = assembly.GetType(parts[1].Replace('/', '+'), throwOnError: false); if (type == null) { list.Add("Missing type: " + parts[1]); continue; } bool flag = false; if (parts[0] == "T") { flag = true; } else if (parts[0] == "F") { flag = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Any((FieldInfo f) => FieldKey(f) == entry); } else if (parts[0] == "M") { flag = (from m in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Cast<MethodBase>().Concat(type.GetConstructors(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) where m.Name == parts[2] select m).Any((MethodBase m) => MethodKey(m) == entry); } else if (parts[0] == "E") { FieldInfo field = type.GetField(parts[2], BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); flag = field != null && Convert.ToInt64(field.GetRawConstantValue()).ToString(CultureInfo.InvariantCulture) == parts[3]; } else if (parts[0] == "P") { flag = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Any((MethodInfo m) => m.Name == parts[2] && string.Join(",", (from p in m.GetParameters() select p.Name).ToArray()) == parts[3]); } if (!flag) { list.Add("API changed: " + entry); } } catch (Exception ex) { list.Add("Cannot inspect " + entry + ": " + ex.GetType().Name + ": " + ex.Message); } } return list; } } public sealed class Plugin : MonoBehaviour { [HarmonyPatch] private static class LuckyBountyOffersPatch { private static MethodBase TargetMethod() { return typeof(BountiesAdventureFeature).GetMethods().Single((MethodInfo m) => m.Name == "GetAvailableBounties" && m.GetParameters().Length != 0 && m.GetParameters()[0].ParameterType == typeof(int)); } private static void Postfix(List<BountyInfo> __result) { if (!Active || __result == null) { return; } try { instance.ApplyBountyRewards(__result); } catch (Exception ex) { Warn("Lucky bounty offers unavailable: " + ex.Message); } } } [HarmonyPatch(typeof(BountyListElement), "SetItem")] private static class LuckyBountyLabelPatch { private static void Postfix(BountyListElement __instance, BountyInfo info) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) if (!Active || info == null) { return; } try { if (instance.LoadBountyRewards() && instance.bountyRewards.TryGetValue(info.ID, out var value) && value.Lucky && info.RewardCoins == value.Coins) { if (Object.op_Implicit((Object)(object)__instance.NameText)) { __instance.NameText.text = "Lucky Bounty: " + __instance.NameText.text; ((Graphic)__instance.NameText).color = new Color(1f, 0.78f, 0.15f); } if (Object.op_Implicit((Object)(object)__instance.Tooltip)) { __instance.Tooltip.m_topic = "Lucky Bounty: " + __instance.Tooltip.m_topic; UITooltip tooltip = __instance.Tooltip; tooltip.m_text += "\n\nLucky Bounty: 3x coins. All other rewards unchanged."; } } } catch (Exception ex) { Warn("Lucky bounty label unavailable: " + ex.Message); } } } public sealed class BountyScope { public string Source; public Biome Biome; } public sealed class RollContext { public string Source; public BountyScope Bounty; public readonly Dictionary<LootDrop, string> Choices = new Dictionary<LootDrop, string>(); } [HarmonyPatch(typeof(Character), "OnDeath")] private static class BountyDeathPatch { private static void Prefix(Character __instance, out BountyScope __state) { __state = bounty; ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); bounty = ReadBounty((component != null) ? component.GetZDO() : null, ((Object)((Component)__instance).gameObject).name); } private static void Finalizer(BountyScope __state) { bounty = __state; } } [HarmonyPatch(typeof(Ragdoll), "Setup")] private static class BountyCorpsePatch { private static void Postfix(Ragdoll __instance, CharacterDrop characterDrop) { try { object obj; if (!Object.op_Implicit((Object)(object)characterDrop)) { obj = null; } else { ZNetView component = ((Component)characterDrop).GetComponent<ZNetView>(); obj = ((component != null) ? component.GetZDO() : null); } ZDO val = (ZDO)obj; ZNetView component2 = ((Component)__instance).GetComponent<ZNetView>(); ZDO val2 = ((component2 != null) ? component2.GetZDO() : null); if (Object.op_Implicit((Object)(object)characterDrop) && val2 != null && val2.IsOwner() && ReadBounty(val, ((Object)((Component)characterDrop).gameObject).name) != null) { val2.Set("local-lucky-bounty-id", val.GetString("BountyID", "")); } } catch (Exception ex) { Warn("Could not retain bounty corpse context: " + ex.Message); } } } [HarmonyPatch(typeof(Ragdoll), "SpawnLoot")] private static class BountyCorpseLootPatch { private static void Prefix(Ragdoll __instance, out BountyScope __state) { __state = bounty; ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); ZDO obj = ((component != null) ? component.GetZDO() : null); bounty = ReadBounty(obj, ((obj != null) ? obj.GetString("characterName", "") : null) ?? "", ragdoll: true); } private static void Finalizer(BountyScope __state) { bounty = __state; } } [HarmonyPatch(typeof(EpicLoot), "GetGatedItemTypeMode")] private static class BossMode { private static void Postfix(ref GatedItemTypeMode __result) { if (Active) { __result = (GatedItemTypeMode)2; } } } [HarmonyPatch] private static class ContextPatch { private static MethodBase TargetMethod() { return typeof(LootRoller).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "RollLootTableInternal" && m.GetParameters()[0].ParameterType == typeof(LootTable)); } private static void Prefix(string objectName, out RollContext __state) { __state = context; context = new RollContext { Source = objectName, Bounty = bounty }; } private static void Finalizer(RollContext __state) { context = __state; } } [HarmonyPatch] private static class NormalPatch { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(LootRoller), "SpawnNormalItem", (Type[])null, (Type[])null); } private static void Prefix(LootDrop lootDrop, out string __state) { __state = selected; selected = Pick(lootDrop); } private static void Finalizer(string __state) { selected = __state; } private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { MethodInfo original = AccessTools.Method(typeof(GatedItemTypeHelper), "GetGatedItemNameFromItemOrType", new Type[2] { typeof(string), typeof(GatedItemTypeMode) }, (Type[])null); MethodInfo operand = AccessTools.Method(typeof(Plugin), "Resolve", (Type[])null, (Type[])null); List<CodeInstruction> list = instructions.ToList(); int num = list.Count((CodeInstruction i) => CodeInstructionExtensions.Calls(i, original)); if (num != 2) { throw new InvalidOperationException("Expected two normal gating calls, found " + num); } foreach (CodeInstruction item in list) { if (CodeInstructionExtensions.Calls(item, original)) { item.operand = operand; } } return list; } } [HarmonyPatch] private static class UnidentifiedPatch { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(LootRoller), "TrySpawnUnidentified", (Type[])null, (Type[])null); } private static void Prefix(LootDrop lootDrop, out string __state) { __state = lootDrop.Item; string text = Pick(lootDrop); if (text != null) { lootDrop.Item = text; } } private static void Finalizer(LootDrop lootDrop, string __state) { lootDrop.Item = __state; } } [HarmonyPatch(typeof(GambleAdventureFeature), "GetGambleItems")] private static class VendorPatch { private static void Postfix(List<SecretStashItemInfo> __result) { if (Active && __result != null) { List<Biome> order = Progression(); int cap = Ceiling(order); __result.RemoveAll((SecretStashItemInfo x) => Equipment(x.Item) && (!GatedItemTypeHelper.AllItemsWithDetails.TryGetValue(x.ItemID, out var value) || Tier(value, order) < 0 || Tier(value, order) > cap)); ApplyLuckyStock(__result, order, cap); } } } private sealed class OfferTag { public string OriginalID; public string Key; } [HarmonyPatch(typeof(MerchantPanel), "OnEnable")] private static class VendorOpenPatch { private static void Postfix(MerchantPanel __instance) { instance.panel = __instance; } } [HarmonyPatch(typeof(GambleAdventureFeature), "GetGambleID")] private static class OfferIdPatch { private static void Postfix(SecretStashItemInfo itemInfo, ref string __result) { if (itemInfo != null && Tags.TryGetValue(itemInfo, out var value)) { __result = value.OriginalID; } } } private Dictionary<string, BountyRewardEntry> bountyRewards; private string bountyRewardPath; private string bountyRewardScope; private bool bountyRewardsFailed; public const string Guid = "local.epicloot.luckyfinds"; internal static ConfigFile StartConfig; internal static ManualLogSource StartLogger; private bool ready; private static Plugin instance; private ConfigEntry<bool> enabledSetting; private ConfigEntry<bool> logFinds; private ConfigEntry<float> chance; private ConfigEntry<float> bountyChance; private ConfigEntry<float> vendorChance; private static readonly Random random = new Random(); private static readonly FieldInfo Decisions = AccessTools.Field(typeof(CreatureSorterRunner), "LastDecisions"); private Harmony harmony; private readonly HashSet<string> warnings = new HashSet<string>(); [ThreadStatic] private static RollContext context; [ThreadStatic] private static string selected; [ThreadStatic] private static BountyScope bounty; private const string RagdollBountyId = "local-lucky-bounty-id"; private static readonly ConditionalWeakTable<SecretStashItemInfo, OfferTag> Tags = new ConditionalWeakTable<SecretStashItemInfo, OfferTag>(); private Dictionary<string, StockEntry> stock; private string stockPath; private List<string> visibleLucky = new List<string>(); private MerchantPanel panel; private AudioSource chimeSource; private AudioClip chime; private bool stockFailed; private ConfigFile Config => StartConfig; private ManualLogSource Logger => StartLogger; private static bool Active { get { if ((Object)(object)instance != (Object)null && instance.ready && instance.enabledSetting.Value && (Object)(object)Player.m_localPlayer != (Object)null && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && !ZNet.instance.IsDedicated()) { return (Object)(object)ZoneSystem.instance != (Object)null; } return false; } } private bool LoadBountyRewards() { string text = PlayerExtensions_Adventure.GetAdventureSaveData(Player.m_localPlayer).WorldID.ToString(CultureInfo.InvariantCulture) + "-" + Player.m_localPlayer.GetPlayerID().ToString(CultureInfo.InvariantCulture); if (text == bountyRewardScope) { return !bountyRewardsFailed; } bountyRewardScope = text; bountyRewardPath = Path.Combine(Paths.ConfigPath, "LuckyFindsBounties", text + ".json"); bountyRewardsFailed = false; try { bountyRewards = (File.Exists(bountyRewardPath) ? JsonConvert.DeserializeObject<Dictionary<string, BountyRewardEntry>>(File.ReadAllText(bountyRewardPath)) : new Dictionary<string, BountyRewardEntry>()); if (bountyRewards == null || bountyRewards.Values.Any((BountyRewardEntry e) => !BountyRules.Valid(e))) { throw new InvalidDataException("Invalid bounty history"); } return true; } catch (Exception ex) { bountyRewardsFailed = true; Warn("Lucky bounty coins disabled: cannot read saved history: " + ex.Message); return false; } } private bool SaveBountyRewards() { try { Directory.CreateDirectory(Path.GetDirectoryName(bountyRewardPath)); string text = bountyRewardPath + ".tmp"; File.WriteAllText(text, JsonConvert.SerializeObject((object)bountyRewards, (Formatting)1)); if (File.Exists(bountyRewardPath)) { File.Replace(text, bountyRewardPath, null); } else { File.Move(text, bountyRewardPath); } return true; } catch (Exception ex) { bountyRewardsFailed = true; Warn("Lucky bounty coins disabled: cannot save history: " + ex.Message); return false; } } private void ApplyBountyRewards(List<BountyInfo> offers) { if (!LoadBountyRewards()) { return; } List<BountyInfo> list = offers.Where((BountyInfo b) => b != null && (int)b.State == 0 && b.PlayerID == Player.m_localPlayer.GetPlayerID()).ToList(); bool flag = false; foreach (BountyInfo item in list) { string iD = item.ID; flag |= BountyRules.DecideOnce(bountyRewards, iD, item.RewardCoins, StockRules.Sample("lucky-bounty-v1|" + bountyRewardScope + "|" + iD)); } if (flag && !SaveBountyRewards()) { return; } foreach (BountyInfo item2 in list) { item2.RewardCoins = bountyRewards[item2.ID].Coins; } } private void Awake() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Expected O, but got Unknown //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Expected O, but got Unknown //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Expected O, but got Unknown instance = this; enabledSetting = Config.Bind<bool>("General", "Enabled", true, "Use native-creature equipment tiers capped by the first undefeated boss. Local host only."); chance = Config.Bind<float>("Creatures", "Lucky Gear Chance Percent", 1f, new ConfigDescription("Per equipment roll chance to bypass the boss cap up to this creature's own native region; never above it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); bountyChance = Config.Bind<float>("Bounties", "Lucky Gear Chance Percent", 5f, new ConfigDescription("Per accepted main bounty equipment roll chance for the next region. Does not affect triple-coin bounties.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); vendorChance = Config.Bind<float>("Vendor", "Lucky Gear Chance Percent", 5f, new ConfigDescription("Per newly generated equipment gamble offer chance for the same category exactly one tier above boss progression. No candidate means the original item.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); logFinds = Config.Bind<bool>("General", "Log Lucky Finds", true, "Log regional equipment decisions and lucky upgrades for verification."); harmony = new Harmony("local.epicloot.luckyfinds"); try { if (Decisions == null) { throw new MissingFieldException("CreatureSorterRunner.LastDecisions"); } harmony.PatchAll(typeof(Plugin).Assembly); ready = true; Logger.LogInfo((object)("Luck Finds 1.5.2 loaded. Lucky gear chances: creatures=" + chance.Value + "%, bounties=" + bountyChance.Value + "%, vendor=" + vendorChance.Value + "%. Lucky bounty coins: 20% chance for 3x coins.")); } catch (Exception ex) { ready = false; harmony.UnpatchSelf(); Logger.LogError((object)("Regional loot inactive: " + ex)); } } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } if (Object.op_Implicit((Object)(object)chimeSource)) { Object.Destroy((Object)(object)chimeSource); } if (Object.op_Implicit((Object)(object)chime)) { Object.Destroy((Object)(object)chime); } } private static void Warn(string text) { if (instance.warnings.Add(text)) { instance.Logger.LogWarning((object)text); } } private static List<Biome> Progression() { return GatedItemTypeHelper.BiomesInOrder.Where((Biome b) => (int)b != 0 && (int)b != 256).ToList(); } private static int Ceiling(List<Biome> order) { List<string> value; return Policy.Ceiling(order.Select((Biome b) => GatedItemTypeHelper.BiomesToBossKeys.TryGetValue(b, out value) && value.Count > 0 && value.All((string k) => ZoneSystem.instance.GetGlobalKey(k))).ToArray()); } private static int Tier(GatedItemDetails d, List<Biome> order) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) if (((d != null) ? d.RequiredBosses : null) == null || d.RequiredBosses.Count == 0) { return -1; } int num = -1; foreach (string requiredBoss in d.RequiredBosses) { int num2 = ((!(requiredBoss == "none")) ? (-1) : 0); for (int i = 0; i < order.Count; i++) { if (GatedItemTypeHelper.BiomesToBossKeys.TryGetValue(order[i], out var value) && value.Contains(requiredBoss)) { num2 = i; } } if (num2 < 0) { return -1; } num = Math.Max(num, num2); } return num; } private static bool Equipment(ItemData item) { if (item == null || !EpicLoot.CanBeMagicItem(item)) { return false; } switch (((object)Unsafe.As<ItemType, ItemType>(ref item.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString()) { case "OneHandedWeapon": case "TwoHandedWeapon": case "Helmet": case "Shield": case "Legs": case "Tool": case "TwoHandedWeaponLeft": case "Bow": case "Chest": case "Shoulder": return true; default: return false; } } private unsafe static string Pick(LootDrop drop) { //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_03ba: Unknown result type (might be due to invalid IL or missing references) //IL_03bf: Unknown result type (might be due to invalid IL or missing references) //IL_048d: Unknown result type (might be due to invalid IL or missing references) //IL_0492: Unknown result type (might be due to invalid IL or missing references) //IL_04af: Unknown result type (might be due to invalid IL or missing references) //IL_04b4: Unknown result type (might be due to invalid IL or missing references) //IL_04d1: Unknown result type (might be due to invalid IL or missing references) //IL_04d6: Unknown result type (might be due to invalid IL or missing references) //IL_0320: Unknown result type (might be due to invalid IL or missing references) //IL_0331: Unknown result type (might be due to invalid IL or missing references) if (!Active || context == null || LootRoller.AnyItemSpawnCheatsActive() || drop == null) { return null; } if (context.Choices.TryGetValue(drop, out var value)) { return value; } string text = null; try { List<Biome> list = Progression(); List<CreatureDecision> list2 = Decisions.GetValue(null) as List<CreatureDecision>; string name = (context.Source ?? "").Replace("(Clone)", "").Trim(); CreatureDecision decision = list2?.FirstOrDefault((Func<CreatureDecision, bool>)((CreatureDecision d) => d.Name == name)); bool flag = context.Bounty != null && context.Bounty.Source == name; int num = (flag ? list.IndexOf(context.Bounty.Biome) : ((decision == null) ? (-1) : list.FindIndex((Biome b) => string.Equals(((object)(*(Biome*)(&b))/*cast due to .constrained prefix*/).ToString(), decision.Biome, StringComparison.OrdinalIgnoreCase)))); if (num < 0) { return null; } ObjectDB val = ObjectDB.instance; object obj; if (val == null) { obj = null; } else { GameObject itemPrefab = val.GetItemPrefab(drop.Item); obj = ((itemPrefab != null) ? itemPrefab.GetComponent<ItemDrop>() : null); } ItemDrop val2 = (ItemDrop)obj; if (!Equipment(val2?.m_itemData)) { return null; } int num2 = Ceiling(list); bool flag2 = (flag ? (num < list.Count - 1) : (num > num2)) && Policy.Wins(random.NextDouble(), flag ? instance.bountyChance.Value : instance.chance.Value); int num3 = (flag ? Policy.BountyTarget(num, list.Count, flag2) : Policy.Target(num, num2, flag2)); GatedItemTypeHelper.AllItemsWithDetails.TryGetValue(drop.Item, out var value2); List<string> list3 = new List<string>(); List<string> list4 = new List<string>(); List<string> list5 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(from r in val.m_recipes where (Object)(object)r != (Object)null && r.m_enabled && !r.m_noCraftOnlyUpgrade && (Object)(object)r.m_item != (Object)null select ((Object)((Component)r.m_item).gameObject).name); foreach (KeyValuePair<string, GatedItemDetails> allItemsWithDetail in GatedItemTypeHelper.AllItemsWithDetails) { GatedItemDetails value3 = allItemsWithDetail.Value; if (((value3 != null) ? value3.RequiredBosses : null) == null || allItemsWithDetail.Value.RequiredBosses.Contains("none") || Tier(allItemsWithDetail.Value, list) != num3 || LootDenyList.IsDenied(allItemsWithDetail.Key) || !hashSet.Contains(allItemsWithDetail.Key)) { continue; } GameObject itemPrefab2 = val.GetItemPrefab(allItemsWithDetail.Key); ItemDrop val3 = ((itemPrefab2 != null) ? itemPrefab2.GetComponent<ItemDrop>() : null); if (Equipment(val3?.m_itemData)) { list5.Add(allItemsWithDetail.Key); if (val3.m_itemData.m_shared.m_itemType == val2.m_itemData.m_shared.m_itemType) { list4.Add(allItemsWithDetail.Key); } if (value2 != null && allItemsWithDetail.Value.Type == value2.Type) { list3.Add(allItemsWithDetail.Key); } } } if (list3.Count == 0) { list3 = ((list4.Count > 0) ? list4 : list5); } if (list3.Count == 0) { Warn("No equipment pool for " + ((object)list[num3]/*cast due to .constrained prefix*/).ToString() + "; retaining boss-gated base roll."); return null; } list3.Sort(StringComparer.Ordinal); text = (list3.Contains(drop.Item) ? drop.Item : list3[random.Next(list3.Count)]); if (instance.logFinds.Value) { instance.Logger.LogInfo((object)(((!flag) ? (flag2 ? "Lucky Find" : "Regional drop") : (flag2 ? "Lucky bounty" : "Bounty drop")) + ": " + name + " native=" + ((object)list[num]/*cast due to .constrained prefix*/).ToString() + " bossCap=" + ((object)list[num2]/*cast due to .constrained prefix*/).ToString() + " target=" + ((object)list[num3]/*cast due to .constrained prefix*/).ToString() + " " + drop.Item + " -> " + text)); } } catch (Exception ex) { Warn("Regional selection failed; retaining boss-gated roll: " + ex.Message); } context.Choices[drop] = text; return text; } internal static string Resolve(string input, GatedItemTypeMode mode) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) return selected ?? GatedItemTypeHelper.GetGatedItemNameFromItemOrType(input, mode); } private static BountyScope ReadBounty(ZDO zdo, string source, bool ragdoll = false) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Invalid comparison between Unknown and I4 //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Invalid comparison between Unknown and I4 //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) if (!Active || zdo == null) { return null; } try { string text = zdo.GetString(ragdoll ? "local-lucky-bounty-id" : "BountyID", ""); if (string.IsNullOrEmpty(text)) { return null; } BountyInfo bountyInfoByID = PlayerExtensions_Adventure.GetAdventureSaveData(Player.m_localPlayer).GetBountyInfoByID(text); if (!Policy.AcceptedBounty(hasId: true, !ragdoll && zdo.GetBool("IsAdd", false), bountyInfoByID != null && ((int)bountyInfoByID.State == 1 || (int)bountyInfoByID.State == 2))) { return null; } return new BountyScope { Source = source.Replace("(Clone)", "").Trim(), Biome = bountyInfoByID.Biome }; } catch (Exception ex) { Warn("Bounty classification unavailable; ordinary rules used: " + ex.Message); return null; } } private bool LoadStock() { string text = PlayerExtensions_Adventure.GetAdventureSaveData(Player.m_localPlayer).WorldID + "-" + Player.m_localPlayer.GetPlayerID(); string text2 = Path.Combine(Paths.ConfigPath, "LuckyFindsStock", text + ".json"); if (text2 == stockPath) { return !stockFailed; } stockPath = text2; visibleLucky.Clear(); stockFailed = false; try { stock = (File.Exists(text2) ? JsonConvert.DeserializeObject<Dictionary<string, StockEntry>>(File.ReadAllText(text2)) : new Dictionary<string, StockEntry>()); if (stock == null || stock.Values.Any((StockEntry x) => x == null)) { throw new InvalidDataException("Invalid stock history"); } return true; } catch (Exception ex) { stockFailed = true; Warn("Lucky vendor stock disabled for this session: cannot read saved history: " + ex.Message); return false; } } private bool SaveStock() { try { Directory.CreateDirectory(Path.GetDirectoryName(stockPath)); string text = stockPath + ".tmp"; File.WriteAllText(text, JsonConvert.SerializeObject((object)stock, (Formatting)1)); if (File.Exists(stockPath)) { File.Replace(text, stockPath, null); } else { File.Move(text, stockPath); } return true; } catch (Exception ex) { Warn("Could not save vendor luck history; no new announcement: " + ex.Message); return false; } } private static string UpgradeFor(SecretStashItemInfo offer, List<Biome> order, int cap, string key) { int target = Policy.VendorTarget(cap, order.Count); if (target < 0 || !offer.IsGamble || !Equipment(offer.Item) || !GatedItemTypeHelper.AllItemsWithDetails.TryGetValue(offer.ItemID, out var original)) { return null; } if (!Policy.Wins(StockRules.Sample(key + "|roll"), instance.vendorChance.Value)) { return null; } ObjectDB db = ObjectDB.instance; HashSet<string> craftable = new HashSet<string>(from r in db.m_recipes where (Object)(object)r != (Object)null && r.m_enabled && !r.m_noCraftOnlyUpgrade && (Object)(object)r.m_item != (Object)null select ((Object)((Component)r.m_item).gameObject).name); List<string> list = (from e in GatedItemTypeHelper.AllItemsWithDetails.Where(delegate(KeyValuePair<string, GatedItemDetails> e) { if (Policy.VendorCandidate(target, Tier(e.Value, order), original.Type, e.Value.Type, craftable.Contains(e.Key) && !LootDenyList.IsDenied(e.Key))) { GameObject itemPrefab = db.GetItemPrefab(e.Key); return Equipment((itemPrefab == null) ? null : itemPrefab.GetComponent<ItemDrop>()?.m_itemData); } return false; }) select e.Key).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToList(); if (list.Count != 0) { return list[(int)(StockRules.Sample(key + "|item") * (double)list.Count)]; } return null; } private static void ApplyLuckyStock(List<SecretStashItemInfo> offers, List<Biome> order, int cap) { //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_0214: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Unknown result type (might be due to invalid IL or missing references) //IL_0230: Expected O, but got Unknown instance.visibleLucky.Clear(); if (!instance.LoadStock()) { return; } try { int currentInterval = ((AdventureFeature)AdventureDataManager.Gamble).GetCurrentInterval(); Dictionary<SecretStashItemInfo, string> dictionary = new Dictionary<SecretStashItemInfo, string>(); bool flag = false; foreach (SecretStashItemInfo offer in offers) { if (offer.IsGamble && Equipment(offer.Item)) { string gambleID = GambleAdventureFeature.GetGambleID(offer); string key = currentInterval + "|" + gambleID; dictionary[offer] = key; flag |= StockRules.DecideOnce(instance.stock, key, () => UpgradeFor(offer, order, cap, instance.stockPath + "|" + key)); } } if (flag && !instance.SaveStock()) { instance.stockFailed = true; return; } for (int num = 0; num < offers.Count; num++) { SecretStashItemInfo val = offers[num]; if (!dictionary.TryGetValue(val, out var value)) { continue; } string upgrade = instance.stock[value].Upgrade; if (string.IsNullOrEmpty(upgrade)) { continue; } GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(upgrade); if (Object.op_Implicit((Object)(object)itemPrefab) && !LootDenyList.IsDenied(upgrade)) { ItemDrop component = itemPrefab.GetComponent<ItemDrop>(); if (Object.op_Implicit((Object)(object)component) && Equipment(component.m_itemData)) { ItemData val2 = component.m_itemData.Clone(); val2.m_dropPrefab = itemPrefab; val2.m_worldLevel = Game.m_worldLevel; val2.m_stack = 1; val2.m_quality = 1; val2.m_durability = val2.GetMaxDurability(); SecretStashItemInfo val3 = new SecretStashItemInfo(upgrade, val2, val.Cost.Clone(), true) { GuaranteedRarity = val.GuaranteedRarity, Rarity = val.Rarity }; Tags.Add(val3, new OfferTag { OriginalID = GambleAdventureFeature.GetGambleID(val), Key = value }); offers[num] = val3; instance.visibleLucky.Add(value); } } } } catch (Exception ex) { Warn("Lucky stock unavailable; remaining offers unchanged: " + ex.Message); } } private void Update() { if (!Active || (Object)(object)panel == (Object)null || !((Behaviour)panel).isActiveAndEnabled || visibleLucky.Count == 0 || stockFailed || stock == null) { return; } try { StockEntry value; List<string> list = (from k in visibleLucky.Distinct() where stock.TryGetValue(k, out value) && StockRules.NeedsAnnouncement(value) select k).ToList(); if (list.Count == 0) { return; } EnsureChime(); foreach (string item in list) { stock[item].Announced = true; } if (!SaveStock()) { foreach (string item2 in list) { stock[item2].Announced = false; } return; } chimeSource.PlayOneShot(chime, 0.65f); ((Character)Player.m_localPlayer).Message((MessageType)2, "Lucky Find! The vendor has special equipment.", 0, (Sprite)null, false); if (logFinds.Value) { Logger.LogInfo((object)("Vendor discovery chime: " + list.Count + " new lucky offer(s); announcement saved.")); } } catch (Exception ex) { Warn("Vendor discovery sound unavailable: " + ex.Message); } } private void EnsureChime() { if ((Object)(object)chimeSource != (Object)null && (Object)(object)chime != (Object)null) { return; } float[] array = new float[57330]; double[] array2 = new double[4] { 659.255, 830.609, 987.767, 1318.51 }; for (int i = 0; i < array2.Length; i++) { for (int j = 0; j < 26460; j++) { int num = (int)((double)i * 0.18 * 44100.0) + j; if (num >= array.Length) { break; } double num2 = (double)j / 44100.0; double num3 = Math.Min(1.0, num2 / 0.012) * Math.Exp(-7.0 * num2) * Math.Min(1.0, (0.6 - num2) / 0.06); array[num] += (float)(0.18 * Math.Sin(Math.PI * 2.0 * array2[i] * num2) * num3); } } chime = AudioClip.Create("Lucky vendor discovery", array.Length, 1, 44100, false); chime.SetData(array, 0); chimeSource = ((Component)this).gameObject.AddComponent<AudioSource>(); chimeSource.playOnAwake = false; chimeSource.spatialBlend = 0f; } } public sealed class StockEntry { public string Upgrade; public bool Announced; } internal static class StockRules { internal static double Sample(string key) { using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(key)); return (double)(uint)((array[0] << 24) | (array[1] << 16) | (array[2] << 8) | array[3]) / 4294967296.0; } internal static bool DecideOnce(Dictionary<string, StockEntry> history, string key, Func<string> roll) { if (history.ContainsKey(key)) { return false; } history.Add(key, new StockEntry { Upgrade = roll() }); return true; } internal static bool NeedsAnnouncement(StockEntry e) { if (e != null && !string.IsNullOrEmpty(e.Upgrade)) { return !e.Announced; } return false; } } internal static class Policy { internal static int Ceiling(bool[] defeated) { if (defeated == null || defeated.Length == 0) { return -1; } for (int i = 0; i < defeated.Length; i++) { if (!defeated[i]) { return i; } } return defeated.Length - 1; } internal static int Target(int native, int cap, bool lucky) { if (native >= 0 && cap >= 0) { if (!lucky) { return Math.Min(native, cap); } return native; } return -1; } internal static int BountyTarget(int region, int count, bool lucky) { if (region >= 0 && region < count) { return Math.Min(region + (lucky ? 1 : 0), count - 1); } return -1; } internal static bool AcceptedBounty(bool hasId, bool isAdd, bool acceptedOrComplete) { return hasId && !isAdd && acceptedOrComplete; } internal static int VendorTarget(int cap, int count) { if (cap < 0 || cap >= count - 1) { return -1; } return cap + 1; } internal static bool VendorCandidate(int target, int candidate, string originalType, string candidateType, bool eligible) { if (eligible && target >= 0 && candidate == target && !string.IsNullOrEmpty(originalType)) { return originalType == candidateType; } return false; } internal static bool Wins(double sample, double percent) { if (!double.IsNaN(percent) && sample >= 0.0 && sample < 1.0) { return sample < Math.Max(0.0, Math.Min(100.0, percent)) / 100.0; } return false; } } }