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 All Shop Items In Level Plus v1.0.0
REPO_All_Shop_Items_in_Level.dll
Decompiled 4 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [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 REPO_All_Shop_Items_in_Level { public static class MyPluginInfo { public const string PLUGIN_GUID = "REPO_All_Shop_Items_in_Level"; public const string PLUGIN_NAME = "ALL Shop Items spawn in Level"; public const string PLUGIN_VERSION = "1.12.0"; } [BepInPlugin("REPO_All_Shop_Items_in_Level", "ALL Shop Items spawn in Level", "1.12.0")] [BepInProcess("REPO.exe")] public class Plugin : BaseUnityPlugin { private class SpawnRecord { public GameObject Obj; public string Name; public Vector3 Pos; } private class CategorySettings { public int TypeId; public string Name; public ConfigEntry<bool> Enabled; public ConfigEntry<float> Chance; public ConfigEntry<bool> UsePrice; } [HarmonyPatch(typeof(EnemyParent), "Despawn")] private class DespawnPatch { private static int GetSpawnValuableCurrent(EnemyParent ep) { try { object? obj = AccessTools.Field(typeof(EnemyParent), "Enemy")?.GetValue(ep); Enemy obj2 = (Enemy)((obj is Enemy) ? obj : null); object? obj3 = AccessTools.Field(typeof(Enemy), "Health")?.GetValue(obj2); EnemyHealth obj4 = (EnemyHealth)((obj3 is EnemyHealth) ? obj3 : null); FieldInfo fieldInfo = AccessTools.Field(typeof(EnemyHealth), "spawnValuableCurrent"); return (!(fieldInfo == null)) ? ((int)fieldInfo.GetValue(obj4)) : 0; } catch (Exception ex) { DebugLog("Error reading spawnValuableCurrent: " + ex.Message); return 0; } } private static itemType GetLootTypeForEnemy(EnemyParent ep) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!EnableEnemyLootTables.Value) { return (itemType)8; } string text = ((Object)ep).name?.ToLower() ?? ""; if (text.Contains("gnome") || text.Contains("small")) { return (itemType)3; } return (itemType)((!text.Contains("large") && !text.Contains("boss")) ? 8 : 0); } private static void Prefix(EnemyParent __instance, out int __state) { __state = GetSpawnValuableCurrent(__instance); } private static void Postfix(EnemyParent __instance, int __state) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) if (!SemiFunc.IsMasterClientOrSingleplayer() || !SpawnHealthPacksFromEnemies.Value || GetSpawnValuableCurrent(__instance) <= __state || (double)Random.Range(0f, 100f) > (double)HealthPackDropChance.Value) { return; } itemType lootTypeForEnemy = GetLootTypeForEnemy(__instance); if ((!GetRandomItemOfType(lootTypeForEnemy, out var item) && !GetRandomItemOfType((itemType)8, out item)) || (Object)(object)item == (Object)null) { return; } try { object? obj = AccessTools.Field(typeof(EnemyParent), "Enemy")?.GetValue(__instance); Enemy val = (Enemy)((obj is Enemy) ? obj : null); if ((Object)(object)val == (Object)null) { return; } Transform val2 = (((Object)(object)val.CustomValuableSpawnTransform != (Object)null) ? val.CustomValuableSpawnTransform : val.CenterTransform); if (!((Object)(object)val2 == (Object)null)) { Vector3 val3 = val2.position + Vector3.up; if (!IsPositionValid(val3)) { DebugLog("Enemy " + ((Object)__instance).name + " has invalid spawn position, skipping drop"); return; } SpawnItem(item, val3, Quaternion.identity); DebugLog($"Dropped {((Object)item).name} from {((Object)__instance).name} ({lootTypeForEnemy})"); } } catch (Exception ex) { DebugLog("Error spawning enemy loot: " + ex.Message); } } } [HarmonyPatch] private class ValuableDirector_Spawn_Patch { private static MethodBase TargetMethod() { MethodInfo methodInfo = AccessTools.Method(typeof(ValuableDirector), "Spawn", (Type[])null, (Type[])null); return ((object)methodInfo != null) ? methodInfo : AccessTools.Method(typeof(ValuableDirector), "SpawnValuable", (Type[])null, (Type[])null); } [HarmonyPrefix] private static void Prefix(object[] __args) { ValuableVolume val = __args.OfType<ValuableVolume>().FirstOrDefault(); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.AddComponent<UsedVolumeTracker>(); } } } internal static ManualLogSource Logger; internal static ConfigEntry<bool> DebugMode; internal static ConfigEntry<bool> ExtractionSavesItems; internal static ConfigEntry<bool> SpawnHealthPacksFromEnemies; internal static ConfigEntry<float> HealthPackDropChance; internal static ConfigEntry<float> MinItemValue; internal static ConfigEntry<float> MaxItemValue; internal static ConfigEntry<int> MaxItemsPerLevel; internal static ConfigEntry<int> MinItemsPerLevel; internal static ConfigEntry<bool> EnableLevelScaling; internal static ConfigEntry<float> LevelScalingMultiplier; internal static ConfigEntry<bool> AnnounceRareItems; internal static ConfigEntry<float> AnnounceValueThreshold; internal static ConfigEntry<bool> EnableEnemyLootTables; internal static readonly Dictionary<string, ConfigEntry<bool>> ItemSpawnEntries = new Dictionary<string, ConfigEntry<bool>>(); internal static readonly Dictionary<string, ConfigEntry<bool>> ItemMapEntries = new Dictionary<string, ConfigEntry<bool>>(); private static string _lastSpawnFailReason; private static string _lastPoolFailReason; private static string _lastSkipReason; private static int _dbgTried; private static int _dbgSkipped; private static int _dbgNoItem; private static int _dbgSpawnFailed; private static readonly Dictionary<string, int> _dbgSpawnedByItem = new Dictionary<string, int>(); private static readonly List<string> _dbgFailures = new List<string>(); private static readonly List<SpawnRecord> _dbgSpawned = new List<SpawnRecord>(); private static readonly Dictionary<int, CategorySettings> CategorySettingsById = new Dictionary<int, CategorySettings>(); private static Dictionary<string, Item> _cachedItemDictionary; private static Dictionary<int, List<Item>> _cachedByType = new Dictionary<int, List<Item>>(); private static float _lastItemRefreshTime; private static bool _isSpawning = false; private static bool _mapEntityLookupDone; private static FieldInfo _mapEntityField; private static bool _mapWarned; public static Plugin Instance { get; private set; } private static string CategoryName(int typeId) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return ((object)(itemType)typeId/*cast due to .constrained prefix*/).ToString(); } private static string SanitizeConfigText(string text) { if (string.IsNullOrEmpty(text)) { return ""; } char[] array = new char[9] { '=', '\n', '\r', '\t', '\\', '"', '\'', '[', ']' }; StringBuilder stringBuilder = new StringBuilder(); foreach (char value in text) { if (Array.IndexOf(array, value) < 0) { stringBuilder.Append(value); } } return stringBuilder.ToString().Trim(); } private static void DebugResetCounters() { _dbgTried = 0; _dbgSkipped = 0; _dbgNoItem = 0; _dbgSpawnFailed = 0; _dbgSpawnedByItem.Clear(); _dbgFailures.Clear(); _dbgSpawned.Clear(); } private static string SafeName(Object o) { return (o != (Object)null) ? o.name : "<destroyed/null>"; } private static void RecordSpawnFailure(Item item, ValuableVolume volume) { _dbgSpawnFailed++; string text = "'" + SafeName((Object)(object)item) + "' at volume '" + SafeName((Object)(object)volume) + "': " + (_lastSpawnFailReason ?? "unknown reason"); _dbgFailures.Add(text); DebugLog("[SPAWN FAIL] " + text); } private static void LogSpawnSummary(int spawned, int min, int cap) { if (DebugMode == null || !DebugMode.Value) { return; } DebugLog($"[SUMMARY] volumes tried={_dbgTried} | spawned={spawned} | skipped (chance/disabled/invalid)={_dbgSkipped} | no item available={_dbgNoItem} | spawn failed={_dbgSpawnFailed} | min={min} cap={cap}"); if (_dbgSpawnedByItem.Count > 0) { DebugLog("[SUMMARY] spawned by item: " + string.Join(", ", (from kv in _dbgSpawnedByItem orderby kv.Value descending select $"{kv.Key} x{kv.Value}").ToArray())); } foreach (string dbgFailure in _dbgFailures) { DebugLog("[SUMMARY] failure: " + dbgFailure); } if ((Object)(object)Instance != (Object)null && _dbgSpawned.Count > 0) { ((MonoBehaviour)Instance).StartCoroutine(Instance.VerifySpawnedItems(new List<SpawnRecord>(_dbgSpawned))); } } private IEnumerator VerifySpawnedItems(List<SpawnRecord> snapshot) { yield return (object)new WaitForSecondsRealtime(3f); if (!SemiFunc.RunIsLevel()) { DebugLog("[VERIFY] skipped: level changed before verification"); yield break; } int ok = 0; foreach (SpawnRecord r in snapshot) { if ((Object)(object)r.Obj == (Object)null) { DebugLog($"[VERIFY] MISSING: '{r.Name}' spawned at {r.Pos} no longer exists (destroyed after spawning)"); } else if (!r.Obj.activeInHierarchy) { DebugLog($"[VERIFY] INACTIVE: '{r.Name}' spawned at {r.Pos} is not active in the scene"); } else if (r.Pos.y - r.Obj.transform.position.y > 50f) { DebugLog($"[VERIFY] FELL: '{r.Name}' spawned at {r.Pos} is now at {r.Obj.transform.position} (fell out of the map?)"); } else { ok++; } } DebugLog($"[VERIFY] {ok}/{snapshot.Count} spawned items are still present and active 3s after spawning"); } private static int VolumeClassOf(int typeId) { return typeId switch { 0 => 1, 3 => 0, _ => 2, }; } private static void Shuffle<T>(List<T> list) { for (int num = list.Count - 1; num > 0; num--) { int index = Random.Range(0, num + 1); T value = list[num]; list[num] = list[index]; list[index] = value; } } private void RegisterCategorySettings(int typeId, string typeName, string section, float otherDefaultChance) { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown CategorySettings categorySettings = new CategorySettings { TypeId = typeId, Name = typeName }; categorySettings.Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>(section, "Category enabled", true, "MASTER SWITCH. If OFF: no item of this category spawns as an extra and they are all hidden from your map, whatever the individual toggles below say. If ON: the individual toggles below apply. (Items you buy or that the game spawns are never affected by the spawn toggles.)"); if (typeId != 8) { categorySettings.Chance = ((BaseUnityPlugin)this).Config.Bind<float>(section, "SpawnChance", typeId switch { 0 => 0.95f, 3 => 2.5f, _ => otherDefaultChance, }, new ConfigDescription("% chance for a '" + typeName + "' item to spawn in a level volume (0 = never)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); } categorySettings.UsePrice = ((BaseUnityPlugin)this).Config.Bind<bool>(section, "UseShopPriceForItemSelection", true, "If ON: cheaper items of this category appear more often."); CategorySettingsById[typeId] = categorySettings; } internal static void DebugLog(string message) { if (DebugMode != null && DebugMode.Value) { Logger.LogInfo((object)("[DEBUG] " + message)); } } private void Awake() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Expected O, but got Unknown //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Expected O, but got Unknown //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Expected O, but got Unknown //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Expected O, but got Unknown //IL_0240: Unknown result type (might be due to invalid IL or missing references) //IL_024a: Expected O, but got Unknown //IL_029d: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Expected O, but got Unknown Instance = this; Logger = ((BaseUnityPlugin)this).Logger; Logger.LogInfo((object)"Plugin REPO_All_Shop_Items_in_Level is loaded!"); Harmony val = new Harmony("REPO_All_Shop_Items_in_Level"); val.PatchAll(typeof(Plugin)); val.PatchAll(typeof(DespawnPatch)); val.PatchAll(typeof(ValuableDirector_Spawn_Patch)); Logger.LogInfo((object)"Harmony patches applied!"); DebugMode = ((BaseUnityPlugin)this).Config.Bind<bool>("1_Debug", "DebugMode", false, "Enable verbose debug logging."); ExtractionSavesItems = ((BaseUnityPlugin)this).Config.Bind<bool>("0_General", "ExtractionSavesItems", true, "If ON: extra items spawned by this mod become permanent only if you bring them into the extraction point (like selling them). They vanish for the rest of the round and come back in the truck next level. Extras you leave behind are lost."); SpawnHealthPacksFromEnemies = ((BaseUnityPlugin)this).Config.Bind<bool>("5_EnemyLoot", "SpawnHealthPacksFromEnemies", true, "Whether health packs can spawn when enemies die"); HealthPackDropChance = ((BaseUnityPlugin)this).Config.Bind<float>("5_EnemyLoot", "HealthPackDropChance", 100f, new ConfigDescription("% chance for a health pack to drop when an enemy dies", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); MinItemValue = ((BaseUnityPlugin)this).Config.Bind<float>("2_Filters", "MinItemValue", 0f, new ConfigDescription("Minimum item value for spawning (0 = disabled)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 999999f), Array.Empty<object>())); MaxItemValue = ((BaseUnityPlugin)this).Config.Bind<float>("2_Filters", "MaxItemValue", 0f, new ConfigDescription("Maximum item value for spawning (0 = disabled)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 999999f), Array.Empty<object>())); MaxItemsPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("2_Filters", "MaxItemsPerLevel", 0, new ConfigDescription("Maximum items to spawn per level (0 = unlimited)", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 999), Array.Empty<object>())); MinItemsPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("2_Filters", "MinItemsPerLevel", 0, new ConfigDescription("Minimum items guaranteed to spawn per level (0 = disabled)", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 999), Array.Empty<object>())); EnableLevelScaling = ((BaseUnityPlugin)this).Config.Bind<bool>("3_LevelScaling", "EnableLevelScaling", true, "Spawn chances scale up as you progress through floors"); LevelScalingMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("3_LevelScaling", "LevelScalingMultiplier", 0.1f, new ConfigDescription("Bonus multiplier per floor completed (0.1 = +10% per floor)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); AnnounceRareItems = ((BaseUnityPlugin)this).Config.Bind<bool>("4_Announce", "AnnounceRareItems", true, "Log to console when an expensive item spawns"); AnnounceValueThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("4_Announce", "AnnounceValueThreshold", 5000f, new ConfigDescription("Min item value to trigger announce", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 999999f), Array.Empty<object>())); EnableEnemyLootTables = ((BaseUnityPlugin)this).Config.Bind<bool>("5_EnemyLoot", "EnableEnemyLootTables", true, "Specific enemy types drop specific item types instead of always health packs"); ((MonoBehaviour)this).StartCoroutine(InitializeItemListsWhenReady()); } private IEnumerator InitializeItemListsWhenReady() { float startTime = Time.realtimeSinceStartup; bool warned = false; while ((Object)(object)StatsManager.instance == (Object)null || StatsManager.instance.itemDictionary == null || StatsManager.instance.itemDictionary.Count == 0) { if (!warned && (double)Time.realtimeSinceStartup - (double)startTime > 30.0) { Logger.LogWarning((object)"StatsManager is taking a while to initialize - still waiting (no timeout)"); warned = true; } yield return (object)new WaitForSecondsRealtime(0.5f); } Logger.LogInfo((object)"Initializing per-item disable lists"); _cachedItemDictionary = StatsManager.instance.itemDictionary; int otherCategoryCount = (from i in _cachedItemDictionary.Values where (Object)(object)i != (Object)null select (int)i.itemType into id where id != 0 && id != 3 && id != 8 select id).Distinct().Count(); float otherDefaultChance = (float)Math.Round(1.5 / (double)Math.Max(1, otherCategoryCount), 3); SortedDictionary<string, int> categoryCounts = new SortedDictionary<string, int>(); bool previousSaveOnSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; foreach (IGrouping<string, Item> group in from i in _cachedItemDictionary.Values where (Object)(object)i != (Object)null group i by ((object)Unsafe.As<itemType, itemType>(ref i.itemType)/*cast due to .constrained prefix*/).ToString() into g orderby g.Key select g) { string typeName = group.Key; int typeId = (int)group.First().itemType; string section = "Category: " + typeName; RegisterCategorySettings(typeId, typeName, section, otherDefaultChance); HashSet<string> used = new HashSet<string>(); List<KeyValuePair<string, Item>> entries = new List<KeyValuePair<string, Item>>(); foreach (Item item in group.OrderBy((Item i) => ((Object)i).name)) { string assetName = ((Object)item).name; string label = SanitizeConfigText(item.itemName); if (label.Length == 0) { label = SanitizeConfigText(assetName); } if (!used.Add(label)) { label = SanitizeConfigText(label + " (" + assetName + ")"); used.Add(label); } entries.Add(new KeyValuePair<string, Item>(label, item)); } entries.Sort((KeyValuePair<string, Item> a, KeyValuePair<string, Item> b) => string.Compare(a.Key, b.Key, StringComparison.OrdinalIgnoreCase)); foreach (KeyValuePair<string, Item> entry in entries) { ItemSpawnEntries[((Object)entry.Value).name] = ((BaseUnityPlugin)this).Config.Bind<bool>(section, "Spawn in level | " + entry.Key, true, new ConfigDescription("If OFF, '" + entry.Key + "' will not spawn as an extra in levels (only used when 'Category enabled' is ON)", (AcceptableValueBase)null, Array.Empty<object>())); } foreach (KeyValuePair<string, Item> entry2 in entries) { ItemMapEntries[((Object)entry2.Value).name] = ((BaseUnityPlugin)this).Config.Bind<bool>(section, "Show on map | " + entry2.Key, true, new ConfigDescription("(Client) If OFF, '" + entry2.Key + "' is hidden from your map (only used when 'Category enabled' is ON)", (AcceptableValueBase)null, Array.Empty<object>())); } categoryCounts[typeName] = entries.Count; } ((BaseUnityPlugin)this).Config.SaveOnConfigSet = previousSaveOnSet; ((BaseUnityPlugin)this).Config.Save(); Logger.LogInfo((object)("Category sections created: " + string.Join(", ", categoryCounts.Select((KeyValuePair<string, int> kv) => $"{kv.Key}={kv.Value}").ToArray()))); Logger.LogInfo((object)$"Items with spawn disabled: {ItemSpawnEntries.Values.Count((ConfigEntry<bool> en) => !en.Value)} | hidden from map: {ItemMapEntries.Values.Count((ConfigEntry<bool> en) => !en.Value)}"); RefreshCachedItemLists(); } private static void RefreshCachedItemLists() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected I4, but got Unknown if (StatsManager.instance?.itemDictionary == null) { return; } _cachedItemDictionary = StatsManager.instance.itemDictionary; Dictionary<int, List<Item>> dictionary = new Dictionary<int, List<Item>>(); foreach (Item candidate in GetCandidates((Item i) => true)) { int key = (int)candidate.itemType; if (!dictionary.TryGetValue(key, out var value)) { value = (dictionary[key] = new List<Item>()); } value.Add(candidate); } _cachedByType = dictionary; _lastItemRefreshTime = Time.time; DebugLog("Cache refreshed: " + string.Join(", ", (from kv in dictionary orderby kv.Key select $"{CategoryName(kv.Key)}={kv.Value.Count}").ToArray())); } private static bool PassesValueFilter(Item item) { float num = (((Object)(object)item.value != (Object)null) ? item.value.valueMin : 0f); return ((double)MinItemValue.Value <= 0.0 || (double)num >= (double)MinItemValue.Value) && ((double)MaxItemValue.Value <= 0.0 || (double)num <= (double)MaxItemValue.Value); } private static bool IsItemAllowed(Item item) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected I4, but got Unknown if (CategorySettingsById.TryGetValue((int)item.itemType, out var value) && !value.Enabled.Value) { return false; } ConfigEntry<bool> value2; return !ItemSpawnEntries.TryGetValue(((Object)item).name, out value2) || value2.Value; } private static string ChanceLabel(CategorySettings c) { if (!c.Enabled.Value) { return "off"; } return GetScaledChance(c.Chance.Value).ToString("F2") + "%"; } private static List<Item> GetCandidates(Func<Item, bool> typeFilter) { if (StatsManager.instance?.itemDictionary == null) { return new List<Item>(); } return (from i in StatsManager.instance.itemDictionary.Values.Where(typeFilter) where (Object)(object)i.value != (Object)null && (double)i.value.valueMin > 0.0 select i).Where(PassesValueFilter).ToList(); } private static Item WeightedOrRandom(List<Item> list, bool usePrice) { if (list == null || list.Count == 0) { return null; } float[] array = new float[list.Count]; float num = 0f; for (int i = 0; i < list.Count; i++) { float num2 = (usePrice ? (1f / Mathf.Max(0.01f, list[i].value.valueMin)) : 1f); array[i] = num2; num += array[i]; } if (float.IsNaN(num) || float.IsInfinity(num)) { return list[Random.Range(0, list.Count)]; } if (num <= 0f) { return null; } float num3 = Random.Range(0f, num); for (int j = 0; j < list.Count; j++) { num3 -= array[j]; if (num3 <= 0f) { return list[j]; } } return list[list.Count - 1]; } private static float GetScaledChance(float baseChance) { if (!EnableLevelScaling.Value || (Object)(object)RunManager.instance == (Object)null) { return baseChance; } int levelsCompleted = RunManager.instance.levelsCompleted; return Mathf.Min(baseChance * (float)(1.0 + (double)levelsCompleted * (double)LevelScalingMultiplier.Value), 100f); } private static bool IsPositionValid(Vector3 pos) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000e: 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) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) return !float.IsNaN(pos.x) && !float.IsNaN(pos.y) && !float.IsNaN(pos.z) && pos != Vector3.zero; } private static bool IsVolumeStillValid(ValuableVolume volume) { if ((Object)(object)volume == (Object)null) { return false; } return !((Object)(object)volume == (Object)null) && !((Object)(object)((Component)volume).gameObject == (Object)null) && ((Component)volume).gameObject.activeInHierarchy; } private static void AnnounceItem(Item item, Vector3 position) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) if (AnnounceRareItems.Value && !((Object)(object)item?.value == (Object)null) && !((double)item.value.valueMin < (double)AnnounceValueThreshold.Value)) { Logger.LogInfo((object)$"[RARE SPAWN] {((Object)item).name} (value ~{item.value.valueMin}) at {position}"); } } private static bool GetRandomItemOfType(itemType type, out Item item) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected I4, but got Unknown return GetRandomItemOfType((int)type, out item); } private static bool GetRandomItemOfType(int typeId, out Item item) { item = null; _lastPoolFailReason = null; string text = CategoryName(typeId); if ((double)Time.time - (double)_lastItemRefreshTime > 60.0) { RefreshCachedItemLists(); } if (CategorySettingsById.TryGetValue(typeId, out var value) && !value.Enabled.Value) { _lastPoolFailReason = "the '" + text + "' category is turned off (master switch 'Category enabled')"; return false; } if (!_cachedByType.TryGetValue(typeId, out var value2) || value2 == null || value2.Count == 0) { _lastPoolFailReason = "the '" + text + "' item pool is empty (cache not ready, or every item was removed by MinItemValue/MaxItemValue)"; return false; } if (ItemSpawnEntries.Count > 0) { value2 = value2.Where((Item i) => IsItemAllowed(i)).ToList(); if (value2.Count == 0) { _lastPoolFailReason = "every '" + text + "' item has 'Spawn in level' turned off in the [Category: " + text + "] config section"; return false; } } CategorySettings value3; bool usePrice = !CategorySettingsById.TryGetValue(typeId, out value3) || value3.UsePrice.Value; item = WeightedOrRandom(value2, usePrice); if ((Object)(object)item == (Object)null) { _lastPoolFailReason = "weighted pick from the '" + text + "' pool returned null"; } return (Object)(object)item != (Object)null; } private static bool GetRandomShopItem(out Item item) { item = null; List<CategorySettings> list = CategorySettingsById.Values.Where((CategorySettings c) => c.Chance != null && c.Enabled.Value).ToList(); Shuffle(list); foreach (CategorySettings item3 in list) { if (GetRandomItemOfType(item3.TypeId, out var item2) && (Object)(object)item2 != (Object)null) { item = item2; return true; } } return false; } private static GameObject SpawnItem(Item item, Vector3 position, Quaternion rotation) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_01ac: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Expected I4, but got Unknown //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) _lastSpawnFailReason = null; if ((Object)(object)item == (Object)null || item.prefab == null) { _lastSpawnFailReason = "null item or null prefab reference"; DebugLog("Spawn failed: null item or prefab"); return null; } string name = ((Object)item).name; if (!IsPositionValid(position)) { _lastSpawnFailReason = $"bad spawn position {position} (NaN or zero)"; DebugLog($"Spawn of {name} rejected: bad position {position}"); return null; } if (_isSpawning) { _lastSpawnFailReason = "re-entrant spawn call blocked (another spawn was in progress)"; DebugLog("Spawn of " + name + " blocked: already inside a spawn call"); return null; } _isSpawning = true; try { string text = "Items/" + name; GameObject val; if (SemiFunc.IsMultiplayer()) { if (!PhotonNetwork.IsConnected || !PhotonNetwork.InRoom) { _lastSpawnFailReason = $"multiplayer run but not in a Photon room (connected={PhotonNetwork.IsConnected}, inRoom={PhotonNetwork.InRoom})"; DebugLog("Spawn of " + name + " blocked: not in a Photon room"); return null; } val = PhotonNetwork.InstantiateRoomObject(text, position, rotation, (byte)0, (object[])null); } else { val = Object.Instantiate<GameObject>(((PrefabRef<GameObject>)(object)item.prefab).Prefab, position, rotation); } if ((Object)(object)val != (Object)null) { val.AddComponent<SpawnedItemTracker>(); AnnounceItem(item, position); _dbgSpawnedByItem.TryGetValue(name, out var value); _dbgSpawnedByItem[name] = value + 1; _dbgSpawned.Add(new SpawnRecord { Obj = val, Name = name, Pos = position }); DebugLog($"[SPAWN OK] {name} ({CategoryName((int)item.itemType)}) at {position}"); } else { _lastSpawnFailReason = "instantiate returned null (resource path '" + text + "' missing, or the prefab could not be created)"; } return val; } catch (Exception ex) { _lastSpawnFailReason = "exception " + ex.GetType().Name + ": " + ex.Message; DebugLog($"Spawn of {name} threw: {ex}"); return null; } finally { _isSpawning = false; } } private static bool HasValuablePropSwitch(ValuableVolume volume) { return (Object)(object)((Component)volume).GetComponentInParent<ValuablePropSwitch>() != (Object)null; } private static bool ChooseCategoryForVolume(ValuableVolume volume, out int typeId) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected I4, but got Unknown typeId = -1; _lastSkipReason = null; if (HasValuablePropSwitch(volume)) { _lastSkipReason = "volume belongs to a ValuablePropSwitch"; return false; } int num = (int)volume.VolumeType; int volumeClass = num switch { 1 => 1, 0 => 0, _ => 2, }; List<CategorySettings> list = CategorySettingsById.Values.Where((CategorySettings c) => c.Chance != null && VolumeClassOf(c.TypeId) == volumeClass).ToList(); if (list.Count == 0) { _lastSkipReason = $"no item category is configured for volume type {num} (config not initialized yet?)"; return false; } List<CategorySettings> list2 = list.Where((CategorySettings c) => c.Enabled.Value).ToList(); if (list2.Count == 0) { _lastSkipReason = "every category for this volume type is turned off (master switch)"; return false; } Shuffle(list2); foreach (CategorySettings item in list2) { float scaledChance = GetScaledChance(item.Chance.Value); if ((double)scaledChance <= 0.0 || !((double)Random.Range(0f, 100f) <= (double)scaledChance)) { continue; } typeId = item.TypeId; return true; } _lastSkipReason = "chance rolls failed (" + string.Join(", ", list2.Select((CategorySettings c) => $"{c.Name} {GetScaledChance(c.Chance.Value):F2}%").ToArray()) + ")"; return false; } private static bool RandomItemSpawn(ValuableVolume volume) { //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) string text = SafeName((Object)(object)volume); _dbgTried++; if (!IsVolumeStillValid(volume)) { _dbgSkipped++; DebugLog("[SKIP] volume '" + text + "': no longer valid or not active"); return false; } if (!ChooseCategoryForVolume(volume, out var typeId)) { _dbgSkipped++; DebugLog("[SKIP] volume '" + text + "': " + _lastSkipReason); return false; } string text2 = CategoryName(typeId); if (!GetRandomItemOfType(typeId, out var item) || (Object)(object)item == (Object)null) { _dbgNoItem++; string text3 = "volume '" + text + "' (" + text2 + "): " + (_lastPoolFailReason ?? "no item available"); _dbgFailures.Add("no item - " + text3); DebugLog("[NO ITEM] " + text3); return false; } if ((Object)(object)SpawnItem(item, ((Component)volume).transform.position, ((Component)volume).transform.rotation) == (Object)null) { RecordSpawnFailure(item, volume); return false; } return true; } [HarmonyPatch(typeof(ValuableDirector), "VolumesAndSwitchSetup")] [HarmonyPostfix] public static void ValuableDirector_VolumesAndSwitchSetup_Postfix() { //IL_0368: Unknown result type (might be due to invalid IL or missing references) //IL_0374: Unknown result type (might be due to invalid IL or missing references) if (!SemiFunc.RunIsLevel() || !SemiFunc.IsMasterClientOrSingleplayer()) { return; } DebugResetCounters(); RefreshCachedItemLists(); List<ValuableVolume> list = (from v in Object.FindObjectsOfType<ValuableVolume>(false) where (Object)(object)((Component)v).gameObject.GetComponent<UsedVolumeTracker>() == (Object)null where !HasValuablePropSwitch(v) select v).ToList(); Logger.LogInfo((object)($"Found {list.Count} volumes | spawn chances: " + string.Join(", ", (from c in CategorySettingsById.Values where c.Chance != null orderby c.Name select c.Name + ":" + ChanceLabel(c)).ToArray()))); Logger.LogInfo((object)("Item pool: " + string.Join(" | ", (from kv in _cachedByType orderby kv.Key select $"{CategoryName(kv.Key)}={kv.Value.Count}").ToArray()))); int value = MaxItemsPerLevel.Value; int value2 = MinItemsPerLevel.Value; int num = 0; foreach (ValuableVolume item2 in list) { if (value <= 0 || num < value) { if (RandomItemSpawn(item2)) { num++; } continue; } DebugLog($"[CAP] reached MaxItemsPerLevel={value}, remaining volumes were not tried"); break; } if (value2 > 0 && num < value2) { DebugLog($"[MIN] only {num} spawned, below MinItemsPerLevel={value2}; filling from unused volumes"); List<ValuableVolume> list2 = list.Where((ValuableVolume v) => (Object)(object)((Component)v).gameObject.GetComponent<UsedVolumeTracker>() == (Object)null).ToList(); for (int num2 = list2.Count - 1; num2 > 0; num2--) { int num3 = Random.Range(0, num2 + 1); List<ValuableVolume> list3 = list2; int index = num2; List<ValuableVolume> list4 = list2; int num4 = num3; ValuableVolume value3 = list2[num3]; ValuableVolume val = list2[num2]; list3[index] = value3; int index2 = num4; ValuableVolume value4 = val; list4[index2] = value4; } foreach (ValuableVolume item3 in list2) { if (num >= value2 || (value > 0 && num >= value)) { break; } if (IsVolumeStillValid(item3) && GetRandomShopItem(out var item) && !((Object)(object)item == (Object)null)) { Component val2 = (Component)(object)item3; _dbgTried++; if ((Object)(object)SpawnItem(item, val2.transform.position, val2.transform.rotation) != (Object)null) { num++; } else { RecordSpawnFailure(item, item3); } } } } Logger.LogInfo((object)$"Spawned {num} items (min={value2} cap={value})"); LogSpawnSummary(num, value2, value); } private static Component GetMapIcon(MapCustom mapCustom) { if (!_mapEntityLookupDone) { _mapEntityLookupDone = true; _mapEntityField = AccessTools.Field(typeof(MapCustom), "mapCustomEntity"); if (_mapEntityField == null) { _mapEntityField = typeof(MapCustom).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((FieldInfo f) => f.FieldType.Name == "MapCustomEntity"); } if (_mapEntityField == null) { Logger.LogWarning((object)"Could not find the map icon field on MapCustom - 'Show on map' hiding is disabled (items are never deactivated)."); } } return (Component)((_mapEntityField != null) ? /*isinst with value type is only supported in some contexts*/: null); } private IEnumerator HideMapIconWhenReady(MapCustom mapCustom, string itemName) { int attempt = 0; while (attempt < 30) { yield return null; if ((Object)(object)mapCustom == (Object)null) { yield break; } Component icon = GetMapIcon(mapCustom); if ((Object)(object)icon != (Object)null) { icon.gameObject.SetActive(false); DebugLog("[MAP] hid the map icon of " + itemName + " (the item itself was not touched)"); yield break; } int num = attempt + 1; attempt = num; } if (!_mapWarned && _mapEntityField != null) { _mapWarned = true; Logger.LogWarning((object)("Map icon for " + itemName + " never appeared, so it could not be hidden (the item itself was left untouched).")); } } [HarmonyPatch(typeof(Map), "AddCustom")] [HarmonyPostfix] public static void Map_AddCustom_Postfix(MapCustom mapCustom) { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected I4, but got Unknown ItemAttributes val = default(ItemAttributes); if (!SemiFunc.RunIsLevel() || (Object)(object)mapCustom == (Object)null || (Object)(object)((Component)mapCustom).gameObject == (Object)null || !((Component)mapCustom).gameObject.TryGetComponent<ItemAttributes>(ref val) || (Object)(object)val?.item == (Object)null) { return; } string name = ((Object)val.item).name; ConfigEntry<bool> value; bool flag = ItemMapEntries.TryGetValue(name, out value) && !value.Value; CategorySettings value2; bool flag2 = CategorySettingsById.TryGetValue((int)val.item.itemType, out value2) && !value2.Enabled.Value; if (flag || flag2) { Component mapIcon = GetMapIcon(mapCustom); if ((Object)(object)mapIcon != (Object)null) { mapIcon.gameObject.SetActive(false); DebugLog("[MAP] hid the map icon of " + name + " (the item itself was not touched)"); } else if (_mapEntityField != null && (Object)(object)Instance != (Object)null) { ((MonoBehaviour)Instance).StartCoroutine(Instance.HideMapIconWhenReady(mapCustom, name)); } } } [HarmonyPatch(typeof(ExtractionPoint), "DestroyAllPhysObjectsInHaulList")] [HarmonyPostfix] public static void ExtractionPoint_DestroyAllPhysObjectsInHaulList_Postfix(ExtractionPoint __instance) { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) if (!SemiFunc.IsMasterClientOrSingleplayer() || !ExtractionSavesItems.Value) { return; } SpawnedItemTracker[] array = Object.FindObjectsOfType<SpawnedItemTracker>(false); if (array.Length == 0) { return; } ExtractionPoint[] source = Object.FindObjectsOfType<ExtractionPoint>(false); int num = 0; SpawnedItemTracker[] array2 = array; foreach (SpawnedItemTracker spawnedItemTracker in array2) { GameObject gameObject = ((Component)spawnedItemTracker).gameObject; if ((Object)(object)gameObject == (Object)null) { continue; } RoomVolumeCheck component = gameObject.GetComponent<RoomVolumeCheck>(); if (component?.CurrentRooms == null || !component.CurrentRooms.Any((RoomVolume r) => (Object)(object)r != (Object)null && r.Extraction)) { continue; } Vector3 itemPos = gameObject.transform.position; ExtractionPoint val = source.OrderBy(delegate(ExtractionPoint pt) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) Vector3 val2 = ((Component)pt).transform.position - itemPos; return ((Vector3)(ref val2)).sqrMagnitude; }).FirstOrDefault(); if ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)__instance) { continue; } ItemAttributes component2 = gameObject.GetComponent<ItemAttributes>(); if ((Object)(object)component2?.item == (Object)null) { continue; } string name = ((Object)component2.item).name; if ((Object)(object)StatsManager.instance == (Object)null) { DebugLog("StatsManager is null during extraction, skipping " + name); continue; } StatsManager.instance.ItemPurchase(name); Logger.LogInfo((object)("Extracted item saved as permanent: " + name)); PhysGrabObject component3 = gameObject.GetComponent<PhysGrabObject>(); if ((Object)(object)component3 != (Object)null) { component3.DestroyPhysGrabObject(); } else { Object.Destroy((Object)(object)gameObject); } num++; } if (num > 0) { Logger.LogInfo((object)$"Saved {num} items from extraction (they return in the truck next level)"); } } } public class SpawnedItemTracker : MonoBehaviour { } public class UsedVolumeTracker : MonoBehaviour { } }