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 LocalCraftFromStoragePrime v1.3.0
plugins/LocalCraftFromStoragePrime.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; 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 TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("LuminairPrime")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Craft from nearby chests and put materials away with one click.")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0+95b1dc7207230ba4fa936e73faaee2ae9e651dd3")] [assembly: AssemblyProduct("LocalCraftFromStoragePrime")] [assembly: AssemblyTitle("LocalCraftFromStoragePrime")] [assembly: AssemblyVersion("1.3.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 LocalCraftFromStoragePrime { internal static class LegacyConfigMigration { internal enum Kind { CopyValue, RenameKey, StoreNearbyLists } internal readonly struct StoreNearbyResult { internal string NewSend { get; } internal string NewKeep { get; } internal IReadOnlyList<string> Moved { get; } internal StoreNearbyResult(string send, string keep, IReadOnlyList<string> moved) { NewSend = send; NewKeep = keep; Moved = moved; } } internal const bool Enabled = true; internal static readonly (string OldSection, string OldKey, string NewSection, string NewKey, Kind Kind)[] Retired = new(string, string, string, string, Kind)[18] { ("01 - General", "IncludedContainerTypes", "01 - General", "ContainerTypes", Kind.RenameKey), ("01 - General", "UseContainerMarkers", "01 - General", "ContainerReserveSlot", Kind.RenameKey), ("05 - Diagnostics", "DebugLogging", "99 - Diagnostics", "DebugLogging", Kind.CopyValue), ("05 - Diagnostics", "ExportDepositItemCatalogue", "99 - Diagnostics", "ExportDepositItemCatalogue", Kind.CopyValue), ("05 - Diagnostics", "StoragePerformanceDiagnostics", "99 - Diagnostics", "StoragePerformanceDiagnostics", Kind.CopyValue), ("04 - Store Nearby", "Enabled", "05 - Store Nearby", "Enabled", Kind.CopyValue), ("04 - Store Nearby", "ButtonLabel", "05 - Store Nearby", "ButtonLabel", Kind.CopyValue), ("04 - Store Nearby", "Distance", "05 - Store Nearby", "Distance", Kind.CopyValue), ("04 - Store Nearby", "AlwaysKeep", "05 - Store Nearby", "AlwaysKeep", Kind.CopyValue), ("04 - Store Nearby", "AlwaysSend", "05 - Store Nearby", "AlwaysSend", Kind.CopyValue), ("04 - Store Nearby", "FailureMessages", "05 - Store Nearby", "FailureMessages", Kind.CopyValue), ("05 - Refuel from Storage", "Fireplace-class Fuel", "04 - Refuel from Storage", "Fireplace-class Fuel", Kind.CopyValue), ("05 - Refuel from Storage", "Smelter-class Fuel", "04 - Refuel from Storage", "Smelter-class Fuel", Kind.CopyValue), ("05 - Refuel from Storage", "Smelter-class Ore", "04 - Refuel from Storage", "Smelter-class Ore", Kind.CopyValue), ("05 - Refuel from Storage", "Smelter-class Non-Ore", "04 - Refuel from Storage", "Smelter-class Non-Ore", Kind.CopyValue), ("05 - Refuel from Storage", "Distance", "04 - Refuel from Storage", "Distance", Kind.CopyValue), ("04 - Store Nearby", "IncludedItems", "05 - Store Nearby", "AlwaysSend", Kind.StoreNearbyLists), ("04 - Store Nearby", "ExcludedItems", "05 - Store Nearby", "AlwaysKeep", Kind.StoreNearbyLists) }; internal const string OldIncludedDefaults = "raspberries, blueberries, cloudberries, vineberry, mushroomcommon, mushroomyellow, mushroomblue, jotunpuffs, magecap, smokepuff, pukeberries, trophies"; internal static HashSet<ConfigDefinition> GetSavedKeys(ConfigFile config) { HashSet<ConfigDefinition> hashSet = new HashSet<ConfigDefinition>(config.Keys); if (typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(config) is IDictionary<ConfigDefinition, string> dictionary) { hashSet.UnionWith(dictionary.Keys); } return hashSet; } internal static bool HasSavedContainerTypes(ConfigFile config) { return GetSavedKeys(config).Any((ConfigDefinition key) => key.Section == "01 - General" && (key.Key == "ContainerTypes" || key.Key == "IncludedContainerTypes")); } internal static void Apply(ConfigFile config, ManualLogSource log, bool explicitContainerTypes = true, Func<ConfigFile, object?>? readOrphans = null, ISet<ConfigDefinition>? savedKeys = null) { if (1 == 0) { return; } try { if (!(((readOrphans != null) ? readOrphans(config) : typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(config)) is IDictionary<ConfigDefinition, string> orphans)) { log.LogWarning((object)"Could not migrate old settings: unsupported BepInEx orphaned-entry shape; kept current settings and old entries."); } else { ApplyCore(config, orphans, log, explicitContainerTypes, savedKeys); } } catch (Exception ex) { log.LogWarning((object)("Could not migrate old settings (" + ex.GetType().Name + ": " + ex.Message + "). Settings may need reapplying; kept current settings and old entries.")); } } private static void ApplyCore(ConfigFile config, IDictionary<ConfigDefinition, string> orphans, ManualLogSource log, bool explicitContainerTypes, ISet<ConfigDefinition>? savedKeys) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown //IL_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Expected O, but got Unknown //IL_01cb: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Expected O, but got Unknown //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_02da: Expected O, but got Unknown //IL_02e4: Unknown result type (might be due to invalid IL or missing references) //IL_02eb: Expected O, but got Unknown //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_020f: Expected O, but got Unknown //IL_0318: Unknown result type (might be due to invalid IL or missing references) //IL_0324: Expected O, but got Unknown //IL_0223: Unknown result type (might be due to invalid IL or missing references) //IL_022f: Expected O, but got Unknown Dictionary<ConfigDefinition, string> dictionary = new Dictionary<ConfigDefinition, string>(); List<(ConfigEntryBase Entry, object Before, object After)> writes = new List<(ConfigEntryBase, object, object)>(); List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (var item2 in Retired.Where(delegate((string OldSection, string OldKey, string NewSection, string NewKey, Kind Kind) r) { Kind item = r.Kind; return (uint)item <= 1u; })) { ConfigDefinition key = new ConfigDefinition(item2.OldSection, item2.OldKey); if (!orphans.TryGetValue(key, out string value)) { continue; } if (!((IDictionary<ConfigDefinition, ConfigEntryBase>)config).TryGetValue(new ConfigDefinition(item2.NewSection, item2.NewKey), out ConfigEntryBase value2)) { log.LogWarning((object)("Kept old setting '" + item2.OldKey + "': its replacement is unavailable.")); continue; } if ((item2.Kind != Kind.RenameKey || savedKeys == null || !savedKeys.Contains(value2.Definition)) && ShouldCopyValue(value2.GetSerializedValue(), TomlTypeConverter.ConvertToString(value2.DefaultValue, value2.SettingType))) { Stage(value2, TomlTypeConverter.ConvertToValue(value, value2.SettingType)); hashSet.Add(item2.NewSection); } dictionary.Add(key, value); } (string, string, string, string, Kind) tuple = Retired.Single(((string OldSection, string OldKey, string NewSection, string NewKey, Kind Kind) r) => r.Kind == Kind.StoreNearbyLists && r.NewKey == "AlwaysSend"); (string, string, string, string, Kind) tuple2 = Retired.Single(((string OldSection, string OldKey, string NewSection, string NewKey, Kind Kind) r) => r.Kind == Kind.StoreNearbyLists && r.NewKey == "AlwaysKeep"); ConfigDefinition key2 = new ConfigDefinition(tuple.Item1, tuple.Item2); ConfigDefinition key3 = new ConfigDefinition(tuple2.Item1, tuple2.Item2); string value3; bool flag = orphans.TryGetValue(key2, out value3); string value4; bool flag2 = orphans.TryGetValue(key3, out value4); if (flag || flag2) { ConfigEntry<string> entry = default(ConfigEntry<string>); ConfigEntry<string> entry2 = default(ConfigEntry<string>); if (config.TryGetEntry<string>(new ConfigDefinition(tuple.Item3, tuple.Item4), ref entry) && config.TryGetEntry<string>(new ConfigDefinition(tuple2.Item3, tuple2.Item4), ref entry2)) { StoreNearbyResult storeNearbyResult = ComputeStoreNearby(flag ? TomlTypeConverter.ConvertToValue<string>(value3) : null, flag2 ? TomlTypeConverter.ConvertToValue<string>(value4) : null, (string)PendingValue((ConfigEntryBase)(object)entry), (string)PendingValue((ConfigEntryBase)(object)entry2)); Stage((ConfigEntryBase)(object)entry, storeNearbyResult.NewSend); Stage((ConfigEntryBase)(object)entry2, storeNearbyResult.NewKeep); list.AddRange(storeNearbyResult.Moved); if (flag) { dictionary.Add(key2, value3); } if (flag2) { dictionary.Add(key3, value4); } } else { log.LogWarning((object)"Kept old Store Nearby lists: both replacement lists must be available."); } } ConfigDefinition key4 = new ConfigDefinition("01 - General", "IncludeShipContainers"); ConfigDefinition key5 = new ConfigDefinition("01 - General", "IncludeCartContainers"); string value5; bool flag3 = orphans.TryGetValue(key4, out value5); string value6; bool flag4 = orphans.TryGetValue(key5, out value6); ConfigEntry<string> entry3 = default(ConfigEntry<string>); if ((flag3 || flag4) && config.TryGetEntry<string>(new ConfigDefinition("01 - General", "ContainerTypes"), ref entry3)) { if (!explicitContainerTypes) { bool flag5 = flag3 && TomlTypeConverter.ConvertToValue<bool>(value5); bool flag6 = flag4 && TomlTypeConverter.ConvertToValue<bool>(value6); Stage((ConfigEntryBase)(object)entry3, "chests" + (flag5 ? ", ships" : "") + (flag6 ? ", carts" : "")); } if (flag3) { dictionary.Add(key4, value5); } if (flag4) { dictionary.Add(key5, value6); } list.Add("Container selection replaced ship/cart settings"); } if (dictionary.Count == 0) { return; } bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { foreach (var item3 in writes) { item3.Entry.BoxedValue = item3.After; } foreach (ConfigDefinition key6 in dictionary.Keys) { orphans.Remove(key6); } config.Save(); } catch { foreach (KeyValuePair<ConfigDefinition, string> item4 in dictionary) { orphans[item4.Key] = item4.Value; } foreach (var item5 in writes) { item5.Entry.BoxedValue = item5.Before; } throw; } finally { config.SaveOnConfigSet = saveOnConfigSet; } foreach (string item6 in hashSet) { list.Add("Settings moved to " + item6); } log.LogInfo((object)((list.Count > 0) ? ("Moved old settings: " + string.Join("; ", list) + ". Old entries removed.") : "Removed unused old settings.")); object PendingValue(ConfigEntryBase val) { return writes.FirstOrDefault<(ConfigEntryBase, object, object)>(((ConfigEntryBase Entry, object Before, object After) write) => write.Entry == val).Item3 ?? val.BoxedValue; } void Stage(ConfigEntryBase val, object obj2) { int num = writes.FindIndex(((ConfigEntryBase Entry, object Before, object After) write) => write.Entry == val); if (num >= 0) { writes[num] = (val, writes[num].Before, obj2); } else if (!object.Equals(val.BoxedValue, obj2)) { writes.Add((val, val.BoxedValue, obj2)); } } } internal static StoreNearbyResult ComputeStoreNearby(string? oldIncluded, string? oldExcluded, string currentSend, string currentKeep) { string[] array = Tokens("raspberries, blueberries, cloudberries, vineberry, mushroomcommon, mushroomyellow, mushroomblue, jotunpuffs, magecap, smokepuff, pukeberries, trophies").ToArray(); string[] array2 = Tokens(oldIncluded).ToArray(); HashSet<string> hashSet = new HashSet<string>(array2.Select(OldKey), StringComparer.OrdinalIgnoreCase); HashSet<string> hashSet2 = new HashSet<string>(array, StringComparer.OrdinalIgnoreCase); HashSet<string> known = new HashSet<string>(Tokens(currentSend).Concat(Tokens(currentKeep)).Select(CurrentKey), StringComparer.Ordinal); List<string> list = new List<string>(); List<string> list2 = new List<string>(); string[] array3; if (oldIncluded != null) { array3 = array; foreach (string text in array3) { if (!hashSet.Contains(text)) { Append(text, list2); } } } foreach (string item in Tokens(oldExcluded)) { Append(item, list2); } array3 = array2; foreach (string token in array3) { if (!hashSet2.Contains(OldKey(token))) { Append(token, list); } } List<string> list3 = new List<string>(); if (list.Count > 0) { list3.Add("AlwaysSend += " + string.Join(", ", list)); } if (list2.Count > 0) { list3.Add("AlwaysKeep += " + string.Join(", ", list2)); } return new StoreNearbyResult(AppendText(currentSend, list), AppendText(currentKeep, list2), list3); void Append(string text3, List<string> target) { string text2 = CurrentKey(text3); if (text2.Length > 0 && known.Add(text2)) { target.Add(text3); } } } internal static bool ShouldCopyValue(string newSerialized, string defaultSerialized) { return string.Equals(newSerialized, defaultSerialized, StringComparison.Ordinal); } private static string OldKey(string token) { return TrophyKey(StorageRules.NormalizeItemName(token)); } private static string CurrentKey(string token) { return TrophyKey(DepositClassifier.Normalize(token)); } private static string TrophyKey(string key) { if (!string.Equals(key, "trophy", StringComparison.OrdinalIgnoreCase)) { return key; } return "trophies"; } private static IEnumerable<string> Tokens(string? value) { return CommaSeparatedTokens.Read(value); } private static string AppendText(string current, List<string> additions) { if (additions.Count != 0) { return (string.IsNullOrWhiteSpace(current) ? "" : (current + ", ")) + string.Join(", ", additions); } return current; } } internal sealed class PluginConfiguration { private sealed class RadiusRange : AcceptableValueRange<float> { private readonly string _setting; internal RadiusRange(string setting) : base(1f, 128f) { _setting = setting; } public override object Clamp(object value) { float num = (float)value; if (float.IsNaN(num) || float.IsInfinity(num)) { Plugin.Log.LogWarning((object)FormattableString.Invariant($"{_setting} must be a finite number from {1f} to {128f}. Using {64f} meters.")); } return RuntimeSettings.NormalizeRadius(num); } } internal const string DiagnosticsSection = "99 - Diagnostics"; private static readonly string DistanceWarning = FormattableString.Invariant($" Stay at or below {64f} m; above that containers may be unavailable or buggy."); private const string KeepSendAdditions = " Accepts comma-separated item names, prefab IDs, item keys, and category names. Categories: berries, mushrooms, produce, meat, fish, ingredients, seeds, eggs, trophies, valuables, boss items, materials, food, meads, ammo, gear, keys, quest items, fireworks, misc."; private readonly ConfigFile _config; private ConfigEntry<bool> _waitForNearbyContainers; private ConfigEntry<bool> _useContainerMarkers; private ConfigEntry<bool> _enabled; private ConfigEntry<bool> _showMaterialQuantities; private ConfigEntry<bool> _craftingFromStorage; private ConfigEntry<bool> _buildingFromStorage; private ConfigEntry<float> _craftingDistance; private ConfigEntry<float> _buildingDistance; private ConfigEntry<string> _includedContainerTypes; private ConfigEntry<string> _reserves; private IReadOnlyDictionary<string, int> _parsedReserves = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private ConfigEntry<bool> _depositEnabled; private ConfigEntry<float> _depositRange; private ConfigEntry<string> _alwaysKeep; private ConfigEntry<bool> _depositFeedback; private ConfigEntry<int> _keepPortalMaterials; private ConfigEntry<int> _keepStonePortalMaterials; private ConfigEntry<int> _keepInventoryRows; private ConfigEntry<string> _alwaysSend; private ConfigEntry<string> _depositButtonLabel; private ConfigEntry<bool> _refuelFireplaceFuel; private ConfigEntry<bool> _refuelSmelterFuel; private ConfigEntry<bool> _refuelSmelterOre; private ConfigEntry<bool> _refuelSmelterNonOre; private ConfigEntry<float> _refuelDistance; internal RuntimeSettings Current { get; private set; } internal ConfigEntry<bool> DebugLogging { get; private set; } internal ConfigEntry<bool> ExportDepositCatalogue { get; private set; } internal ConfigEntry<bool> StoragePerfDiagnostics { get; private set; } internal ConfigEntry<bool> StorageReadinessDiagnostics { get; private set; } internal ConfigEntry<bool> StorageLoadingPreview { get; private set; } internal PluginConfiguration(ConfigFile config) { _config = config; Current = RuntimeSettings.Default; } internal bool IsReservesEntry(SettingChangedEventArgs args) { return (object)args.ChangedSetting == _reserves; } internal void Bind() { //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Expected O, but got Unknown //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Expected O, but got Unknown //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_022e: Expected O, but got Unknown //IL_02b9: Unknown result type (might be due to invalid IL or missing references) //IL_02c3: Expected O, but got Unknown //IL_0336: Unknown result type (might be due to invalid IL or missing references) //IL_0340: Expected O, but got Unknown //IL_0369: Unknown result type (might be due to invalid IL or missing references) //IL_0373: Expected O, but got Unknown //IL_039c: Unknown result type (might be due to invalid IL or missing references) //IL_03a6: Expected O, but got Unknown HashSet<ConfigDefinition> savedKeys = LegacyConfigMigration.GetSavedKeys(_config); bool explicitContainerTypes = LegacyConfigMigration.HasSavedContainerTypes(_config); _enabled = _config.Bind<bool>("01 - General", "Enabled", true, "Main switch for enabling/disabling crafting, building, refueling, and Store Nearby."); _includedContainerTypes = _config.Bind<string>("01 - General", "ContainerTypes", "chests", "Choose which storage container types the mod will access. Supported aliases: chests, personal (your personal chest), ships, carts, barrel, wardrobe, pots. Supports game prefab IDs (like piece_chest_wood for a wooden chest)."); _useContainerMarkers = _config.Bind<bool>("01 - General", "ContainerReserveSlot", true, "Use Wood Arrows in a storage container’s top-right slot: 1 arrow excludes the container from all mod features, and 2–100 arrows will reserve that many of each item in that container (overriding ReserveItems)."); _reserves = _config.Bind<string>("01 - General", "ReserveItems", "", "Set the minimum quantity of each item to leave in storage containers. Example: *:1,wood:5 means nothing can be drawn down below 1, and wood can't go below 5. Variable names can be found by enabling log: ExportDepositItemCatalogue."); _craftingFromStorage = _config.Bind<bool>("02 - Craft from Storage", "Enabled", true, "Use materials from nearby storage containers when crafting."); _craftingDistance = _config.Bind<float>("02 - Craft from Storage", "Distance", 64f, new ConfigDescription("Storage search distance in meters from the player when crafting." + DistanceWarning, (AcceptableValueBase)(object)new RadiusRange("Craft from Storage Distance"), Array.Empty<object>())); _buildingFromStorage = _config.Bind<bool>("03 - Build from Storage", "Enabled", true, "Use materials from nearby storage containers when building with the Hammer."); _buildingDistance = _config.Bind<float>("03 - Build from Storage", "Distance", 64f, new ConfigDescription("Storage search distance in meters from the player when building with the Hammer." + DistanceWarning, (AcceptableValueBase)(object)new RadiusRange("Build from Storage Distance"), Array.Empty<object>())); _refuelFireplaceFuel = _config.Bind<bool>("04 - Refuel from Storage", "Fireplace-class Fuel", true, "Add wood, resin, and coal from nearby storage containers to campfires, bonfires, hearths, torches, sconces, and braziers."); _refuelSmelterFuel = _config.Bind<bool>("04 - Refuel from Storage", "Smelter-class Fuel", true, "Add coal, sap, or ice from nearby storage containers to processing stations, and wood to charcoal kilns."); _refuelSmelterOre = _config.Bind<bool>("04 - Refuel from Storage", "Smelter-class Ore", true, "Add metal ore, metal scraps, and Petrified Tissue from nearby storage containers to smelters and blast furnaces."); _refuelSmelterNonOre = _config.Bind<bool>("04 - Refuel from Storage", "Smelter-class Non-Ore", true, "Add non-ore processing inputs and fuel for stone ovens and frost foundries from nearby storage containers. Includes flax, barley, oats, oat seeds, and soft tissue for spinning wheels, windmills, and eitr refineries, plus wood for stone ovens and Liquid Frost for frost foundries."); _refuelDistance = _config.Bind<float>("04 - Refuel from Storage", "Distance", 64f, new ConfigDescription("Storage search distance in meters from the player when refueling." + DistanceWarning, (AcceptableValueBase)(object)new RadiusRange("Refuel from Storage Distance"), Array.Empty<object>())); _depositEnabled = _config.Bind<bool>("05 - Store Nearby", "Enabled", true, "Show the \"Store Nearby\" button in the player inventory."); _depositButtonLabel = _config.Bind<string>("05 - Store Nearby", "ButtonLabel", "Store Nearby", $"Set the \"Store Nearby\" button text (up to {32} characters). Leave it blank for a square button with no text."); _depositRange = _config.Bind<float>("05 - Store Nearby", "Distance", 64f, new ConfigDescription("Search distance in meters from the player when using Store Nearby." + DistanceWarning, (AcceptableValueBase)(object)new RadiusRange("Store Nearby Distance"), Array.Empty<object>())); _alwaysKeep = _config.Bind<string>("05 - Store Nearby", "AlwaysKeep", "", "Keep these items or categories in your inventory when using Store Nearby. Example: \"honey\" because you like to eat it rather than store it. AlwaysKeep takes priority over AlwaysSend. Accepts comma-separated item names, prefab IDs, item keys, and category names. Categories: berries, mushrooms, produce, meat, fish, ingredients, seeds, eggs, trophies, valuables, boss items, materials, food, meads, ammo, gear, keys, quest items, fireworks, misc."); _alwaysSend = _config.Bind<string>("05 - Store Nearby", "AlwaysSend", "", "Send these items or categories to nearby storage containers when using Store Nearby, even if normally kept. Example: food, gear. AlwaysKeep takes priority over AlwaysSend. Equipped items, kept inventory rows, quest items, and kept portal materials stay in your inventory. Accepts comma-separated item names, prefab IDs, item keys, and category names. Categories: berries, mushrooms, produce, meat, fish, ingredients, seeds, eggs, trophies, valuables, boss items, materials, food, meads, ammo, gear, keys, quest items, fireworks, misc."); _keepInventoryRows = _config.Bind<int>("05 - Store Nearby", "KeepInventoryRows", 0, new ConfigDescription("Keep items in the top row plus this many additional inventory rows when using Store Nearby.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())); _keepPortalMaterials = _config.Bind<int>("05 - Store Nearby", "KeepPortalMaterials", 0, new ConfigDescription("Keep materials for one workbench plus the selected number of wooden portals when using Store Nearby.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())); _keepStonePortalMaterials = _config.Bind<int>("05 - Store Nearby", "KeepStonePortalMaterials", 0, new ConfigDescription("Keep materials for the selected number of stone portals plus one workbench and one stonecutter. Both portal keeps share one workbench.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())); _depositFeedback = _config.Bind<bool>("05 - Store Nearby", "FailureMessages", true, "Explain why a Store Nearby click was unsuccessful."); _showMaterialQuantities = _config.Bind<bool>("99 - Diagnostics", "ShowMaterialQuantities", true, "Show required/available material quantities in crafting tooltips, including usable nearby storage. Toggle for display troubleshooting."); DebugLogging = _config.Bind<bool>("99 - Diagnostics", "DebugLogging", false, "Log container searches, item counts, item transfers, and Store Nearby decisions. Debug messages appear in Player.log. To include them in BepInEx/LogOutput.log, add Debug to [Logging.Disk] LogLevels in BepInEx/config/BepInEx.cfg and enable disk logging."); _waitForNearbyContainers = _config.Bind<bool>("99 - Diagnostics", "WaitForNearbyContainers", false, "Experimental: pause Store Nearby while known nearby containers are still appearing. Added to investigate reported failures after portal travel. Enable explicitly for testing; it does not detect every lag-related failure."); StorageReadinessDiagnostics = _config.Bind<bool>("99 - Diagnostics", "StorageReadinessDiagnostics", false, "Record nearby container loading and Store Nearby attempts in the BepInEx log. Look for [StorageTrace] lines."); StorageLoadingPreview = _config.Bind<bool>("99 - Diagnostics", "StorageLoadingPreview", false, "Test the disabled Store Nearby button and waiting message. Added to investigate reported container-loading delays after portal travel; this preview pauses sending without simulating lag. Switch it off to resume. It resets when restarting or leaving a world."); StorageLoadingPreview.Value = false; ExportDepositCatalogue = _config.Bind<bool>("99 - Diagnostics", "ExportDepositItemCatalogue", false, "Create an item list that can be used in ReserveItems and Store Nearby. Includes item names, item keys, prefab IDs, and default Store Nearby decisions for items loaded by the game and installed mods. Created after entering a world and saved to BepInEx/config/LuminairPrime.valheim.localcraftfromstorageprime.deposit-items.md."); StoragePerfDiagnostics = _config.Bind<bool>("99 - Diagnostics", "StoragePerformanceDiagnostics", false, "Log how long storage operations take and the time between game frames. Use these timings to investigate slowdowns or stuttering. Summaries appear in BepInEx/LogOutput.log without enabling DebugLogging."); LegacyConfigMigration.Apply(_config, Plugin.Log, explicitContainerTypes, null, savedKeys); } internal void ReadSettings(bool reparseReserves = true) { if (reparseReserves) { List<string> list = new List<string>(); _parsedReserves = StorageRules.ParseReserves(_reserves.Value, list.Add); foreach (string item in list) { Plugin.Log.LogWarning((object)("Ignored invalid reserve entry '" + item + "'. Use Item, Item:Count, or *:Count. Item counts may be zero; wildcard counts must be above zero.")); } } Current = new RuntimeSettings(_enabled.Value, _craftingFromStorage.Value, _buildingFromStorage.Value, _craftingDistance.Value, _buildingDistance.Value, ContainerTypePolicy.Parse(_includedContainerTypes.Value, delegate(string token, bool forbidden) { Plugin.Log.LogWarning((object)("Ignored container type '" + token + "': " + (forbidden ? "permanently forbidden" : "unsupported") + " entry.")); }), _parsedReserves, _depositEnabled.Value, _depositRange.Value, DepositPolicy.ParseTokens(_alwaysKeep.Value), _depositFeedback.Value, DepositPolicy.ParseTokens(_alwaysSend.Value), _depositButtonLabel.Value, _refuelFireplaceFuel.Value, _refuelDistance.Value, _refuelSmelterFuel.Value, _refuelSmelterOre.Value, _refuelSmelterNonOre.Value, _useContainerMarkers.Value, _keepPortalMaterials.Value, _keepInventoryRows.Value, _keepStonePortalMaterials.Value, _showMaterialQuantities.Value, _waitForNearbyContainers.Value); } } internal static class RuntimeSettingsLimits { internal const float MinDistance = 1f; internal const float MaxDistance = 128f; internal const float DefaultDistance = 64f; internal const float SafeTestedMax = 64f; } internal sealed class RuntimeSettings { internal const bool DefaultEnabled = true; internal const bool DefaultShowMaterialQuantities = true; internal const bool DefaultCraftingFromStorage = true; internal const bool DefaultBuildingFromStorage = true; internal const bool DefaultDepositEnabled = true; internal const bool DefaultDepositFeedback = true; internal const bool DefaultWaitForNearbyContainers = false; internal const bool DefaultFireplaceFuelFromStorage = true; internal const bool DefaultSmelterFuelFromStorage = true; internal const bool DefaultSmelterOreFromStorage = true; internal const bool DefaultSmelterNonOreFromStorage = true; internal const bool DefaultUseContainerMarkers = true; internal const int DefaultKeepPortalMaterials = 0; internal const int DefaultKeepStonePortalMaterials = 0; internal const int DefaultKeepInventoryRows = 0; internal static RuntimeSettings Default { get; } = new RuntimeSettings(enabled: true, craftingFromStorage: true, buildingFromStorage: true, 64f, 64f, ContainerTypePolicy.Parse("chests"), StorageRules.DefaultReserves, depositEnabled: true, 64f, (IReadOnlyCollection<string>)(object)Array.Empty<string>()); internal bool UseContainerMarkers { get; } internal int KeepPortalMaterials { get; } internal int KeepStonePortalMaterials { get; } internal int KeepInventoryRows { get; } internal bool Enabled { get; } internal bool ShowMaterialQuantities { get; } internal bool CraftingFromStorage { get; } internal bool BuildingFromStorage { get; } internal float CraftingDistance { get; } internal float BuildingDistance { get; } internal IReadOnlyCollection<string> IncludedContainerTypes { get; } internal IReadOnlyDictionary<string, int> Reserves { get; } internal bool DepositEnabled { get; } internal float DepositRange { get; } internal IReadOnlyCollection<string> AlwaysKeep { get; } internal bool DepositFeedback { get; } internal bool WaitForNearbyContainers { get; } internal IReadOnlyCollection<string> AlwaysSend { get; } internal string DepositButtonLabel { get; } internal bool FireplaceFuelFromStorage { get; } internal float RefuelDistance { get; } internal bool SmelterFuelFromStorage { get; } internal bool SmelterOreFromStorage { get; } internal bool SmelterNonOreFromStorage { get; } internal RuntimeSettings(bool enabled, bool craftingFromStorage, bool buildingFromStorage, float craftingDistance, float buildingDistance, IReadOnlyCollection<string> includedContainerTypes, IReadOnlyDictionary<string, int> reserves, bool depositEnabled, float depositRange, IReadOnlyCollection<string> alwaysKeep, bool depositFeedback = true, IReadOnlyCollection<string>? alwaysSend = null, string? depositButtonLabel = "Store Nearby", bool fireplaceFuelFromStorage = true, float refuelDistance = 64f, bool smelterFuelFromStorage = true, bool smelterOreFromStorage = true, bool smelterNonOreFromStorage = true, bool useContainerMarkers = true, int keepPortalMaterials = 0, int keepInventoryRows = 0, int keepStonePortalMaterials = 0, bool showMaterialQuantities = true, bool waitForNearbyContainers = false) { ShowMaterialQuantities = showMaterialQuantities; WaitForNearbyContainers = waitForNearbyContainers; UseContainerMarkers = useContainerMarkers; KeepPortalMaterials = InventoryKeepRules.NormalizeCount(keepPortalMaterials, 100); KeepStonePortalMaterials = InventoryKeepRules.NormalizeCount(keepStonePortalMaterials, 100); KeepInventoryRows = InventoryKeepRules.NormalizeCount(keepInventoryRows, 100); Enabled = enabled; CraftingFromStorage = craftingFromStorage; BuildingFromStorage = buildingFromStorage; CraftingDistance = NormalizeRadius(craftingDistance); BuildingDistance = NormalizeRadius(buildingDistance); IncludedContainerTypes = new HashSet<string>(includedContainerTypes, StringComparer.OrdinalIgnoreCase); Reserves = reserves; DepositEnabled = depositEnabled; DepositRange = NormalizeRadius(depositRange); AlwaysKeep = alwaysKeep; DepositFeedback = depositFeedback; AlwaysSend = (IReadOnlyCollection<string>)(((object)alwaysSend) ?? ((object)Array.Empty<string>())); DepositButtonLabel = DepositLabel.Normalize(depositButtonLabel); FireplaceFuelFromStorage = fireplaceFuelFromStorage; RefuelDistance = NormalizeRadius(refuelDistance); SmelterFuelFromStorage = smelterFuelFromStorage; SmelterOreFromStorage = smelterOreFromStorage; SmelterNonOreFromStorage = smelterNonOreFromStorage; } internal int ReserveFor(string itemName) { if (Reserves.TryGetValue(StorageRules.NormalizeItemName(itemName), out var value)) { return value; } if (!Reserves.TryGetValue("*", out var value2)) { return 0; } return value2; } internal static float NormalizeRadius(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(1f, Math.Min(128f, value)); } return 64f; } } internal sealed class GameStorageReadinessSource : IStorageReadinessSource { private readonly Vector3 _origin; private readonly RuntimeSettings _settings; private readonly bool _details; internal GameStorageReadinessSource(Vector3 origin, RuntimeSettings settings, bool details = false) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) _origin = origin; _settings = settings; _details = details; } public IEnumerable<StorageReadinessRecord> Read() { if ((Object)(object)ZNet.instance == (Object)null || (Object)(object)ZNetScene.instance == (Object)null || ZDOMan.instance == null || (Object)(object)ZoneSystem.instance == (Object)null) { throw new InvalidOperationException("world unavailable"); } Dictionary<int, string> selected = new Dictionary<int, string>(); foreach (string includedContainerType in _settings.IncludedContainerTypes) { if (ContainerTypePolicy.Allows(includedContainerType, 1L, associatedRoot: true, tombstone: false, _settings.IncludedContainerTypes)) { selected[StringExtensionMethods.GetStableHashCode(includedContainerType)] = includedContainerType; } } if (selected.Count == 0) { yield break; } Vector3 reference = ZNet.instance.GetReferencePosition(); SimulationDistance syncedSimulationDistance = ZNet.instance.GetSyncedSimulationDistance(); List<ZDO> list = new List<ZDO>(); ZDOMan.instance.FindSectorObjects(ZoneSystem.GetZone(reference), new SimulationDistance(((SimulationDistance)(ref syncedSimulationDistance)).NearSimulationDistance, 0, ((SimulationDistance)(ref syncedSimulationDistance)).IsClassic), list, (List<ZDO>)null); if (list.Count > 20000) { throw new InvalidOperationException("sector record limit"); } foreach (ZDO item in list) { if (selected.TryGetValue(item.GetPrefab(), out string value)) { Vector3 position = item.GetPosition(); Vector3 val = position - _origin; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (!(sqrMagnitude > _settings.DepositRange * _settings.DepositRange)) { bool flag = (Object)(object)ZNetScene.instance.FindInstance(item) != (Object)null; yield return new StorageReadinessRecord(((object)Unsafe.As<ZDOID, ZDOID>(ref item.m_uid)/*cast due to .constrained prefix*/).ToString(), sqrMagnitude, selected: true, item.GetLong(ZDOVars.s_creator, 0L) != 0, ZNetScene.InActiveArea(position, reference), ZoneSystem.instance.IsZoneLoaded(ZoneSystem.GetZone(position)), (Object)(object)ZNetScene.instance.GetPrefab(item.GetPrefab()) != (Object)null, flag, _details ? FormattableString.Invariant($"prefab={value} distance={Math.Sqrt(sqrMagnitude):0.0} owner={item.GetOwner()} revision={item.DataRevision} busy={item.GetInt(ZDOVars.s_inUse, 0)} instance={flag}") : ""); } } } } } internal enum EnumerateCaller { Presence, Scan, Capture } internal enum CraftPathKind { RecipeCheck, RecipeCheckNested, RecipeDisplay, BuildRequirement, BuildRequirementNested, BuildDisplay, Placement, Consumption, BuildAction, Unknown } internal enum DiscoveryMissReason { NoCache, PlayerChanged, StationChanged, RadiusChanged, OriginMoved, CacheExpired, CallbackPresent, Other } internal sealed class PerformanceCounters { internal const double WindowSeconds = 5.0; internal const double SpikeThresholdMs = 50.0; private readonly long[] _enumCounts = new long[3]; private readonly double[] _enumMsTotal = new double[3]; private readonly double[] _enumMsMax = new double[3]; private readonly long[] _enumReturnedTotal = new long[3]; private readonly int[] _enumReturnedMax = new int[3]; private long _presencePolls; private double _presenceMsTotal; private double _presenceMsMax; private long _presenceTrue; private long _presenceFalse; private long _presenceFailed; private long _discoveryHits; private double _discoveryHitMsTotal; private double _discoveryHitMsMax; private long _discoveryScans; private double _discoveryScanMsTotal; private double _discoveryScanMsMax; private long _discoveryEligibleTotal; private int _discoveryEligibleMax; private long _craftLoads; private double _craftLoadMsTotal; private double _craftLoadMsMax; private long _craftInventoriesTotal; private int _craftInventoriesMax; private long _hammerHits; private long _hammerCaptures; private double _hammerCaptureMsTotal; private double _hammerCaptureMsMax; private long _frames; private double _frameMsTotal; private double _frameMsMax; private bool _maxFrameHadEnum; private long _spikes; private long _spikesWithEnum; private long _enumFrames; private readonly long[] _buildUiCalls = new long[2]; private readonly double[] _buildUiTotal = new double[2]; private readonly double[] _buildUiMax = new double[2]; internal const int CraftPathKindCount = 10; internal const int DiscoveryMissReasonCount = 8; private static readonly double[] FrameBucketEdges = new double[20] { 5.0, 8.333333333333334, 11.11111111111111, 14.285714285714286, 16.666666666666668, 20.0, 25.0, 33.333333333333336, 50.0, 60.0, 80.0, 100.0, 150.0, 200.0, 300.0, 500.0, 1000.0, 2000.0, 5000.0, 5000.0 }; private static readonly double[] PreparationBucketEdges = new double[12] { 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 25.0, 50.0, 100.0, 500.0, 500.0 }; private static readonly string[] CraftPathKindKeys = new string[10] { "rc", "rn", "rd", "br", "bn", "bd", "pl", "cs", "ba", "uk" }; private static readonly string[] DiscoveryMissKeys = new string[8] { "nocache", "player", "station", "radius", "origin", "expired", "callback", "other" }; private readonly long[] _contextCounts = new long[10]; private readonly long[] _preparationCounts = new long[10]; private readonly long[] _cacheMisses = new long[8]; private readonly long[] _frameBuckets = new long[FrameBucketEdges.Length]; private readonly long[] _preparationBuckets = new long[PreparationBucketEdges.Length]; private long _nestedActionChecks; private long _preparations; private long _framesWithPreparation; private long _maxPreparationsPerFrame; private int _preparationsThisFrame; private double _preparationMsTotal; private double _preparationMsMax; private long _displayFallbackPreparations; private double _preparationDiscoveryMs; private double _preparationLoadMs; private double _preparationAdapterMs; private double _preparationMarkerMs; private double _preparationResidualMs; private long _loadCalls; private long _loadNullInventories; private long _loadTrue; private long _loadFalse; private long _loadThrew; private long _markerReads; private long _queries; private long _queriesExact; private long _queriesCapped; private long _queriesAnyQuality; private long _queriesSpecificQuality; private long _queriesWorldMatch; private long _snapshotHits; private long _snapshotMiss; private long _sourcesVisited; private int _maxSourcesPerQuery; private long _stacksVisited; private long _cacheSameFrameReuses; private long _cacheRevalidations; private long _entriesRevalidated; private long _entriesDropped; private long _entriesMoved; private int _preparationDepth; private CraftPathKind _preparationKind; private double _scratchDiscoveryMs; private double _scratchLoadMs; private double _scratchAdapterMs; private double _scratchMarkerMs; private int _scratchLoadCalls; private int _scratchLoadNull; private int _scratchLoadTrue; private int _scratchLoadFalse; private int _scratchLoadThrew; private int _scratchMarkerReads; private int _queryDepth; private long _scratchStacks; private string _pendingAttribution = string.Empty; internal long PresencePolls => _presencePolls; internal long DiscoveryHits => _discoveryHits; internal long DiscoveryScans => _discoveryScans; internal long CraftLoads => _craftLoads; internal long HammerHits => _hammerHits; internal long HammerCaptures => _hammerCaptures; internal long Frames => _frames; internal long Spikes => _spikes; internal long NestedActionChecks => _nestedActionChecks; internal long Preparations => _preparations; internal long FramesWithPreparations => _framesWithPreparation; internal long MaxPreparationsPerFrame => _maxPreparationsPerFrame; internal double PreparationMsTotal => _preparationMsTotal; internal double PreparationMsMax => _preparationMsMax; internal long DisplayFallbackPreparations => _displayFallbackPreparations; internal double PreparationDiscoveryMs => _preparationDiscoveryMs; internal double PreparationLoadMs => _preparationLoadMs; internal double PreparationAdapterMs => _preparationAdapterMs; internal double PreparationMarkerMs => _preparationMarkerMs; internal double PreparationResidualMs => _preparationResidualMs; internal long LoadCalls => _loadCalls; internal long LoadNullInventories => _loadNullInventories; internal long LoadTrue => _loadTrue; internal long LoadFalse => _loadFalse; internal long LoadThrew => _loadThrew; internal long MarkerReads => _markerReads; internal long Queries => _queries; internal long QueriesExact => _queriesExact; internal long QueriesCapped => _queriesCapped; internal long QueriesAnyQuality => _queriesAnyQuality; internal long QueriesSpecificQuality => _queriesSpecificQuality; internal long QueriesWorldMatch => _queriesWorldMatch; internal long SnapshotHits => _snapshotHits; internal long SnapshotMiss => _snapshotMiss; internal long SourcesVisited => _sourcesVisited; internal int MaxSourcesPerQuery => _maxSourcesPerQuery; internal long StacksVisited => _stacksVisited; internal long CacheSameFrameReuses => _cacheSameFrameReuses; internal long CacheRevalidations => _cacheRevalidations; internal long EntriesRevalidated => _entriesRevalidated; internal long EntriesDropped => _entriesDropped; internal long EntriesMoved => _entriesMoved; internal static int FrameBucketLength => FrameBucketEdges.Length; internal static int PreparationBucketLength => PreparationBucketEdges.Length; internal bool HasActivity { get { if (_enumCounts[0] + _enumCounts[1] + _enumCounts[2] <= 0 && _presencePolls <= 0 && _discoveryHits <= 0 && _discoveryScans <= 0 && _craftLoads <= 0 && _hammerHits <= 0) { return _hammerCaptures > 0; } return true; } } internal void OnBuildUi(bool rows, double elapsedMs) { int num = ((!rows) ? 1 : 0); _buildUiCalls[num]++; _buildUiTotal[num] += Math.Max(0.0, elapsedMs); _buildUiMax[num] = Math.Max(_buildUiMax[num], elapsedMs); } internal long EnumCount(EnumerateCaller caller) { return _enumCounts[(int)caller]; } internal double EnumMsTotal(EnumerateCaller caller) { return _enumMsTotal[(int)caller]; } internal double EnumMsMax(EnumerateCaller caller) { return _enumMsMax[(int)caller]; } internal long EnumReturnedTotal(EnumerateCaller caller) { return _enumReturnedTotal[(int)caller]; } internal int EnumReturnedMax(EnumerateCaller caller) { return _enumReturnedMax[(int)caller]; } internal long ContextCount(CraftPathKind kind) { return _contextCounts[(int)kind]; } internal long PreparationCount(CraftPathKind kind) { return _preparationCounts[(int)kind]; } internal long CacheMiss(DiscoveryMissReason reason) { return _cacheMisses[(int)reason]; } internal long FrameBucketCount(int index) { return _frameBuckets[index]; } internal long PreparationBucketCount(int index) { return _preparationBuckets[index]; } internal double FramePercentile(double percentile) { return PercentileEdge(_frameBuckets, FrameBucketEdges, percentile); } internal double PreparationPercentile(double percentile) { return PercentileEdge(_preparationBuckets, PreparationBucketEdges, percentile); } internal void OnEnumerate(EnumerateCaller caller, double elapsedMs, int returned) { if (elapsedMs < 0.0) { elapsedMs = 0.0; } if (returned < 0) { returned = 0; } _enumCounts[(int)caller]++; _enumMsTotal[(int)caller] += elapsedMs; if (elapsedMs > _enumMsMax[(int)caller]) { _enumMsMax[(int)caller] = elapsedMs; } _enumReturnedTotal[(int)caller] += returned; if (returned > _enumReturnedMax[(int)caller]) { _enumReturnedMax[(int)caller] = returned; } } internal void OnPresence(double elapsedMsInclusive, bool? result) { if (elapsedMsInclusive < 0.0) { elapsedMsInclusive = 0.0; } _presencePolls++; _presenceMsTotal += elapsedMsInclusive; if (elapsedMsInclusive > _presenceMsMax) { _presenceMsMax = elapsedMsInclusive; } if (result == true) { _presenceTrue++; } else if (result == false) { _presenceFalse++; } else { _presenceFailed++; } } internal void OnDiscoveryHit(double elapsedMsInclusive) { if (elapsedMsInclusive < 0.0) { elapsedMsInclusive = 0.0; } _discoveryHits++; _discoveryHitMsTotal += elapsedMsInclusive; if (elapsedMsInclusive > _discoveryHitMsMax) { _discoveryHitMsMax = elapsedMsInclusive; } } internal void OnDiscoveryScan(double elapsedMsInclusive, int eligible) { if (elapsedMsInclusive < 0.0) { elapsedMsInclusive = 0.0; } if (eligible < 0) { eligible = 0; } _discoveryScans++; _discoveryScanMsTotal += elapsedMsInclusive; if (elapsedMsInclusive > _discoveryScanMsMax) { _discoveryScanMsMax = elapsedMsInclusive; } _discoveryEligibleTotal += eligible; if (eligible > _discoveryEligibleMax) { _discoveryEligibleMax = eligible; } } internal void OnCraftLoad(double elapsedMsInclusive, int inventories) { if (elapsedMsInclusive < 0.0) { elapsedMsInclusive = 0.0; } if (inventories < 0) { inventories = 0; } _craftLoads++; _craftLoadMsTotal += elapsedMsInclusive; if (elapsedMsInclusive > _craftLoadMsMax) { _craftLoadMsMax = elapsedMsInclusive; } _craftInventoriesTotal += inventories; if (inventories > _craftInventoriesMax) { _craftInventoriesMax = inventories; } } internal void OnHammerHit() { _hammerHits++; } internal void OnHammerCapture(double elapsedMsInclusive) { if (elapsedMsInclusive < 0.0) { elapsedMsInclusive = 0.0; } _hammerCaptures++; _hammerCaptureMsTotal += elapsedMsInclusive; if (elapsedMsInclusive > _hammerCaptureMsMax) { _hammerCaptureMsMax = elapsedMsInclusive; } } internal void OnContextCreated(CraftPathKind kind) { int num = ClampKind(kind); _contextCounts[num]++; if (kind == CraftPathKind.RecipeCheckNested || kind == CraftPathKind.BuildRequirementNested) { _nestedActionChecks++; } } internal void BeginCraftPreparation(CraftPathKind kind) { _preparationDepth++; if (_preparationDepth <= 1) { ClearPreparationScratch(); _preparationKind = kind; if (_preparationsThisFrame == 0) { _framesWithPreparation++; } _preparationsThisFrame++; if (_preparationsThisFrame > _maxPreparationsPerFrame) { _maxPreparationsPerFrame = _preparationsThisFrame; } } } internal void OnDiscoveryInPreparation(double elapsedMs) { if (_preparationDepth != 0) { _scratchDiscoveryMs += Clamp(elapsedMs); } } internal void OnContainerLoad(double elapsedMs, bool delegateReturned, bool delegateSucceeded, bool inventoryLoaded) { if (_preparationDepth != 0) { _scratchLoadMs += Clamp(elapsedMs); _scratchLoadCalls++; if (!inventoryLoaded) { _scratchLoadNull++; } if (!delegateReturned) { _scratchLoadThrew++; } else if (delegateSucceeded) { _scratchLoadTrue++; } else { _scratchLoadFalse++; } } } internal void OnAdapterConstruction(double elapsedMs) { if (_preparationDepth != 0) { _scratchAdapterMs += Clamp(elapsedMs); } } internal void OnMarkerRead(double elapsedMs) { if (_preparationDepth != 0) { _scratchMarkerMs += Clamp(elapsedMs); _scratchMarkerReads++; } } internal void EndCraftPreparation(double elapsedMsInclusive) { if (_preparationDepth == 0) { return; } _preparationDepth--; if (_preparationDepth <= 0) { double num = Clamp(elapsedMsInclusive); int num2 = ClampKind(_preparationKind); _preparationCounts[num2]++; _preparations++; _preparationMsTotal += num; if (num > _preparationMsMax) { _preparationMsMax = num; } _preparationBuckets[BucketIndex(PreparationBucketEdges, num)]++; if (_preparationKind == CraftPathKind.BuildDisplay) { _displayFallbackPreparations++; } _preparationDiscoveryMs += _scratchDiscoveryMs; _preparationLoadMs += _scratchLoadMs; _preparationAdapterMs += _scratchAdapterMs; _preparationMarkerMs += _scratchMarkerMs; _loadCalls += _scratchLoadCalls; _loadNullInventories += _scratchLoadNull; _loadTrue += _scratchLoadTrue; _loadFalse += _scratchLoadFalse; _loadThrew += _scratchLoadThrew; _markerReads += _scratchMarkerReads; _preparationResidualMs += Clamp(num - _scratchDiscoveryMs - _scratchLoadMs - _scratchAdapterMs); ClearPreparationScratch(); } } internal void OnSnapshotQuery() { _queries++; _snapshotHits++; } internal void BeginCountQuery(bool exact, bool anyQuality, bool worldMatch) { _queries++; _snapshotMiss++; if (exact) { _queriesExact++; } else { _queriesCapped++; } if (anyQuality) { _queriesAnyQuality++; } else { _queriesSpecificQuality++; } if (worldMatch) { _queriesWorldMatch++; } _scratchStacks = 0L; _queryDepth++; } internal void EndCountQuery(int sourcesVisited) { if (_queryDepth != 0) { _queryDepth--; int num = Math.Max(0, sourcesVisited); _sourcesVisited += num; if (num > _maxSourcesPerQuery) { _maxSourcesPerQuery = num; } _stacksVisited += _scratchStacks; _scratchStacks = 0L; } } internal void OnItemStackInspected() { if (_queryDepth > 0) { _scratchStacks++; } } internal void OnDiscoverySameFrameReuse() { _cacheSameFrameReuses++; } internal void OnDiscoveryRevalidation(int entries, int dropped, int moved) { _cacheRevalidations++; _entriesRevalidated += Math.Max(0, entries); _entriesDropped += Math.Max(0, dropped); _entriesMoved += Math.Max(0, moved); } internal void OnDiscoveryCacheMiss(DiscoveryMissReason reason) { _cacheMisses[(int)reason]++; } internal void TickFrame(double frameMs, bool hadEnum) { if (frameMs < 0.0) { frameMs = 0.0; } _frames++; _frameMsTotal += frameMs; _frameBuckets[BucketIndex(FrameBucketEdges, frameMs)]++; if (frameMs > _frameMsMax) { _frameMsMax = frameMs; _maxFrameHadEnum = hadEnum; } if (hadEnum) { _enumFrames++; } if (frameMs > 50.0) { _spikes++; if (hadEnum) { _spikesWithEnum++; } } _preparationsThisFrame = 0; } internal void Reset() { Array.Clear(_buildUiCalls, 0, 2); Array.Clear(_buildUiTotal, 0, 2); Array.Clear(_buildUiMax, 0, 2); Array.Clear(_enumCounts, 0, _enumCounts.Length); Array.Clear(_enumMsTotal, 0, _enumMsTotal.Length); Array.Clear(_enumMsMax, 0, _enumMsMax.Length); Array.Clear(_enumReturnedTotal, 0, _enumReturnedTotal.Length); Array.Clear(_enumReturnedMax, 0, _enumReturnedMax.Length); _presencePolls = 0L; _presenceMsTotal = 0.0; _presenceMsMax = 0.0; _presenceTrue = 0L; _presenceFalse = 0L; _presenceFailed = 0L; _discoveryHits = 0L; _discoveryHitMsTotal = 0.0; _discoveryHitMsMax = 0.0; _discoveryScans = 0L; _discoveryScanMsTotal = 0.0; _discoveryScanMsMax = 0.0; _discoveryEligibleTotal = 0L; _discoveryEligibleMax = 0; _craftLoads = 0L; _craftLoadMsTotal = 0.0; _craftLoadMsMax = 0.0; _craftInventoriesTotal = 0L; _craftInventoriesMax = 0; _hammerHits = 0L; _hammerCaptures = 0L; _hammerCaptureMsTotal = 0.0; _hammerCaptureMsMax = 0.0; _frames = 0L; _frameMsTotal = 0.0; _frameMsMax = 0.0; _maxFrameHadEnum = false; _spikes = 0L; _spikesWithEnum = 0L; _enumFrames = 0L; Array.Clear(_contextCounts, 0, _contextCounts.Length); Array.Clear(_preparationCounts, 0, _preparationCounts.Length); Array.Clear(_cacheMisses, 0, _cacheMisses.Length); Array.Clear(_frameBuckets, 0, _frameBuckets.Length); Array.Clear(_preparationBuckets, 0, _preparationBuckets.Length); _nestedActionChecks = 0L; _preparations = 0L; _framesWithPreparation = 0L; _maxPreparationsPerFrame = 0L; _preparationsThisFrame = 0; _preparationMsTotal = 0.0; _preparationMsMax = 0.0; _displayFallbackPreparations = 0L; _preparationDiscoveryMs = 0.0; _preparationLoadMs = 0.0; _preparationAdapterMs = 0.0; _preparationMarkerMs = 0.0; _preparationResidualMs = 0.0; _loadCalls = 0L; _loadNullInventories = 0L; _loadTrue = 0L; _loadFalse = 0L; _loadThrew = 0L; _markerReads = 0L; _queries = 0L; _queriesExact = 0L; _queriesCapped = 0L; _queriesAnyQuality = 0L; _queriesSpecificQuality = 0L; _queriesWorldMatch = 0L; _snapshotHits = 0L; _snapshotMiss = 0L; _sourcesVisited = 0L; _maxSourcesPerQuery = 0; _stacksVisited = 0L; _cacheSameFrameReuses = 0L; _cacheRevalidations = 0L; _entriesRevalidated = 0L; _entriesDropped = 0L; _entriesMoved = 0L; _preparationDepth = 0; _preparationKind = CraftPathKind.Unknown; _queryDepth = 0; _scratchStacks = 0L; ClearPreparationScratch(); } private void ClearPreparationScratch() { _scratchDiscoveryMs = 0.0; _scratchLoadMs = 0.0; _scratchAdapterMs = 0.0; _scratchMarkerMs = 0.0; _scratchLoadCalls = 0; _scratchLoadNull = 0; _scratchLoadTrue = 0; _scratchLoadFalse = 0; _scratchLoadThrew = 0; _scratchMarkerReads = 0; } private static double Clamp(double value) { if (!(value < 0.0)) { return value; } return 0.0; } private static int ClampKind(CraftPathKind kind) { if (kind >= CraftPathKind.RecipeCheck && kind < (CraftPathKind)10) { return (int)kind; } return 9; } private static int BucketIndex(double[] edges, double value) { for (int i = 0; i < edges.Length - 1; i++) { if (value < edges[i]) { return i; } } return edges.Length - 1; } private double PercentileEdge(long[] buckets, double[] edges, double percentile) { long num = 0L; for (int i = 0; i < buckets.Length; i++) { num += buckets[i]; } if (num == 0L) { return 0.0; } double num2 = Math.Ceiling((double)num * Math.Clamp(percentile, 0.0, 1.0)); if (num2 < 1.0) { num2 = 1.0; } long num3 = 0L; for (int j = 0; j < buckets.Length; j++) { num3 += buckets[j]; if ((double)num3 >= num2) { return edges[j]; } } return edges[^1]; } internal bool TryCloseWindow(double windowSeconds, int gc0Delta, int gc1Delta, int gc2Delta, out string summary) { summary = string.Empty; if (windowSeconds < 5.0 || (_frames == 0L && !HasActivity)) { _pendingAttribution = string.Empty; if (windowSeconds >= 5.0) { Reset(); } return false; } double num = ((_frames > 0) ? (_frameMsTotal / (double)_frames) : 0.0); summary = string.Format(CultureInfo.InvariantCulture, "[PerfDiag] backend=registry {0:0.0}s window: frames={1} avgFrame={2:0.0}ms maxFrame={3:0.0}ms(maxEnum={4}) spikes>50ms={5}(enum={6}) enumFrames={7} | enum presence={8} total={9:0.00}ms max={10:0.00}ms returned={11}(max{12}) | enum scan={13} total={14:0.00}ms max={15:0.00}ms returned={16}(max{17}) | enum capture={18} total={19:0.00}ms max={20:0.00}ms returned={21}(max{22}) | presence(incl.)={23} total={24:0.00}ms max={25:0.00}ms true={26} false={27} failed={28} | discovery hits={29} total={30:0.00}ms max={31:0.00}ms scans(incl.)={32} total={33:0.00}ms max={34:0.00}ms eligible={35}(max{36}) | craftLoad(incl.)={37} total={38:0.00}ms max={39:0.00}ms inv={40}(max{41}) | hammer hits={42} captures(incl.)={43} total={44:0.00}ms max={45:0.00}ms | gc0=+{46} gc1=+{47} gc2=+{48} (Timers are elapsed wall time including GC/descheduling; frame intervals also include render/other work. Do not sum inclusive timings.)", windowSeconds, _frames, num, _frameMsMax, _maxFrameHadEnum ? "yes" : "no", _spikes, _spikesWithEnum, _enumFrames, _enumCounts[0], _enumMsTotal[0], _enumMsMax[0], _enumReturnedTotal[0], _enumReturnedMax[0], _enumCounts[1], _enumMsTotal[1], _enumMsMax[1], _enumReturnedTotal[1], _enumReturnedMax[1], _enumCounts[2], _enumMsTotal[2], _enumMsMax[2], _enumReturnedTotal[2], _enumReturnedMax[2], _presencePolls, _presenceMsTotal, _presenceMsMax, _presenceTrue, _presenceFalse, _presenceFailed, _discoveryHits, _discoveryHitMsTotal, _discoveryHitMsMax, _discoveryScans, _discoveryScanMsTotal, _discoveryScanMsMax, _discoveryEligibleTotal, _discoveryEligibleMax, _craftLoads, _craftLoadMsTotal, _craftLoadMsMax, _craftInventoriesTotal, _craftInventoriesMax, _hammerHits, _hammerCaptures, _hammerCaptureMsTotal, _hammerCaptureMsMax, gc0Delta, gc1Delta, gc2Delta); summary += string.Format(CultureInfo.InvariantCulture, " | buildRows(incl.)={0} total={1:0.00}ms max={2:0.00}ms buildIcons(excl.capture)={3} total={4:0.00}ms max={5:0.00}ms (mod UI only; deferred canvas work excluded)", _buildUiCalls[0], _buildUiTotal[0], _buildUiMax[0], _buildUiCalls[1], _buildUiTotal[1], _buildUiMax[1]); _pendingAttribution = BuildAttributionLine(); Reset(); return true; } internal bool TryCloseAttributionWindow(out string line) { line = _pendingAttribution; _pendingAttribution = string.Empty; return line.Length > 0; } private string BuildAttributionLine() { return string.Format(CultureInfo.InvariantCulture, "[PerfDiag] attr: frames={0} ctx={1} prep={2} nested={3} framesWithPrep={4} maxPrepPerFrame={5} prepTotal={6} | discoverMs={7:0.00} loadCalls={8} loadNull={9} loadTrue={10} loadFalse={11} loadMs={12:0.00} adapterMs={13:0.00} markerReads={14} markerMs={15:0.00} residualMs={16:0.00} | queries={17} exact={18} capped={19} anyQuality={20} quality={21} worldMatch={22} snapshotHits={23} snapshotMiss={24} displayFallbackPrep={25} sourcesVisited={26} maxSourcesPerQuery={27} stacksVisited={28} | cacheSameFrame={29} cacheRevalidated={30} entriesRevalidated={31} entriesDropped={32} entriesMoved={33} miss={34} | frameHist={35} p50={36:0.00}ms p95={37:0.00}ms p99={38:0.00}ms | prepHist={39} prepP50={40:0.00}ms prepP95={41:0.00}ms prepP99={42:0.00}ms (ctx/prep keys rc,rn,rd,br,bn,bd,pl,cs,ba,uk; miss keys nocache,player,station,radius,origin,expired,callback,other; p50/p95/p99 are bucket-upper-edge estimates from per-frame samples with precision equal to the bucket width, not percentiles of five-second means, and are 0.00ms when that histogram has no samples; discoverMs/loadMs/adapterMs are exclusive segments inside craftLoad(incl.) with markerMs nested inside adapterMs, so they never sum exactly and residualMs is the unattributed remainder of the inclusive timer; markerReads equals containers prepared inside a preparation, stacksVisited is counted only while diagnostics are on, queries equals snapshotHits plus snapshotMiss (exact plus capped), and snapshotHits/snapshotMiss split queries by whether the display snapshot answered them, which is not the discovery cache misses in miss=)", _frames, KindList(_contextCounts), KindList(_preparationCounts), _nestedActionChecks, _framesWithPreparation, _maxPreparationsPerFrame, _preparations, _preparationDiscoveryMs, _loadCalls, _loadNullInventories, _loadTrue, _loadFalse, _preparationLoadMs, _preparationAdapterMs, _markerReads, _preparationMarkerMs, _preparationResidualMs, _queries, _queriesExact, _queriesCapped, _queriesAnyQuality, _queriesSpecificQuality, _queriesWorldMatch, _snapshotHits, _snapshotMiss, _displayFallbackPreparations, _sourcesVisited, _maxSourcesPerQuery, _stacksVisited, _cacheSameFrameReuses, _cacheRevalidations, _entriesRevalidated, _entriesDropped, _entriesMoved, ReasonList(), BucketList(_frameBuckets), PercentileEdge(_frameBuckets, FrameBucketEdges, 0.5), PercentileEdge(_frameBuckets, FrameBucketEdges, 0.95), PercentileEdge(_frameBuckets, FrameBucketEdges, 0.99), BucketList(_preparationBuckets), PercentileEdge(_preparationBuckets, PreparationBucketEdges, 0.5), PercentileEdge(_preparationBuckets, PreparationBucketEdges, 0.95), PercentileEdge(_preparationBuckets, PreparationBucketEdges, 0.99)); } private static string KindList(long[] counts) { StringBuilder stringBuilder = new StringBuilder(counts.Length * 7); for (int i = 0; i < counts.Length; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append(CraftPathKindKeys[i]).Append(':').Append(counts[i].ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } private string ReasonList() { StringBuilder stringBuilder = new StringBuilder(88); for (int i = 0; i < _cacheMisses.Length; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append(DiscoveryMissKeys[i]).Append(':').Append(_cacheMisses[i].ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } private static string BucketList(long[] buckets) { StringBuilder stringBuilder = new StringBuilder(buckets.Length * 5); for (int i = 0; i < buckets.Length; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append(buckets[i].ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } } internal static class PerformanceDiagnostics { internal static readonly PerformanceCounters Counters = new PerformanceCounters(); private static readonly PreparationTrace PreparationTrace = new PreparationTrace(); private static bool _hasBaseline; private static float _lastFrameTime; private static float _windowStart; private static bool _hadEnumSinceTick; private static int _gc0Start; private static int _gc1Start; private static int _gc2Start; internal static bool Enabled => Plugin.StoragePerfDiagnostics?.Value ?? false; internal static long HammerCaptures => Counters.HammerCaptures; internal static long Timestamp() { try { if (!Enabled) { return 0L; } return Stopwatch.GetTimestamp(); } catch { return 0L; } } internal static double ElapsedMs(long start) { try { return (double)(Stopwatch.GetTimestamp() - start) * 1000.0 / (double)Stopwatch.Frequency; } catch { return 0.0; } } internal static void OnBuildUi(bool rows, long start) { if (Enabled && start != 0L) { Counters.OnBuildUi(rows, ElapsedMs(start)); } } internal static void OnEnumerate(EnumerateCaller caller, long start, int returned) { if (Enabled) { Counters.OnEnumerate(caller, ElapsedMs(start), returned); _hadEnumSinceTick = true; } } internal static void OnPresence(long start, bool? result) { if (Enabled) { Counters.OnPresence(ElapsedMs(start), result); } } internal static void OnDiscoveryHit(long start) { if (Enabled) { Counters.OnDiscoveryHit(ElapsedMs(start)); PreparationTrace.Hit(); } } internal static void OnDiscoveryScan(long start, int eligible) { if (Enabled) { Counters.OnDiscoveryScan(ElapsedMs(start), eligible); PreparationTrace.Scan(); } } internal static void OnCraftLoad(long start, int inventories) { if (Enabled) { Counters.OnCraftLoad(ElapsedMs(start), inventories); PreparationTrace.Complete(inventories); } } internal static void OnHammerCapture(long start) { if (Enabled) { Counters.OnHammerCapture(ElapsedMs(start)); } } internal static void OnHammerHit() { if (Enabled) { Counters.OnHammerHit(); } } internal static void OnContextCreated(CraftPathKind kind) { if (Enabled) { Counters.OnContextCreated(kind); } } internal static bool BeginCraftPreparation(CraftPathKind kind) { if (!Enabled) { return false; } PreparationTrace.Begin(kind, Time.frameCount, (!PreparationTrace.IsPreparing) ? GC.CollectionCount(0) : 0); Counters.BeginCraftPreparation(kind); return true; } internal static void EndCraftPreparation(bool tracked, long start) { if (tracked) { double num = ElapsedMs(start); PreparationTrace.End(num, GC.CollectionCount(0)); Counters.EndCraftPreparation(num); } } internal static void OnDiscoveryInPreparation(long start) { if (Enabled && start != 0L) { double num = ElapsedMs(start); Counters.OnDiscoveryInPreparation(num); PreparationTrace.Discovery(num); } } internal static void OnContainerLoad(long start, bool delegateReturned, bool delegateSucceeded, bool inventoryLoaded) { if (Enabled && start != 0L) { double num = ElapsedMs(start); Counters.OnContainerLoad(num, delegateReturned, delegateSucceeded, inventoryLoaded); PreparationTrace.Load(num, delegateReturned, delegateSucceeded, inventoryLoaded); } } internal static void OnAdapterConstruction(long start) { if (Enabled && start != 0L) { double num = ElapsedMs(start); Counters.OnAdapterConstruction(num); PreparationTrace.Adapter(num); } } internal static void OnMarkerRead(long start) { if (Enabled && start != 0L) { double num = ElapsedMs(start); Counters.OnMarkerRead(num); PreparationTrace.Marker(num); } } internal static void OnSnapshotQuery() { if (Enabled) { Counters.OnSnapshotQuery(); } } internal static bool BeginCountQuery(bool exact, bool anyQuality, bool worldMatch) { if (!Enabled) { return false; } Counters.BeginCountQuery(exact, anyQuality, worldMatch); return true; } internal static void EndCountQuery(bool tracked, int sourcesVisited) { if (Enabled && tracked) { Counters.EndCountQuery(sourcesVisited); } } internal static void OnItemStackInspected() { Counters.OnItemStackInspected(); } internal static void OnDiscoverySameFrameReuse() { if (Enabled) { Counters.OnDiscoverySameFrameReuse(); } } internal static void OnDiscoveryRevalidation(int entries, int dropped, int moved) { if (Enabled) { Counters.OnDiscoveryRevalidation(entries, dropped, moved); PreparationTrace.Revalidate(); } } internal static void OnDiscoveryCacheMiss(DiscoveryMissReason reason) { if (Enabled) { Counters.OnDiscoveryCacheMiss(reason); PreparationTrace.Miss(reason); } } internal static void TickFrame() { float realtimeSinceStartup; try { realtimeSinceStartup = Time.realtimeSinceStartup; } catch { return; } if (!Enabled) { if (_hasBaseline) { PreparationTrace.Reset(); } _hasBaseline = false; _lastFrameTime = realtimeSinceStartup; _windowStart = realtimeSinceStartup; _hadEnumSinceTick = false; return; } if (!_hasBaseline) { _hasBaseline = true; _lastFrameTime = realtimeSinceStartup; _windowStart = realtimeSinceStartup; _hadEnumSinceTick = false; _gc0Start = GC.CollectionCount(0); _gc1Start = GC.CollectionCount(1); _gc2Start = GC.CollectionCount(2); Counters.Reset(); PreparationTrace.Reset(); return; } double num = (double)(realtimeSinceStartup - _lastFrameTime) * 1000.0; _lastFrameTime = realtimeSinceStartup; bool hadEnumSinceTick = _hadEnumSinceTick; _hadEnumSinceTick = false; if (num < 0.0) { num = 0.0; } Counters.TickFrame(num, hadEnumSinceTick); PreparationTrace.Tick(Time.frameCount, num); if (num > 50.0) { Plugin.Log.LogInfo((object)string.Format("[PerfDiag] slow frame {0:0.0}ms enum={1}.", num, hadEnumSinceTick ? "yes" : "no")); } double num2 = realtimeSinceStartup - _windowStart; if (!(num2 >= 5.0)) { return; } int num3 = GC.CollectionCount(0); int num4 = GC.CollectionCount(1); int num5 = GC.CollectionCount(2); if (Counters.TryCloseWindow(num2, num3 - _gc0Start, num4 - _gc1Start, num5 - _gc2Start, out string summary)) { Plugin.Log.LogInfo((object)summary); if (Counters.TryCloseAttributionWindow(out string line)) { Plugin.Log.LogInfo((object)line); } Plugin.Log.LogInfo((object)PreparationTrace.FormatWindow()); } PreparationTrace.ClearWindow(); _windowStart = realtimeSinceStartup; _gc0Start = num3; _gc1Start = num4; _gc2Start = num5; } } internal struct PreparationTraceSample { internal CraftPathKind Kind; internal int Frame; internal int NextTickFrame; internal int Sources; internal int Loads; internal int LoadTrue; internal int LoadFalse; internal int LoadThrew; internal int LoadNull; internal int Hits; internal int Scans; internal int Revalidations; internal int MissMask; internal int GcCollections; internal bool Completed; internal bool HasNextTick; internal double Ms; internal double DiscoveryMs; internal double LoadMs; internal double MaxLoadMs; internal double AdapterMs; internal double MarkerMs; internal double OtherMs; internal double NextTickMs; } internal struct PreparationTickSample { internal int Frame; internal int Preparations; internal double IntervalMs; internal double PreparationMs; internal double MaxPreparationMs; } internal sealed class PreparationTrace { private int _depth; private int _gcStart; private int _tickPreparations; private double _tickPreparationMs; private double _tickMaxPreparationMs; private PreparationTraceSample _scratch; private PreparationTraceSample _peak; private PreparationTickSample _worstTick; internal bool IsPreparing => _depth > 0; internal bool HasPeak { get; private set; } internal bool HasTick { get; private set; } internal PreparationTraceSample Peak => _peak; internal PreparationTickSample WorstTick => _worstTick; internal void Begin(CraftPathKind kind, int frame, int gcCollections) { if (++_depth == 1) { _scratch = new PreparationTraceSample { Kind = kind, Frame = frame, Sources = -1 }; _gcStart = gcCollections; } } internal void Discovery(double ms) { if (IsPreparing) { _scratch.DiscoveryMs += Positive(ms); } } internal void Adapter(double ms) { if (IsPreparing) { _scratch.AdapterMs += Positive(ms); } } internal void Marker(double ms) { if (IsPreparing) { _scratch.MarkerMs += Positive(ms); } } internal void Hit() { if (IsPreparing) { _scratch.Hits++; } } internal void Scan() { if (IsPreparing) { _scratch.Scans++; } } internal void Revalidate() { if (IsPreparing) { _scratch.Revalidations++; } } internal void Miss(DiscoveryMissReason reason) { if (IsPreparing) { _scratch.MissMask |= 1 << (int)reason; } } internal void Load(double ms, bool returned, bool succeeded, bool hasInventory) { if (IsPreparing) { ms = Positive(ms); _scratch.LoadMs += ms; _scratch.MaxLoadMs = Math.Max(_scratch.MaxLoadMs, ms); _scratch.Loads++; if (!hasInventory) { _scratch.LoadNull++; } if (!returned) { _scratch.LoadThrew++; } else if (succeeded) { _scratch.LoadTrue++; } else { _scratch.LoadFalse++; } } } internal void Complete(int sources) { if (_depth == 1) { _scratch.Sources = sources; _scratch.Completed = true; } } internal void End(double ms, int gcCollections) { if (_depth != 0 && --_depth == 0) { _scratch.Ms = Positive(ms); _scratch.GcCollections = Math.Max(0, gcCollections - _gcStart); _scratch.OtherMs = Positive(_scratch.Ms - _scratch.DiscoveryMs - _scratch.LoadMs - _scratch.AdapterMs); _tickPreparations++; _tickPreparationMs += _scratch.Ms; _tickMaxPreparationMs = Math.Max(_tickMaxPreparationMs, _scratch.Ms); if (!HasPeak || _scratch.Ms > _peak.Ms) { _peak = _scratch; HasPeak = true; } _scratch = default(PreparationTraceSample); } } internal void Tick(int frame, double intervalMs) { intervalMs = Positive(intervalMs); if (HasPeak && !_peak.HasNextTick) { _peak.NextTickFrame = frame; _peak.NextTickMs = intervalMs; _peak.HasNextTick = true; } if (!HasTick || intervalMs > _worstTick.IntervalMs) { _worstTick = new PreparationTickSample { Frame = frame, IntervalMs = intervalMs, Preparations = _tickPreparations, PreparationMs = _tickPreparationMs, MaxPreparationMs = _tickMaxPreparationMs }; HasTick = true; } _tickPreparations = 0; _tickPreparationMs = (_tickMaxPreparationMs = 0.0); } internal string FormatWindow() { return FormattableString.Invariant($"[PerfDiag] prepTrace: schema=1 hasCall={(HasPeak ? 1 : 0)} callFrame={_peak.Frame} mode={_peak.Kind} completed={_peak.Completed} ms={_peak.Ms:F3} discMs={_peak.DiscoveryMs:F3} loadMs={_peak.LoadMs:F3} maxLoadMs={_peak.MaxLoadMs:F3} adapterMs={_peak.AdapterMs:F3} markerNestedMs={_peak.MarkerMs:F3} otherMs={_peak.OtherMs:F3} sources={_peak.Sources} loads={_peak.Loads} loadTrue={_peak.LoadTrue} loadFalse={_peak.LoadFalse} loadNull={_peak.LoadNull} loadThrew={_peak.LoadThrew} hits={_peak.Hits} scans={_peak.Scans} reval={_peak.Revalidations} missMask={_peak.MissMask} gcDuringCall={_peak.GcCollections} hasNextTick={_peak.HasNextTick} nextTickFrame={_peak.NextTickFrame} nextTickMs={_peak.NextTickMs:F3} | worstTickFrame={_worstTick.Frame} intervalMs={_worstTick.IntervalMs:F3} preps={_worstTick.Preparations} prepMs={_worstTick.PreparationMs:F3} maxPrepMs={_worstTick.MaxPreparationMs:F3}"); } internal void ClearWindow() { bool hasPeak = (HasTick = false); HasPeak = hasPeak; _peak = default(PreparationTraceSample); _worstTick = default(PreparationTickSample); } internal void Reset() { ClearWindow(); _depth = (_gcStart = (_tickPreparations = 0)); _scratch = default(PreparationTraceSample); _tickPreparationMs = (_tickMaxPreparationMs = 0.0); } private static double Positive(double ms) { return Math.Max(0.0, ms); } } internal static class StorageReadinessMonitor { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__17_0; internal void <RegisterPreviewCommand>b__17_0(ConsoleEventArgs args) { bool flag = args.Length == 2 && string.Equals(args[1], "on", StringComparison.OrdinalIgnoreCase); bool flag2 = args.Length == 2 && string.Equals(args[1], "off", StringComparison.OrdinalIgnoreCase); if (!flag && !flag2) { args.Context.AddString("Usage: lcfs_storage_preview on|off"); return; } ConfigEntry<bool> storageLoadingPreview = Plugin.StorageLoadingPreview; if (storageLoadingPreview == null || (flag && (Object)(object)Player.m_localPlayer == (Object)null)) { args.Context.AddString("Enter a world before previewing Store Nearby."); return; } storageLoadingPreview.Value = flag; StorageTrace.Event("presentation-preview", $"enabled={flag}", 0L); DepositButton.RefreshOperationState(); args.Context.AddString(flag ? "Store Nearby preview enabled. Sending is paused; use lcfs_storage_preview off to finish." : "Store Nearby preview disabled. No items were sent."); } } internal const double SampleInterval = 1.0; private static double _next; private static double _heartbeat; private static Player? _player; private static Vector3 _origin; private static bool _teleporting; private static bool _visible; private static string _lastSummary = ""; private static readonly Dictionary<string, string> LastDetails = new Dictionary<string, string>(); internal static StorageReadinessSnapshot Current { get; private set; } = StorageReadinessSnapshot.Unknown("not sampled"); private static bool FeatureEnabled { get { if (Plugin.Settings.Enabled) { return Plugin.Settings.DepositEnabled; } return false; } } internal static bool Blocked => StorageReadinessProbe.ShouldBlock(FeatureEnabled, Plugin.Settings.WaitForNearbyContainers, Plugin.StorageLoadingPreview?.Value ?? false, Current); internal static void RegisterPreviewCommand() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown object obj = <>c.<>9__17_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { bool flag = args.Length == 2 && string.Equals(args[1], "on", StringComparison.OrdinalIgnoreCase); bool flag2 = args.Length == 2 && string.Equals(args[1], "off", StringComparison.OrdinalIgnoreCase); if (!flag && !flag2) { args.Context.AddString("Usage: lcfs_storage_preview on|off"); } else { ConfigEntry<bool> storageLoadingPreview = Plugin.StorageLoadingPreview; if (storageLoadingPreview == null || (flag && (Object)(object)Player.m_localPlayer == (Object)null)) { args.Context.AddString("Enter a world before previewing Store Nearby."); } else { storageLoadingPreview.Value = flag; StorageTrace.Event("presentation-preview", $"enabled={flag}", 0L); DepositButton.RefreshOperationState(); args.Context.AddString(flag ? "Store Nearby preview enabled. Sending is paused; use lcfs_storage_preview off to finish." : "Store Nearby preview disabled. No items were sent."); } } }; <>c.<>9__17_0 = val; obj = (object)val; } new ConsoleCommand("lcfs_storage_preview", "[on|off] Preview the Store Nearby waiting state.", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } internal static void Tick(bool force = false) { //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) StorageTrace.Configure(); Player localPlayer = Player.m_localPlayer; if (!FeatureEnabled || (Object)(object)localPlayer == (Object)null) { if ((Object)(object)_player != (Object)null) { Reset("player unavailable"); } return; } if (!Plugin.Settings.WaitForNearbyContainers && !StorageTrace.Enabled) { if (Current.Reason != "observation disabled") { Current = StorageReadinessSnapshot.Unknown("observation disabled"); } return; } double now = ContainerAccessService.Now; if (!((Object)(object)localPlayer != (Object)(object)_player)) { Vector3 val = ((Component)localPlayer).transform.position - _origin; if (!(((Vector3)(ref val)).sqrMagnitude > 1f)) { goto IL_00d2; } } if (Current.Reason != "position changed") { Current = StorageReadinessSnapshot.Unknown("position changed"); } goto IL_00d2; IL_00d2: if (!force && now < _next) { return; } _next = now + 1.0; _player = localPlayer; _origin = ((Component)localPlayer).transform.position; bool flag = ((Character)localPlayer).IsTeleporting(); bool flag2 = InventoryGui.IsVisible(); if (StorageTrace.Enabled && (flag != _teleporting || flag2 != _visible)) { StorageTrace.Event("player-state", $"teleporting={flag} inventoryVisible={flag2}", 0L); } _teleporting = flag; _visible = flag2; long timestamp = Stopwatch.GetTimestamp(); Current = StorageReadinessProbe.Capture(new GameStorageReadinessSource(_origin, Plugin.Settings, StorageTrace.Enabled), Plugin.Settings.DepositRange); double num = (double)(Stopwatch.GetTimestamp() - timestamp) * 1000.0 / (double)Stopwatch.Frequency; if (!StorageTrace.Enabled) { return; } StorageTrace.RecordProbe(num); string text = $"state={Current.State} known={Current.Known} instantiated={Current.Instantiated} pending={Current.Pending} reason={Current.Reason} guard={Plugin.Settings.WaitForNearbyContainers} preview={Plugin.StorageLoadingPreview?.Value ?? false} teleporting={flag} inventoryVisible={flag2}"; if (force || text != _lastSummary || now >= _heartbeat) { StorageTrace.Event("readiness", FormattableString.Invariant($"{text} x={_origin.x:0.0} y={_origin.y:0.0} z={_origin.z:0.0} range={Plugin.Settings.DepositRange:0.0} probeMs={num:0.00} scanned={Current.Scanned} detailsTruncated={Current.DetailsTruncated} detailsOmitted={Current.DetailsOmitted} pendingDetailsOmitted={Current.PendingDetailsOmitted} dropped={StorageTrace.Dropped} probes={StorageTrace.ProbeCount} avgProbeMs={StorageTrace.AverageProbeMs:0.00} maxProbeMs={StorageTrace.MaxProbeMs:0.00}"), 0L); _lastSummary = text; _heartbeat = now + 10.0; } HashSet<string> hashSet = new HashSet<string>(); foreach (StorageReadinessRecord detail in Current.Details) { hashSet.Add(detail.Id); if (force || !LastDetails.TryGetValue(detail.Id, out string value) || value != detail.Detail) { StorageTrace.Event("nearby-record", "chest=" + detail.Id + " " + detail.Detail, 0L); } LastDetails[detail.Id] = detail.Detail; } List<string> list = new List<string>(); foreach (string key in LastDetails.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (string item in list) { StorageTrace.Event("record-left-detail-window", $"chest={item} queryUnknown={Current.State == StorageReadinessState.Unknown} detailsTruncated={Current.DetailsTruncated}", 0L); LastDetails.Remove(item); } } internal static void Reset(string reason, bool clearPreview = true) { StorageTrace.Event("context-reset", reason, 0L); _next = (_heartbeat = 0.0); _player = null; _lastSummary = ""; LastDetails.Clear(); _teleporting = (_visible = false); Current = StorageReadinessSnapshot.Unknown(reason); if (!clearPreview) { return; } ConfigEntry<bool>? storageLoadingPreview = Plugin.StorageLoadingPreview; if (storageLoadingPreview != null && storageLoadingPreview.Value) { try { Plugin.StorageLoadingPreview.Value = false; } catch (Exception ex) { StorageTrace.Event("preview-reset-failed", ex.GetType().Name, 0L); } } } } internal enum StorageReadinessState { Unknown, NoKnownPending, KnownPending } internal readonly struct StorageReadinessRecord { internal readonly string Id; internal readonly string Detail; internal readonly float DistanceSquared; internal readonly bool Selected; internal readonly bool PlayerBuilt; internal readonly bool Active; internal readonly bool ZoneLoaded; internal readonly bool PrefabAvailable; internal readonly bool Instantiated; internal StorageReadinessRecord(string id, float distanceSquared, bool selected, bool playerBuilt, bool active, bool zoneLoaded, bool prefabAvailable, bool instantiated, string detail) { Id = id; DistanceSquared = distanceSquared; Selected = selected; PlayerBuilt = playerBuilt; Active = active; ZoneLoaded = zoneLoaded; PrefabAvailable = prefabAvailable; Instantiated = instantiated; Detail = detail; } } internal interface IStorageReadinessSource { IEnumerable<StorageReadinessRecord> Read(); } internal sealed class StorageReadinessSnapshot { internal readonly StorageReadinessState State; internal readonly int Known; internal readonly int Instantiated; internal readonly int Pending; internal readonly int Scanned; internal readonly IReadOnlyList<StorageReadinessRecord> Details; internal readonly string Reason; internal bool DetailsTruncated => Known > Details.Count; internal int DetailsOmitted => Math.Max(0, Known - Details.Count); internal int PendingDetailsOmitted { get { int num = 0; foreach (StorageReadinessRecord detail in Details) { if (!detail.Instantiated) { num++; } } return Math.Max(0, Pending - num); } } internal StorageReadinessSnapshot(StorageReadinessState state, int known, int instantiated, int scanned, IReadOnlyList<StorageReadinessRecord> details, string reason = "") { State = state; Known = known; Instantiated = instantiated; Pending = known - instantiated; Scanned = scanned; Details = details; Reason = reason; } internal static StorageReadinessSnapshot Unknown(string reason) { return new StorageReadinessSnapshot(StorageReadinessState.Unknown, 0, 0, 0, Array.Empty<StorageReadinessRecord>(), reason); } } internal static class StorageReadinessProbe { internal const int MaxRecords = 20000; internal const int MaxDetails = 64; internal const string WaitingText = "Nearby containers are loading.\nPlease wait."; internal static bool ShouldBlock(bool enabled, bool guard, bool preview, StorageReadinessSnapshot snapshot) { if (enabled) { if (!preview) { if (guard) { return snapshot.State == StorageReadinessState.KnownPending; } return false; } return true; } return false; } internal static StorageReadinessSnapshot Capture(IStorageReadinessSource source, float radius) { if (float.IsNaN(radius) || float.IsInfinity(radius) || radius <= 0f) { return StorageReadinessSnapshot.Unknown("invalid radius"); } List<StorageReadinessRecord> list = new List<StorageReadinessRecord>(); List<StorageReadinessRecord> list2 = new List<StorageReadinessRecord>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); int num = 0; int num2 = 0; int num3 = 0; try { foreach (StorageReadinessRecord item in source.Read()) { if (++num3 > 20000) { return StorageReadinessSnapshot.Unknown("record limit"); } if (item.Selected && item.PlayerBuilt && item.Active && item.ZoneLoaded && item.PrefabAvailable && !float.IsNaN(item.DistanceSquared) && !(item.DistanceSquared < 0f) && !(item.DistanceSquared > radius * radius) && hashSet.Add(item.Id)) { num++; if (item.Instantiated) { num2++; } List<StorageReadinessRecord> list3 = (item.Instantiated ? list2 : list); if (list3.Count < 64) { list3.Add(item); } } } List<StorageReadinessRecord> list4 = new List<StorageReadinessRecord>(list); foreach (StorageReadinessRecord item2 in list2) { if (list4.Count != 64) { list4.Add(item2); continue; } break; } return new StorageReadinessSnapshot((num <= num2) ? StorageReadinessState.NoKnownPending : StorageReadinessState.KnownPending, num, num2, num3, list4.AsReadOnly()); } catch (Exception ex) { return StorageReadinessSnapshot.Unknown(ex.GetType().Name + ": " + ex.Message); } } } internal static class StorageTrace { private static string _capture = ""; private static double _started; private static StorageTraceBudget _budget = new StorageTraceBudget(); private static StorageTraceBudget _importantBudget = new StorageTraceBudget(); private static double _probeTotalMs; internal static long ProbeCount { get; private set; } internal static double MaxProbeMs { get; private set; } internal static double AverageProbeMs { get { if (ProbeCount != 0L) { return _probeTotalMs / (double)ProbeCount; } return 0.0; } } internal static bool Enabled => Plugin.StorageReadinessDiagnostics?.Value ?? false; internal static long Dropped => _budget.Dropped + _importantBudget.Dropped; internal static void Configure() { try { if (Enabled && _capture.Length == 0) { _capture = Guid.NewGuid().ToString("N"); _started = ContainerAccessService.Now; _budget = new StorageTraceBudget(); _importantBudget = new StorageTraceBudget(); ProbeCount = 0L; _probeTotalMs = (MaxProbeMs = 0.0); Event("capture-start", string.Format("utc={0:O} version={1} build={2} knownRecordsAreNotServerTotals=true", DateTime.UtcNow, "1.3.0", typeof(Plugin).Assembly.ManifestModule.ModuleVersionId), 0L); } else if (!Enabled) { Stop("disabled"); } } catch { } } internal static void RecordProbe(double elapsedMs) { if (Enabled && !double.IsNaN(elapsedMs) && !double.IsInfinity(elapsedMs) && !(elapsedMs < 0.0)) { ProbeCount++; _probeTotalMs += elapsedMs; if (elapsedMs > MaxProbeMs) { MaxProbeMs = elapsedMs; } } } internal static void Stop(string reason) { if (_capture.Length == 0) { return; } string capture = _capture; _capture = ""; try { Plugin.Log.LogInfo((object)FormattableString.Invariant($"[StorageTrace] capture={capture} t={ContainerAccessService.Now - _started:0.000} epoch={ContainerAccessService.SessionEpoch} event=capture-stop reason={StorageTraceBudget.SingleLine(reason)} dropped={Dropped} probes={ProbeCount} avgProbeMs={AverageProbeMs:0.00} maxProbeMs={MaxProbeMs:0.00}")); } catch { } } internal static void Event(string name, string detail, long operation = 0L) { if (!Enabled) { return; } try { if (_capture.Length == 0) { Configure(); } double now = ContainerAccessService.Now; bool flag; switch (name) { case "capture-start": case "context-reset": case "deposit-outcome": case "operation-start": case "send-click": case "send-blocked": case "operation-finish": flag = true; break; default: flag = false; break; } if ((flag ? _importantBudget : _budget).Allow(now)) { if (detail.Length > 2000) { detail = detail.Substring(0, 2000) + " [text truncated]"; } Plugin.Log.LogInfo((object)FormattableString.Invariant($"[StorageTrace] capture={_capture} t={now - _started:0.000} epoch={ContainerAccessService.SessionEpoch} op={operation} event={name} {StorageTraceBudget.SingleLine(detail)}")); } } catch { } } internal static void Chest(string name, Container chest, long operation, string detail) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) if (!Enabled) { return; } try { ZNetView view = ContainerApi.GetView(chest); ZDO val = (((Object)(object)view != (Object)null && view.IsValid()) ? view.GetZDO() : null); Event(name, (val == null) ? ("chest=invalid " + detail) : $"chest={val.m_uid} owner={val.GetOwner()} revision={val.DataRevision} busy={val.GetInt(ZDOVars.s_inUse, 0)} localUse={chest.IsInUse()} {detail}", operation); } catch (Exception ex) { Event("chest-observation-failed", ex.GetType().Name, operation); } } } internal sealed class StorageTraceBudget { internal const int EventsPerSecond = 160; private double _window = double.NegativeInfinity; private int _used; internal long Dropped { get; private set; } internal bool Allow(double now) { if (now < _window || now - _window >= 1.0) { _window = now; _used = 0; } if (_used++ < 160) { return true; } Dropped++; return false; } internal static string SingleLine(string text) { return text.Replace('\r', ' ').Replace('\n', ' '); } } internal sealed class BuildMaterialTotals { private readonly Dictionary<string, int> _items = new Dictionary<string, int>(StringComparer.Ordinal); internal int Count(string name) { if (!_items.TryGetValue(name, out var value)) { return 0; } return value; } internal void Add(string name, int amount) { _items[name] = StorageRules.SaturatingAdd(Count(name), amount); } internal void AddStorage(BuildMaterialTotals chest, Func<string, int> reserveFor) { foreach (KeyValuePair<string, int> item in chest._items) { Add(item.Key, StorageRules.AvailableAfterReserve(item.Value, reserveFor(item.Key))); } } } internal static class BuildMenuIcons { private static readonly Color Unavailable = new Color(1f, 1f, 0f, 0.75f); private static HammerMaterialSnapshot? _lastSnapshot; private static bool _wasEnabled; internal static bool Enabled { get { if (Plugin.Settings.Enabled) { return Plugin.Settings.BuildingFromStorage; } return false; } } internal static void RefreshBuildAvailabilityIconColors(BuildUi ui, bool force = false) { //IL_028f: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Unknown result type (might be due to invalid IL or missing references) if (!force && !((Component)ui).gameObject.activeInHierarchy) { return; } bool enabled = Enabled; if (!force && !enabled && !_wasEnabled) { return; } bool flag = _wasEnabled && !enabled; _wasEnabled = enabled; if (!enabled) { _lastSnapshot = null; } if (!enabled) { if (!flag || !(PatchContracts.BuildPieceButtonsField?.GetValue(ui) is List<BuildUiPieceButton> list)) { return; } object? obj = PatchContracts.BuildSpecialPieceButtonField?.GetValue(ui); BuildUiPieceButton val = (BuildUiPieceButton)((obj is BuildUiPieceButton) ? obj : null); foreach (BuildUiPieceButton item in list) { item.UpdateRequirements(); } if ((Object)(object)val != (Object)null) { val.UpdateRequirements(); } return; } List<BuildUiPieceButton> list2 = null; List<BuildUiPieceButton> list3 = null; BuildUiPieceButton val2 = null; long start = 0L; try { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { _lastSnapshot = null; return; } HammerMaterialSnapshot orRefresh = HammerMaterialReader.GetOrRefresh(localPlayer, Plugin.Settings); if ((!force && orRefresh == _lastSnapshot) || !(PatchContracts.BuildPieceButtonsField?.GetValue(ui) is List<BuildUiPieceButton> list4)) { return; } list3 = list4; object? obj2 = PatchContracts.BuildSpecialPieceButtonField?.GetValue(ui); val2 = (BuildUiPieceButton)((obj2 is BuildUiPieceButton) ? obj2 : null); start = PerformanceDiagnostics.Timestamp(); _lastSnapshot = orRefresh; list2 = new List<BuildUiPieceButton>(list4); if ((Object)(object)val2 != (Object)null) { list2.Add(val2); } ConfigEntry<bool>? debugLogging = Plugin.DebugLogging; int num; long num2; if (debugLogging == null) { num = 0; } else { num = (debugLogging.Value ? 1 : 0); if (num != 0) { num2 = Stopwatch.GetTimestamp(); goto IL_018b; } } num2 = 0L; goto IL_018b; IL_018b: long start2 = num2; List<(Image, Piece, CraftingStation, bool)> list5 = CollectBuildIconEntries(list2, ((Component)localPlayer).transform.position); BuildMaterialTotals carried = ReadInventory(((Humanoid)localPlayer).GetInventory()); Color[] colors = CalculateBuildIconColors(list5, carried, orRefresh); PublishBuildIconColors(list5, colors); if (num != 0) { Plugin.Debug($"Hammer batch: {list5.Count} icons, snapshot available={orRefresh.Available}, {PerformanceDiagnostics.ElapsedMs(start2):0.00} ms."); } } catch (Exception ex) { if (list2 != null || list3 != null) { foreach (BuildUiPieceButton item2 in list2 ?? list3) { object? obj3 = PatchContracts.BuildPieceIconField?.GetValue(item2); Image val3 = (Image)((obj3 is Image) ? obj3 : null); if (val3 != null) { ((Graphic)val3).color = Color.white; } } } if (list2 == null && (Object)(object)val2 != (Object)null) { object? obj4 = PatchContracts.BuildPieceIconField?.GetValue(val2); Image val4 = (Image)((obj4 is Image) ? obj4 : null); if (val4 != null) { ((Graphic)val4).color = Color.white; } } Plugin.Debug("Could not refresh Hammer batch: " + ex.GetType().Name + ": " + ex.Message); } finally { PerformanceDiagnostics.OnBuildUi(rows: false, start); } } private static List<(Image Icon, Piece Piece, CraftingStation? Station, bool HasOrigin)> CollectBuildIconEntries(IReadOnlyList<BuildUiPieceButton> buttons, Vector3 playerPosition) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) Dictionary<string, CraftingStation> dictionary = new Dictionary<string,