Decompiled source of Storehouse v0.8.8

plugins/Storehouse/Storehouse.dll

Decompiled 19 hours ago
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.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Configs;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;
using UnityEngine.Rendering;
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("Storehouse")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("Storehouse")]
[assembly: AssemblyTitle("Storehouse")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Finn.Storehouse
{
	[BepInPlugin("com.joostliebregts.storehouse", "Storehouse", "0.8.8")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInProcess("valheim.exe")]
	[BepInProcess("valheim_server.exe")]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Id = "com.joostliebregts.storehouse";

		public const string Version = "0.8.8";

		internal static ManualLogSource Log;

		internal static string Dir;

		internal static readonly List<string> Failures = new List<string>();

		private const string LiveSlot = "com.joostliebregts.storehouse.live";

		internal static bool FromScripts;

		private bool started;

		private bool failed;

		private bool quitting;

		private float nextWaitLog;

		private Action onQuit;

		private Harmony harmony;

		private EventHandler<SettingChangedEventArgs> onConfig;

		private EventHandler<ConfigurationSynchronizationEventArgs> onSync;

		private float nextModelCheck;

		private float nextConfigCheck;

		private DateTime configStamp;

		private const string UnofficialNote = "Unofficial mod, not affiliated with or endorsed by Iron Gate or Coffee Stain.";

		internal static bool Live { get; private set; }

		internal static bool Headless => (int)SystemInfo.graphicsDeviceType == 4;

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			FromScripts = typeof(Plugin).Assembly.GetName().Name != "Storehouse";
			Dir = (FromScripts ? Path.Combine(Paths.BepInExRootPath, "scripts", "Storehouse") : Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location));
			onQuit = delegate
			{
				quitting = true;
			};
			Application.quitting += onQuit;
			if (FromScripts)
			{
				Log.LogInfo((object)"Storehouse 0.8.8: loaded from BepInEx/scripts (live reload); hooking in once Jotunn is up and an older copy has left.");
			}
			else
			{
				Begin();
			}
		}

		private void Begin()
		{
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Expected O, but got Unknown
			started = true;
			Live = true;
			AppDomain.CurrentDomain.SetData("com.joostliebregts.storehouse.live", this);
			if (FromScripts)
			{
				Guard("version check", ShowToVersionCheck);
			}
			Guard("settings", delegate
			{
				Settings.Bind(((BaseUnityPlugin)this).Config);
			});
			Guard("icon settings", delegate
			{
				Icons.Bind(((BaseUnityPlugin)this).Config);
			});
			if (!Object.op_Implicit((Object)(object)ObjectDB.instance))
			{
				PrefabManager.OnVanillaPrefabsAvailable += AddPieces;
			}
			Guard("words", AddLocalization);
			harmony = new Harmony("com.joostliebregts.storehouse." + Guid.NewGuid().ToString("N"));
			Guard("patches", delegate
			{
				harmony.PatchAll(typeof(Patches));
			});
			Guard("load guard", delegate
			{
				harmony.PatchAll(typeof(LoadGuard));
			});
			Guard("skin patches", delegate
			{
				harmony.PatchAll(typeof(SkinPatches));
			});
			Guard("armory tidy", delegate
			{
				harmony.PatchAll(typeof(Mannequins));
			});
			Guard("typed slots", delegate
			{
				harmony.PatchAll(typeof(TypedSlots));
			});
			Guard("dev fill", DevFill.Register);
			Guard("skins", delegate
			{
				Skins.Load();
				Settings.SkinChests.SettingChanged += delegate
				{
					if (Live)
					{
						Skins.Refresh();
					}
				};
			});
			Guard("perf overlay", delegate
			{
				Settings.PerfOverlay.SettingChanged += delegate
				{
					Overlay();
				};
				Overlay();
			});
			onSync = delegate
			{
				if (Live)
				{
					Settings.ApplyToPieces();
				}
			};
			onConfig = delegate
			{
				if (Live)
				{
					Settings.ApplyToPieces();
				}
			};
			SynchronizationManager.OnConfigurationSynchronized += onSync;
			((BaseUnityPlugin)this).Config.SettingChanged += onConfig;
			if (Object.op_Implicit((Object)(object)ObjectDB.instance))
			{
				AddPieces();
			}
			void Overlay()
			{
				if (Object.op_Implicit((Object)(object)this))
				{
					PerfOverlay component = ((Component)this).GetComponent<PerfOverlay>();
					if (Settings.PerfOverlay.Value && !Object.op_Implicit((Object)(object)component))
					{
						((Component)this).gameObject.AddComponent<PerfOverlay>();
					}
					else if (!Settings.PerfOverlay.Value && Object.op_Implicit((Object)(object)component))
					{
						Object.Destroy((Object)(object)component);
					}
				}
			}
		}

		private void ShowToVersionCheck()
		{
			PluginInfo info = ((BaseUnityPlugin)this).Info;
			if ((Object)(object)info.Instance != (Object)(object)this)
			{
				AccessTools.Property(typeof(PluginInfo), "Instance").SetValue(info, this);
			}
			Chainloader.PluginInfos["com.joostliebregts.storehouse"] = info;
			FieldInfo fieldInfo = AccessTools.Field(typeof(BepInExUtils), "Plugins");
			if (fieldInfo.GetValue(null) is BaseUnityPlugin[] source)
			{
				fieldInfo.SetValue(null, source.Where(delegate(BaseUnityPlugin p)
				{
					if (Object.op_Implicit((Object)(object)p))
					{
						PluginInfo info2 = p.Info;
						object obj;
						if (info2 == null)
						{
							obj = null;
						}
						else
						{
							BepInPlugin metadata = info2.Metadata;
							obj = ((metadata != null) ? metadata.GUID : null);
						}
						return (string?)obj != "com.joostliebregts.storehouse";
					}
					return false;
				}).Append((BaseUnityPlugin)(object)this).ToArray());
			}
			Log.LogInfo((object)$"Version check: this live copy counts as Storehouse {info.Metadata.Version}; a server with any 0.8 lets it in.");
		}

		private void TryBegin()
		{
			object? data = AppDomain.CurrentDomain.GetData("com.joostliebregts.storehouse.live");
			Object val = (Object)((data is Object) ? data : null);
			PluginInfo value;
			bool flag = Chainloader.PluginInfos.TryGetValue("com.jotunn.jotunn", out value) && Object.op_Implicit((Object)(object)value.Instance);
			if ((Object.op_Implicit(val) && val != (Object)(object)this) || !flag)
			{
				if (Time.realtimeSinceStartup >= nextWaitLog)
				{
					nextWaitLog = Time.realtimeSinceStartup + 5f;
					Log.LogInfo((object)((!flag) ? "Waiting for Jotunn." : "Waiting for the previous Storehouse copy to leave."));
				}
				return;
			}
			try
			{
				Begin();
			}
			catch (Exception arg)
			{
				failed = true;
				Live = false;
				Log.LogError((object)$"Live reload: could not hook in; this copy stays idle until the next build ({arg}).");
			}
		}

		private void OnDestroy()
		{
			Application.quitting -= onQuit;
			ModelWatch.Stop();
			LoadWarning component = ((Component)this).GetComponent<LoadWarning>();
			if (component != null)
			{
				Object.Destroy((Object)(object)component);
			}
			if (!started)
			{
				return;
			}
			Live = false;
			try
			{
				Patches.RestoreGrid();
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Storehouse: container grid not restored (" + ex.Message + ")."));
			}
			if (AppDomain.CurrentDomain.GetData("com.joostliebregts.storehouse.live") == this)
			{
				AppDomain.CurrentDomain.SetData("com.joostliebregts.storehouse.live", null);
			}
			PrefabManager.OnVanillaPrefabsAvailable -= AddPieces;
			PieceManager.OnPiecesRegistered -= HammerOrder;
			if (onSync != null)
			{
				SynchronizationManager.OnConfigurationSynchronized -= onSync;
			}
			if (onConfig != null)
			{
				((BaseUnityPlugin)this).Config.SettingChanged -= onConfig;
			}
			Harmony obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			if (!quitting)
			{
				try
				{
					Reload.Teardown();
				}
				catch (Exception arg)
				{
					Log.LogError((object)$"Live reload: cleanup failed ({arg}).");
				}
				Log.LogInfo((object)"Storehouse copy unloaded (live reload).");
			}
		}

		private static void Guard(string step, Action run)
		{
			try
			{
				run();
			}
			catch (Exception arg)
			{
				Failures.Add(step);
				Log.LogError((object)string.Format("Storehouse {0}: start-up step '{1}' failed; the pieces stay registered, this part is off until the next start ({2}).", "0.8.8", step, arg));
			}
		}

		private void Update()
		{
			if (!started)
			{
				TryBegin();
			}
			else if (!failed)
			{
				long timestamp = Stopwatch.GetTimestamp();
				Perf.Frame();
				try
				{
					Tick();
				}
				finally
				{
					Perf.Add(timestamp);
				}
			}
		}

		private void LateUpdate()
		{
			if (!started || failed)
			{
				return;
			}
			long timestamp = Stopwatch.GetTimestamp();
			try
			{
				StoreContents.DrawAll();
			}
			finally
			{
				Perf.Add(timestamp);
				Perf.EndFrame();
			}
		}

		private void Tick()
		{
			if (Time.unscaledTime >= nextConfigCheck)
			{
				nextConfigCheck = Time.unscaledTime + 1f;
				DateTime dateTime = (File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) ? File.GetLastWriteTimeUtc(((BaseUnityPlugin)this).Config.ConfigFilePath) : default(DateTime));
				if (configStamp == default(DateTime))
				{
					configStamp = dateTime;
				}
				else if (dateTime != configStamp)
				{
					configStamp = dateTime;
					((BaseUnityPlugin)this).Config.Reload();
				}
			}
			if (!(Time.time < nextModelCheck))
			{
				nextModelCheck = Time.time + 1f;
				if (Settings.LiveModels.Value && ModelWatch.Changed())
				{
					long timestamp = Stopwatch.GetTimestamp();
					CribModel.CheckForChanges();
					StoreModel.CheckAllForChanges();
					Perf.LiveCheck(timestamp);
				}
				Icons.Tick();
			}
		}

		private static void Unofficial(Dictionary<string, string> words)
		{
			string key = Kinds.All.First((Kind k) => k == Kinds.Crib || Kinds.InRelease(k)).Token + "_desc";
			if (words.TryGetValue(key, out var value) && !value.EndsWith("Unofficial mod, not affiliated with or endorsed by Iron Gate or Coffee Stain.", StringComparison.Ordinal))
			{
				words[key] = value + "\n\nUnofficial mod, not affiliated with or endorsed by Iron Gate or Coffee Stain.";
			}
		}

		private static void AddLocalization()
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>
			{
				["$item_storehouse_board"] = "Board",
				["$item_storehouse_board_desc"] = "A plank split and planed from a log. What furniture and storage are built from.",
				[Kinds.Strongbox.Token] = "Hauler's Strongbox",
				[Kinds.Strongbox.Token + "_desc"] = "A black metal chest built for a cart, not a doorway. Holds twice what a black metal chest does. Stands on level ground or a floor.",
				[Kinds.Crib.Token] = "Timber Crib",
				[Kinds.Crib.Token + "_desc"] = "An open crib of logs and iron-banded beams. Holds wood only: wood, fine wood, core wood, ancient bark, yggdrasil wood, ashwood, boards. Stands on level ground or a floor.",
				[Kinds.Bin.Token] = "Ore Bin",
				[Kinds.Bin.Token + "_desc"] = "A stone-lined bin with an iron rim. Holds ore, scrap, stone, coal and metal bars only. Stands on level ground or a floor."
			};
			Unofficial(dictionary);
			CustomLocalization localization = LocalizationManager.Instance.GetLocalization();
			string key3 = "English";
			localization.AddTranslation(ref key3, dictionary);
			Dictionary<string, string> dictionary2 = new Dictionary<string, string>();
			Kind[] all = Kinds.All;
			foreach (Kind kind in all)
			{
				if (kind.Desc != null)
				{
					dictionary2[kind.Token] = kind.Shown ?? kind.Title;
					dictionary2[kind.Token + "_desc"] = kind.Desc;
				}
			}
			Unofficial(dictionary2);
			CustomLocalization localization2 = LocalizationManager.Instance.GetLocalization();
			key3 = "English";
			localization2.AddTranslation(ref key3, dictionary2);
			object? obj = AccessTools.Field(typeof(Localization), "m_instance")?.GetValue(null);
			Localization val = (Localization)((obj is Localization) ? obj : null);
			if (val == null)
			{
				return;
			}
			MethodInfo methodInfo = AccessTools.Method(typeof(Localization), "AddWord", new Type[2]
			{
				typeof(string),
				typeof(string)
			}, (Type[])null);
			if ((object)methodInfo == null)
			{
				return;
			}
			foreach (KeyValuePair<string, string> item in dictionary.Concat(dictionary2))
			{
				methodInfo.Invoke(val, new object[2]
				{
					item.Key.TrimStart('$'),
					item.Value
				});
			}
		}

		private void AddPieces()
		{
			PrefabManager.OnVanillaPrefabsAvailable -= AddPieces;
			Guard("board", Board.Add);
			if (Settings.DumpPrefabs.Value && !Object.op_Implicit((Object)(object)PrefabManager.Instance.GetPrefab(Kinds.Crib.Prefab)))
			{
				Guard("prefab dump", delegate
				{
					Inspect.Dump(PrefabManager.Instance.GetPrefab(Kinds.Crib.Base), "vanilla base of the Timber Crib");
				});
			}
			int num = 0;
			Kind[] all = Kinds.All;
			foreach (Kind kind in all)
			{
				try
				{
					AddPiece(kind);
				}
				catch (Exception arg)
				{
					Log.LogError((object)$"{kind.Title}: not added ({arg})");
				}
				if (Object.op_Implicit((Object)(object)PrefabManager.Instance.GetPrefab(kind.Prefab)))
				{
					num++;
				}
				else
				{
					Failures.Add("piece " + kind.Title);
				}
			}
			PieceManager.OnPiecesRegistered += HammerOrder;
			Guard("hammer order", HammerOrder);
			Guard("armory sort", delegate
			{
				Mannequins.HookSort(harmony);
			});
			try
			{
				Reload.AfterPieces();
			}
			catch (Exception arg2)
			{
				Log.LogError((object)$"Live reload: the world's pieces were not all redressed ({arg2}).");
			}
			string text = "";
			if (FromScripts)
			{
				text = " (ScriptEngine copy " + typeof(Plugin).Assembly.GetName().Name + ")";
			}
			Log.LogInfo((object)(string.Format("Storehouse {0}: live, sha256 {1}, {2}/{3} pieces, {4} start-up failure(s)", "0.8.8", DllSha(), num, Kinds.All.Length, Failures.Count) + ((Failures.Count > 0) ? (": " + string.Join("; ", Failures)) : "") + text + "."));
			if (!Headless && !Object.op_Implicit((Object)(object)((Component)this).GetComponent<LoadWarning>()))
			{
				((Component)this).gameObject.AddComponent<LoadWarning>().Set(num, Kinds.All.Length);
			}
		}

		internal static void HammerOrder()
		{
			PieceTable pieceTable = PieceManager.Instance.GetPieceTable(PieceTables.Hammer);
			if (Object.op_Implicit((Object)(object)pieceTable))
			{
				HashSet<string> ours = new HashSet<string>(Kinds.All.Select((Kind k) => k.Prefab), StringComparer.Ordinal);
				List<GameObject> pieces = pieceTable.m_pieces;
				int num = pieces.FindIndex((GameObject p) => Object.op_Implicit((Object)(object)p) && ours.Contains(((Object)p).name));
				if (num < 0)
				{
					num = pieces.Count;
				}
				pieces.RemoveAll((GameObject p) => Object.op_Implicit((Object)(object)p) && ours.Contains(((Object)p).name));
				Kind[] all = Kinds.All;
				pieces.InsertRange(Math.Min(num, pieces.Count), from k in all
					select PrefabManager.Instance.GetPrefab(k.Prefab) into p
					where Object.op_Implicit((Object)(object)p)
					select p);
				if (Object.op_Implicit((Object)(object)Player.m_localPlayer))
				{
					AccessTools.Method(typeof(Player), "UpdateAvailablePiecesList", (Type[])null, (Type[])null)?.Invoke(Player.m_localPlayer, null);
				}
			}
		}

		private string DllSha()
		{
			try
			{
				string path = (FromScripts ? Path.Combine(Paths.BepInExRootPath, "scripts", "Storehouse.dll") : ((BaseUnityPlugin)this).Info.Location);
				using SHA256 sHA = SHA256.Create();
				using FileStream inputStream = File.OpenRead(path);
				return BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "").ToLowerInvariant()
					.Substring(0, 12);
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Storehouse: could not hash its own DLL (" + ex.Message + ")."));
				return "unknown";
			}
		}

		private void AddPiece(Kind kind)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: Expected O, but got Unknown
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Expected O, but got Unknown
			//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Expected O, but got Unknown
			GameObject prefab = PrefabManager.Instance.GetPrefab(kind.Prefab);
			if (prefab != null)
			{
				Reload.Rebuild(prefab, kind);
				return;
			}
			PieceConfig val = new PieceConfig
			{
				Name = kind.Token,
				Description = kind.Token + "_desc",
				PieceTable = PieceTables.Hammer,
				Category = "Storage",
				CraftingStation = (kind.Station?.Value ?? kind.DefaultStation)
			};
			foreach (var (text, num) in Settings.ParseRecipe(kind.Recipe?.Value ?? kind.DefaultRecipe))
			{
				val.AddRequirement(new RequirementConfig(text, num, 0, true));
			}
			CustomPiece val2 = new CustomPiece(kind.Prefab, kind.Base, val);
			if ((Object)(object)val2.PiecePrefab == (Object)null)
			{
				Log.LogError((object)("Could not clone " + kind.Base + "; the " + kind.Title + " is not added."));
				return;
			}
			GameObject piecePrefab = val2.PiecePrefab;
			Piece piece = val2.Piece;
			Container val3 = Configure(piecePrefab, piece, kind);
			if (!Object.op_Implicit((Object)(object)val3))
			{
				Log.LogError((object)(kind.Base + " has no Container; the " + kind.Title + " is not added."));
				return;
			}
			kind.Build(piecePrefab, val3);
			piece.m_icon = Icons.Render(piecePrefab, kind) ?? piece.m_icon;
			PieceManager.Instance.AddPiece(val2);
			Reload.RegisterLate(piecePrefab, kind);
			if (Settings.DumpPrefabs.Value)
			{
				Inspect.Dump(piecePrefab, kind.Title + " (built)");
			}
			Log.LogInfo((object)$"{kind.Title}: piece added ({kind.Width}x{kind.Height}).");
		}

		internal static Container Configure(GameObject go, Piece p, Kind kind)
		{
			p.m_comfort = 0;
			p.m_notOnFloor = false;
			switch (kind.Placement)
			{
			case Placement.Ground:
				p.m_groundOnly = false;
				p.m_notOnTiltingSurface = true;
				break;
			case Placement.Wall:
				p.m_groundOnly = false;
				p.m_notOnFloor = true;
				p.m_notOnTiltingSurface = false;
				break;
			default:
				p.m_groundOnly = false;
				break;
			}
			Container componentInChildren = go.GetComponentInChildren<Container>(true);
			if (!Object.op_Implicit((Object)(object)componentInChildren))
			{
				return null;
			}
			componentInChildren.m_name = kind.Token;
			componentInChildren.m_width = kind.Width;
			componentInChildren.m_height = kind.Height;
			return componentInChildren;
		}
	}
	internal enum Placement
	{
		Ground,
		Normal,
		Wall
	}
	internal sealed class Kind
	{
		public string Title;

		public string Prefab;

		public string Token;

		public string Base;

		public string Section;

		public string DefaultRecipe;

		public string DefaultStation;

		public string DefaultAccepts;

		public string FilterLabel;

		public string Shown;

		public HashSet<string> Never;

		public string NeverWhy;

		public string Desc;

		public string Slug;

		public string Piece;

		public string[] Rounds;

		public bool Display;

		public bool Mirror;

		public bool Heap;

		public bool Homes;

		public Action<GameObject> Dress;

		public int Planks;

		public int DefaultRows;

		public Placement Placement;

		public ConfigEntry<int> Rows;

		public ConfigEntry<float> ItemSize;

		public ConfigEntry<Color> GlassColour;

		public ConfigEntry<string> Model;

		public ConfigEntry<string> Recipe;

		public ConfigEntry<string> Station;

		public ConfigEntry<string> Accepts;

		public Action<GameObject, Container> Build;

		public Func<ItemData, bool> Rule;

		public Filter Filter;

		public int Columns;

		public string Spoken
		{
			get
			{
				string text = Shown ?? Title;
				int num = text.IndexOf(", ", StringComparison.Ordinal);
				if (num <= 0)
				{
					return text;
				}
				return text.Substring(num + 2) + " " + text.Substring(0, num);
			}
		}

		public int Width
		{
			get
			{
				if (Planks <= 0)
				{
					if (Columns <= 0)
					{
						return 8;
					}
					return Columns;
				}
				return Settings.SlotsPerPlank.Value;
			}
		}

		public int Height
		{
			get
			{
				if (Planks <= 0)
				{
					return Rows?.Value ?? TypedSlots.Rows(this, Width) ?? DefaultRows;
				}
				return Planks;
			}
		}
	}
	internal static class Kinds
	{
		public const int Columns = 8;

		public const int MinRows = 1;

		public static readonly Kind Strongbox = new Kind
		{
			Title = "Hauler's Strongbox",
			Prefab = "piece_storehouse_strongbox",
			Token = "$piece_storehouse_strongbox",
			Base = "piece_chest_blackmetal",
			Section = "1 - Haulers Strongbox",
			DefaultRows = 8,
			DefaultRecipe = "BlackMetal:12,Iron:8,FineWood:20",
			DefaultStation = "forge",
			Build = StrongboxModel.Build
		};

		public static readonly Kind Crib = new Kind
		{
			Title = "Timber Crib",
			Prefab = "piece_storehouse_timbercrib",
			Token = "$piece_storehouse_timbercrib",
			Base = "piece_chest_wood",
			Section = "2 - Timber Crib",
			DefaultRows = 8,
			DefaultRecipe = "Wood:20,StorehouseBoard:10,BronzeNails:10",
			DefaultStation = "piece_workbench",
			DefaultAccepts = "Wood,FineWood,RoundLog,ElderBark,YggdrasilWood,Blackwood,Frostwood,Root,WrithanRoots,BarkaBranch,StorehouseBoard",
			FilterLabel = "Wood only",
			Build = CribModel.Build,
			Dress = delegate(GameObject go)
			{
				CribModel.Dress(go, quiet: true);
			}
		};

		public static readonly Kind Bin = new Kind
		{
			Title = "Ore Bin",
			Prefab = "piece_storehouse_orebin",
			Token = "$piece_storehouse_orebin",
			Base = "piece_chest",
			Section = "3 - Ore Bin",
			DefaultRows = 6,
			DefaultRecipe = "Stone:30,Iron:6,Wood:10",
			DefaultStation = "piece_stonecutter",
			DefaultAccepts = "CopperOre,TinOre,IronScrap,IronOre,SilverOre,BlackMetalScrap,FlametalOre,FlametalOreNew,GoldOre,CopperScrap,BronzeScrap,Stone,Flint,Coal,Obsidian,Crystal,Grausten,BlackMarble,SulfurStone,Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew,Gold",
			FilterLabel = "Ore, stone and metal only",
			Build = Kitbash.OreBin
		};

		private static readonly char[] notInSection = new char[8] { '=', '\n', '\t', '\\', '"', '\'', '[', ']' };

		public static readonly Dictionary<string, string[]> ModelAlternates = new Dictionary<string, string[]>(StringComparer.Ordinal)
		{
			["bombcrate"] = new string[1] { "_r2" },
			["fletcher"] = new string[4] { "_r3ds", "_r3cs", "_r3s", "_r2" },
			["ossuary"] = new string[3] { "_r5", "_r3", "_r4" },
			["weaponrack"] = new string[2] { "_r3", "_r1" },
			["weaponrackb"] = new string[2] { "_r3", "_r1" },
			["arsenal"] = new string[1] { "_r1" },
			["arsenalb"] = new string[3] { "_r4", "_r3", "_r1" },
			["utilitywall"] = new string[1] { "_r1" },
			["utilityisland"] = new string[1] { "_r1" },
			["armoryrail"] = new string[1] { "_r1" },
			["armoryrail3"] = new string[1] { "_r1" },
			["armoryisland"] = new string[1] { "_r2" },
			["armorcoffer"] = new string[2] { "_r2", "_r1" },
			["armorcabinet"] = new string[2] { "_r2", "_r1" },
			["armory"] = new string[1] { "_r1" },
			["armoryb"] = new string[2] { "_r2b", "_r1" },
			["arsenalkit_shield"] = new string[2] { "_r3", "_r4" },
			["arsenalkit_spine"] = new string[1] { "_r4" },
			["arsenalkit_long"] = new string[2] { "_r5b", "_r4" },
			["arrowtubes"] = new string[1] { "_r3" },
			["coopersrow"] = new string[2] { "_r10", "_r9" },
			["coopersrowstack"] = new string[2] { "_r3", "_r2" }
		};

		public static readonly Kind[] Stores = new Kind[53]
		{
			Shown("Brewer's Cabinet", Store("meadrack", "Mead Rack", "A rack of shelves for bottles. Holds meads and potions only.", "Meads and potions only", ItemRules.Mead, "BarleyWine,BarleyWineBase,MeadBase*", 64, Placement.Normal, "StorehouseBoard:16,BronzeNails:10", "piece_workbench", null, null, display: false, 0, new string[2] { "_r3", "_r2" }, mirror: true)),
			Store("larder", "Larder Hutch", "A kitchen hutch for cooked food. Holds meals only.", "Meals only", ItemRules.Meal, "", 64, Placement.Normal, "StorehouseBoard:18,BronzeNails:12", "piece_workbench", null, null, display: true, 0, new string[5] { "_r6", "_r5", "_r4", "_r3", "_r2" }, mirror: true),
			Store("meatrack", "Meat Rack", "Hooks and bins for the day's catch. Holds meat and fish, raw or cooked, and the animal parts for food and meads.", "Meat and fish only", ItemRules.RawMeat, "RawMeat,DeerMeat,NeckTail,WolfMeat,LoxMeat,SerpentMeat,ChickenMeat,HareMeat,BugMeat,AsksvinMeat,VoltureMeat,BoneMawSerpentMeat,FishRaw,Bloodclot,Entrails", 64, Placement.Normal, "Wood:12,StorehouseBoard:10,BronzeNails:10", "piece_workbench"),
			Store("rootbin", "Produce Crates", "Crates for what the farm and the forest give. Holds vegetables, mushrooms, berries, herbs, seeds and eggs only.", "Produce only", ItemRules.Plant, "Turnip,Onion,Carrot,Barley,Flax,Thistle,Dandelion,Fiddleheadfern,*Seeds,Mushroom*,*berries,Raspberry,Cloudberry,Acorn,AncientSeed,FirCone,FirConeFrost,PineCone,FreshSeaweed", 72, Placement.Normal, "StorehouseBoard:14,BronzeNails:8", "piece_workbench", null, null, display: false, 0, new string[7] { "_r8", "_r7", "_r6", "_r5", "_r4", "_r3", "_r2" }),
			Store("orebin", "Ore Bin", "A timber bin for the mine's haul. Holds ore and scrap only. Stands on level ground or a floor.", "Ore and scrap only", ItemRules.Ore, "CopperOre,TinOre,IronScrap,IronOre,SilverOre,BlackMetalScrap,FlametalOre,FlametalOreNew,CopperScrap,BronzeScrap", 80, Placement.Ground, "Wood:12,StorehouseBoard:10,Stone:10,IronNails:8,Iron:4", "piece_workbench", null, "piece_storehouse_orebin"),
			Store("bunker", "Coal & Stone Bunker", "Three bays for loose heaps. Holds coal and stone only. Stands on level ground or a floor.", "Coal and stone only", ItemRules.StoneCoal, "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,SulfurStone,StoneRock,PungentPebbles,BFP_Clay,Ice,Crystal", 90, Placement.Ground, "Wood:16,StorehouseBoard:12,IronNails:8,Iron:4", "piece_workbench"),
			Store("bunkeropen", "Open Coal & Stone Bunker", "The bunker without a load-bearing top: three open bays under a leaning back wall. Holds coal and stone only; nothing stacks on it.", "Coal and stone only", ItemRules.StoneCoal, "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,SulfurStone,StoneRock,PungentPebbles,BFP_Clay,Ice,Crystal", 90, Placement.Ground, "Wood:14,StorehouseBoard:10,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[1] { "_r1" }),
			Store("metalrack", "Metal Rack (tall)", "A tall timber rack for bars, iron-strapped. Holds metal bars only. Stacks.", "Metal bars only", ItemRules.Metal, "Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew", 64, Placement.Ground, "Wood:12,StorehouseBoard:12,IronNails:8,Iron:4", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }, mirror: true),
			Store("metalrackhalf", "Metal Rack (tall, half)", "A tall timber rack for bars, half as wide. Holds metal bars only. Stacks.", "Metal bars only", ItemRules.Metal, "Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew", 32, Placement.Ground, "Wood:8,StorehouseBoard:8,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: true, 4),
			Store("metalracklow", "Metal Rack (low)", "A low timber rack for bars. Holds metal bars only. Stacks.", "Metal bars only", ItemRules.Metal, "Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew", 32, Placement.Ground, "Wood:8,StorehouseBoard:8,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: true),
			Store("metalracklowhalf", "Metal Rack (low, half)", "A low timber rack for bars, half as wide. Holds metal bars only. Stacks.", "Metal bars only", ItemRules.Metal, "Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew", 16, Placement.Ground, "Wood:4,StorehouseBoard:4,IronNails:4,Iron:1", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: true, 4),
			Store("metalpallet", "Metal Pallet", "A low pallet for stacked bars. Holds metal bars only. Stacks.", "Metal bars only", ItemRules.Metal, "Copper,Tin,Bronze,Iron,Silver,BlackMetal,Flametal,FlametalNew", 64, Placement.Ground, "Wood:16,StorehouseBoard:10,IronNails:8,Iron:4", "piece_workbench", null, null, display: false, 0, new string[4] { "_r4", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: true),
			Store("coopersrow", "Cooper's Row", "Barrels, pots and tubs for sticky goods: tar, resin, sap, guck, ooze and powder, one kind per vessel.", "Tar, resin, sap and goo only", null, "Tar,Resin,Sap,Guck,Ooze,RoyalJelly,Honey,CharcoalResin,OozeMork,HatefulBlood,Voidplasm,BlobVial,Ectoplasm,ProustitePowder", 64, Placement.Ground, "Wood:10,StorehouseBoard:12,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[9] { "_r11", "_r10", "_r9", "_r6", "_r5", "_r4", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("coopersrowstack", "Stacking Cooper's Row", "Cooper's Row in a six-post frame under a plank deck: the same vessels, room above to pour, and another one stacks on top.", "Tar, resin, sap and goo only", null, "Tar,Resin,Sap,Guck,Ooze,RoyalJelly,Honey,CharcoalResin,OozeMork,HatefulBlood,Voidplasm,BlobVial,Ectoplasm,ProustitePowder", 64, Placement.Ground, "Wood:16,StorehouseBoard:18,IronNails:10,Iron:4", "piece_workbench", null, null, display: false, 0, new string[4] { "_r4", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("fletcher", "Fletcher's Barrels", "Barrels for arrows, tubs for bolts and a straw crate for bombs, one kind per holder.", "Arrows, bolts and bombs only", ItemRules.ArrowOrBolt, "Arrow*,Bolt*,Bomb*,VA*Arrow*,VA*arrow*,VA*Bolt*,VA*bolt*,TurretBolt*,Catapult_*,FireworksRocket_*,Sparkler", 64, Placement.Ground, "StorehouseBoard:14,BronzeNails:10,LeatherScraps:4", "piece_workbench", null, null, display: false, 0, new string[7] { "_r3es", "_r3ds", "_r3cs", "_r3s", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("treasure", "Treasure Hoard", "An iron-bound coffer heaped with coins and gold. Gems and jewellery go in the Jeweller's Casket. Open the inventory for any stored kinds without a display place.", "Coins and gold only", ItemRules.CoinOrGold, "Coins,AncientCoin,Gold", 96, Placement.Ground, "StorehouseBoard:12,IronNails:8,Iron:4,DeerHide:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("apothecary", "Apothecary Rack", "Jars, crocks, bags and baskets on a standing rack for the small things creatures leave behind, one kind per vessel.", "Creature parts only", null, "Feathers,Needle,GreydwarfEye,GiantBloodSack,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,Mandible,SerpentScale,BonemawSerpentScale,FreezeGland,MoleClaws", 64, Placement.Normal, "StorehouseBoard:12,BronzeNails:8,LeatherScraps:2", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }),
			Store("hiderack", "Hide Rack", "Hinged frames on an iron-banded post, one hide stretched in each, the folded hides piled in the bay below.", "Hides and leather only", null, "TrollHide,WolfPelt,LoxPelt,DeerHide,LeatherScraps,AskHide,ScaleHide,BjornHide,MooseHide,SealHide,Leatherstraps", 64, Placement.Ground, "Wood:10,StorehouseBoard:12,BronzeNails:8,LeatherScraps:4", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("clothrack", "Cloth Rack", "A narrow rack for cloth and thread: linen, jute and Norn thread, one kind per bay.", "Cloth and thread only", null, "LinenThread,JuteRed,JuteBlue,NornThread,CandleWick", 32, Placement.Normal, "StorehouseBoard:10,BronzeNails:6,LinenThread:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 4),
			Store("ossuary", "Ossuary Bin", "A stone-footed bin for bones: fragments, charred and withered bones, skulls and antlers, one kind per bay.", "Bones only", null, "BoneFragments,CharredBone,WitheredBone,Charredskull,HardAntler,AsksvinCarrionSkull,BonemawSerpentTooth,AsksvinCarrionNeck,AsksvinCarrionPelvic,AsksvinCarrionRibcage,UndeadBjornRibcage,LargeBone,GhostSkull", 128, Placement.Ground, "Wood:12,StorehouseBoard:10,Stone:16,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[5] { "_r5b", "_r5", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("arrowtubes", "Arrow Tubes", "A stepped stand of tubes, one per kind of arrow, each with its painted band; bundles packed tight.", "Arrows only", ItemRules.Arrow, "Arrow*,VA*Arrow*,VA*arrow*", 80, Placement.Ground, "Wood:12,StorehouseBoard:14,BronzeNails:10,LeatherScraps:4", "piece_workbench", null, null, display: false, 0, new string[4] { "_r4", "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("bolttubs", "Bolt Crate", "A stepped crate of square cells for handheld crossbow bolts, one kind per cell. Siege ammunition needs its own fitted storage.", "Crossbow bolts only", ItemRules.Bolt, "Bolt*,VA*Bolt*,VA*bolt*", 64, Placement.Ground, "Wood:10,StorehouseBoard:12,IronNails:8", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("bombcrate", "Bomb Crate", "A compact stepped crate of straw-bedded trays, one kind of bomb per tray, a reserve tier at the back and a bucket of sand at hand.", "Bombs only", null, "Bomb*", 64, Placement.Ground, "StorehouseBoard:14,IronNails:8,Iron:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r3", "_r2" }, mirror: false, 0, heap: false, homes: true),
			Shown("Tall Apothecary Rack", Store("apothecarytall", "Tall Apothecary Rack", "The Apothecary Rack two metres wide and as tall as a metal rack: forty places, each vessel following what it holds.", "Creature parts only", null, "Feathers,Needle,GreydwarfEye,GiantBloodSack,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,Mandible,SerpentScale,BonemawSerpentScale,FreezeGland,MoleClaws", 64, Placement.Normal, "StorehouseBoard:16,BronzeNails:10,LeatherScraps:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" })),
			Store("pantry", "Pantry Dresser", "A cook's dry reserve for flour, spices, herbs and processed ingredients. Genuine uncooked dishes are optional stock; prepared meals belong in the Larder.", "Dry cooking ingredients and uncooked food only", null, "SpiceForests,SpicePlains,SpiceMountains,SpiceOceans,SpiceMistlands,SpiceDeepNorth,SpiceAshlands,BarleyFlour,OatFlour,BreadDough,FragrantBundle,FeastAshlands_Material,FeastBlackforest_Material,FeastDeepNorth_Material,FeastMeadows_Material,FeastMistlands_Material,FeastMountains_Material,FeastOceans_Material,FeastPlains_Material,FeastSwamps_Material,BakedPoteitrUncooked,FishAndBreadUncooked,HoneyGlazedChickenUncooked,KaleChipsUncooked,LoxPieUncooked,MagicallyStuffedShroomUncooked,MeatPlatterUncooked,MisthareSupremeUncooked,OvenPancakeUncooked,PiquantPieUncooked,RoastedCrustPieUncooked,VikingCupcakeUncooked", 80, Placement.Normal, "StorehouseBoard:16,BronzeNails:10", "piece_workbench", null, null, display: false, 0, new string[3] { "_r4", "_r2", "_r1" }),
			Store("smithdrawers", "Smith's Drawers", "A chest of timber drawers pulled half open: nails, chain, rings, springs, axe heads and the smith's odds and ends.", "Smithing parts only", null, "BronzeNails,IronNails,Chain,BarrelRings,MechanicalSpring,AxeHead1,AxeHead2,ScytheHandle,CeramicPlate,CharredCogwheel,SharpeningStone,BellFragment", 64, Placement.Normal, "StorehouseBoard:12,Iron:4,IronNails:10", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("jeweller", "Jeweller's Casket", "A casket of lined trays for gems and jewellery, one kind per tray. Coins and gold go in the Treasure Hoard; open the inventory for stored kinds without a place.", "Gems and jewellery only", ItemRules.Jewel, "Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,SilverNecklace,CrownJewel,Pot_Shard*", 64, Placement.Normal, "StorehouseBoard:8,IronNails:6,LinenThread:4", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Store("embercabinet", "Ember Cabinet", "An iron lantern cabinet with a niche for each glowing thing: wisps, cores, embers, orbs and glow worms.", "Glowing things only", ItemRules.ArcaneCore, "Wisp,GlowWorm,FaderEmber,MemorialCoal,MoltenCore,BlackCore,FrostCore,ShieldCore,OrbFrostFire,OrbThunderBlood,SurtlingCore,FrozenFuel,Thunderstone,Eitr,DragonTear", 64, Placement.Normal, "StorehouseBoard:8,Iron:6,IronNails:6", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 0, heap: false, homes: true),
			Shown("Trophy Hall (80, first)", Store("trophyhall", "Trophy Hall", "A free-standing wall of boards, one trophy mounted on each.", "Trophies only", (ItemData item) => (int)item.m_shared.m_itemType == 13, "Trophy*", 80, Placement.Normal, "Wood:20,StorehouseBoard:24,IronNails:12", "piece_workbench", null, null, display: true, 0, new string[3] { "_r3", "_r2", "_r1" }, mirror: true, 10)),
			Shown("Trophy Hall", Store("trophyhall13x5", "Trophy Hall 13x5", "A long wall of boards, one trophy mounted on each, the bosses' too.", "Trophies only", (ItemData item) => (int)item.m_shared.m_itemType == 13, "Trophy*", 65, Placement.Normal, "Wood:18,StorehouseBoard:22,IronNails:11", "piece_workbench", null, null, display: true, 0, new string[1] { "_r1" }, mirror: true, 13)),
			Store("trophyhall10x7", "Tall Trophy Hall", "A tall wall of boards, one trophy mounted on each, the bosses' too.", "Trophies only", (ItemData item) => (int)item.m_shared.m_itemType == 13, "Trophy*", 70, Placement.Normal, "Wood:18,StorehouseBoard:24,IronNails:12", "piece_workbench", null, null, display: true, 0, new string[1] { "_r1" }, mirror: true, 10),
			Store("armorcoffer", "Armor Coffer", "An open reserve coffer for armor: helmets in their trays, chest, legs and capes folded in piles. Shows every piece it holds.", "Armor and trinkets only", EquipRules.Armor, "", 64, Placement.Normal, "StorehouseBoard:16,IronNails:10,Iron:2", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }),
			Store("armorcabinet", "Armor Cabinet", "A tall coffer cabinet for armor: helmets in cubbies, the rest folded in piles. Shows every piece it holds.", "Armor and trinkets only", EquipRules.Armor, "", 64, Placement.Normal, "StorehouseBoard:20,IronNails:12,Iron:4", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }),
			Store("weaponrack", "Weapon Rack A", "A narrow rack for eight one-handed weapons at their true size. Put several side by side.", "One-handed weapons only", EquipRules.RackWeapon, "", 8, Placement.Normal, "StorehouseBoard:6,IronNails:4", "piece_workbench", null, null, display: false, 0, new string[4] { "_r4", "_r3", "_r2", "_r1" }),
			Store("weaponrackb", "Weapon Rack B", "The narrow weapon rack in a second build: eight one-handed weapons at their true size.", "One-handed weapons only", EquipRules.RackWeapon, "", 8, Placement.Normal, "StorehouseBoard:6,IronNails:4", "piece_workbench", null, null, display: false, 0, new string[4] { "_r4", "_r3", "_r2", "_r1" }),
			Store("arsenal", "Arsenal A", "Weapons and shields: one-handed and long weapons, bows, reserve shields edge-on and the best shield on show.", "Weapons, tools and shields only", EquipRules.Arsenal, "", 64, Placement.Normal, "StorehouseBoard:16,IronNails:10,Iron:4", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }),
			Shown("Arsenal", Store("arsenalb", "Arsenal B", "Weapons, bows, reserve shields edge-on and the best shield on show.", "Weapons, tools and shields only", EquipRules.Arsenal, "", 32, Placement.Normal, "StorehouseBoard:18,IronNails:10,Iron:4", "piece_workbench", null, null, display: false, 0, new string[5] { "_r6", "_r4", "_r3", "_r2", "_r1" })),
			Store("arsenalkit_spine", "Arsenal Spine", "A trestle spine with arms on both faces: swords, knives, axes and maces hung by their heads in three tiers. Shield troughs snap to its sides, the long and bow racks to its ends.", "One-handed swords, knives, axes and maces only", EquipRules.KitSpine, "", 56, Placement.Normal, "StorehouseBoard:20,IronNails:12,Iron:4", "piece_workbench", null, null, display: false, 0, new string[2] { "_r5", "_r4" }),
			Store("arsenalkit_shield", "Arsenal Shield Trough", "A low trough with a high back: shields lean in their own heel slots, the largest in the turned end bay. Snaps to a side of the Arsenal Spine.", "Shields only", EquipRules.KitShield, "", 10, Placement.Normal, "StorehouseBoard:12,IronNails:8", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3b", "_r3", "_r4" }, mirror: false, 5),
			Store("arsenalkit_long", "Arsenal Long Rack", "A straw-filled box with an upper guide: spears, atgeirs, sledges, battleaxes and greatswords standing upright. Snaps to an end of the Arsenal Spine.", "Spears and two-handed weapons only", EquipRules.KitLong, "", 12, Placement.Normal, "StorehouseBoard:12,IronNails:8", "piece_workbench", null, null, display: false, 0, new string[4] { "_r5c", "_r5b", "_r5", "_r4" }, mirror: false, 6),
			Store("arsenalkit_bow", "Arsenal Bow Rack", "An A-frame end cap: six bows in skinny slots and two crossbows across their pegs. Snaps to an end of the Arsenal Spine.", "Bows and crossbows only", EquipRules.KitBow, "", 8, Placement.Normal, "StorehouseBoard:8,IronNails:6", "piece_workbench", null, null, display: false, 0, new string[2] { "_r4", "_r5" }),
			Store("armory", "Armory A", "Armor, weapons and shields together: armor folded in the coffer, weapons racked, the best shield on show.", "Armor, weapons and shields only", EquipRules.Combined, "", 64, Placement.Normal, "StorehouseBoard:24,IronNails:14,Iron:6", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }),
			Shown("Armory", Store("armoryb", "Armory B", "Reachable from both sides: armor, weapons and shields together.", "Armor, weapons and shields only", EquipRules.Combined, "", 64, Placement.Normal, "StorehouseBoard:24,IronNails:14,Iron:6", "piece_workbench", null, null, display: false, 0, new string[4] { "_r2c", "_r2b", "_r2", "_r1" })),
			Store("armoryrail", "Armory Rail", "A rail of carved wooden mannequins, each wearing a full set of armor with its cape, weapon and shield, ready to grab.", "Armor, one-handed weapons and shields only", EquipRules.Mannequin, "", 36, Placement.Normal, "StorehouseBoard:18,IronNails:10,Iron:4", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3b", "_r3", "_r1" }, mirror: false, 6),
			Store("armoryisland", "Armory Island", "A two-sided island of carved wooden mannequins, each wearing a full set of armor with its cape, weapon and shield.", "Armor, one-handed weapons and shields only", EquipRules.Mannequin, "", 36, Placement.Normal, "StorehouseBoard:20,IronNails:12,Iron:4", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3", "_r2", "_r1" }, mirror: false, 6),
			Store("armoryrail3", "Short Armory Rail", "A short rail of three carved wooden mannequins, each wearing a full set of armor with its cape, weapon and shield.", "Armor, one-handed weapons and shields only", EquipRules.Mannequin, "", 18, Placement.Normal, "StorehouseBoard:10,IronNails:6,Iron:2", "piece_workbench", null, null, display: false, 0, new string[3] { "_r3b", "_r3", "_r1" }, mirror: false, 3),
			Store("armorstand", "Armor Stand", "A carved wooden mannequin on its own deck, wearing a full set of armor with its cape, weapon and shield.", "Armor, one-handed weapons and shields only", EquipRules.Mannequin, "", 6, Placement.Normal, "StorehouseBoard:5,IronNails:3,Iron:1", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }, mirror: false, 1),
			Store("armorstandopen", "Armor Stand, open", "A carved wooden mannequin on its own open deck, without the back rail, wearing a full set of armor with its cape, weapon and shield. Snaps to other stands and the armory rails.", "Armor, one-handed weapons and shields only", EquipRules.Mannequin, "", 6, Placement.Normal, "StorehouseBoard:5,IronNails:3,Iron:1", "piece_workbench", null, null, display: false, 0, new string[1] { "_r2" }, mirror: false, 1),
			Store("utilitywall", "Utility Wall", "Shelves and sockets for tools, belts and wearables, trinkets, and a saddle on the deep bay floor.", "Tools, utility items, trinkets and saddles only", EquipRules.Utility, "Saddle*", 64, Placement.Normal, "StorehouseBoard:14,IronNails:8,Iron:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }),
			Store("utilityisland", "Utility Island", "A low island table for tools, wearables, trinkets and two saddles on its top.", "Tools, utility items, trinkets and saddles only", EquipRules.Utility, "Saddle*", 64, Placement.Normal, "StorehouseBoard:12,IronNails:8,Iron:2", "piece_workbench", null, null, display: false, 0, new string[2] { "_r2", "_r1" }),
			Shelf(1, "StorehouseBoard:5,BronzeNails:4"),
			Shelf(2, "StorehouseBoard:10,BronzeNails:8"),
			Shelf(3, "StorehouseBoard:15,BronzeNails:12")
		};

		public static readonly HashSet<string> Released = new HashSet<string>(StringComparer.Ordinal)
		{
			"meadrack", "larder", "meatrack", "rootbin", "orebin", "bunker", "bunkeropen", "metalrack", "metalrackhalf", "metalracklow",
			"metalracklowhalf", "metalpallet", "coopersrow", "coopersrowstack", "fletcher", "treasure", "apothecary", "hiderack", "clothrack", "ossuary",
			"emergencyshelf"
		};

		public static readonly HashSet<string> NextRelease = new HashSet<string>(StringComparer.Ordinal)
		{
			"armoryrail", "armoryrail3", "armoryisland", "armorstand", "weaponrack", "weaponrackb", "bolttubs", "bombcrate", "arrowtubes", "apothecarytall",
			"arsenalb", "armoryb", "embercabinet", "pantry", "smithdrawers", "jeweller", "trophyhall13x5", "trophyhall10x7", "arsenalkit_spine", "arsenalkit_shield",
			"arsenalkit_long", "arsenalkit_bow", "armorstandopen", "armorcoffer", "armorcabinet"
		};

		public static readonly (string Name, string[] Slugs)[] Groups = new(string, string[])[10]
		{
			("Wood, ore and stone", new string[4] { "timbercrib", "orebin", "bunker", "bunkeropen" }),
			("Metal", new string[5] { "metalrack", "metalrackhalf", "metalracklow", "metalracklowhalf", "metalpallet" }),
			("Kitchen", new string[5] { "meadrack", "larder", "meatrack", "rootbin", "pantry" }),
			("Crafting goods", new string[9] { "coopersrow", "coopersrowstack", "apothecary", "apothecarytall", "hiderack", "clothrack", "ossuary", "smithdrawers", "embercabinet" }),
			("Ammunition", new string[4] { "fletcher", "arrowtubes", "bolttubs", "bombcrate" }),
			("Weapons", new string[7] { "weaponrack", "weaponrackb", "arsenalb", "arsenalkit_spine", "arsenalkit_shield", "arsenalkit_long", "arsenalkit_bow" }),
			("Armor", new string[8] { "armoryrail", "armoryrail3", "armoryisland", "armorstand", "armorstandopen", "armorcabinet", "armorcoffer", "armoryb" }),
			("Trophies", new string[2] { "trophyhall13x5", "trophyhall10x7" }),
			("Valuables", new string[2] { "treasure", "jeweller" }),
			("Shelves", new string[1] { "emergencyshelf" })
		};

		public static readonly Kind[] All = new Kind[1] { Crib }.Concat(Stores.Where(InRelease)).OrderBy(Rank).ToArray();

		private static string SectionSafe(string title)
		{
			return new string(title.Where((char c) => Array.IndexOf(notInSection, c) < 0).ToArray());
		}

		private static Kind Store(string slug, string title, string desc, string label, Func<ItemData, bool> rule, string accepts, int units, Placement placement, string recipe, string station, string piece = null, string prefab = null, bool display = false, int planks = 0, string[] rounds = null, bool mirror = false, int columns = 0, bool heap = false, bool homes = false)
		{
			string text = ((piece == null) ? slug : (slug + "_" + piece));
			if (prefab == null)
			{
				prefab = "piece_storehouse_" + text;
			}
			return new Kind
			{
				Title = title,
				Prefab = prefab,
				Token = "$" + prefab,
				Base = "piece_chest_wood",
				Section = "4 - " + SectionSafe(title),
				Slug = slug,
				Piece = piece,
				Desc = desc,
				FilterLabel = label,
				Rule = rule,
				DefaultAccepts = accepts,
				DefaultRows = (units + ((columns > 0) ? columns : 8) - 1) / ((columns > 0) ? columns : 8),
				Columns = columns,
				Placement = placement,
				DefaultRecipe = recipe,
				DefaultStation = station,
				Display = display,
				Planks = planks,
				Rounds = rounds,
				Mirror = mirror,
				Heap = heap,
				Homes = homes,
				Build = delegate(GameObject go, Container container)
				{
					StoreModel.Get(slug, piece, title).BuildPiece(go);
				},
				Dress = delegate(GameObject go)
				{
					StoreModel.Get(slug, piece, title).BuildPiece(go, quiet: true);
				}
			};
		}

		private static Kind Shown(string name, Kind k)
		{
			k.Shown = name;
			return k;
		}

		private static Kind Never(HashSet<string> refused, string why, Kind k)
		{
			k.Never = refused;
			k.NeverWhy = why;
			return k;
		}

		private static Kind Shelf(int planks, string recipe)
		{
			return Store("emergencyshelf", string.Format("Shelf ({0} plank{1})", planks, (planks > 1) ? "s" : ""), "A 2 m wall shelf with " + planks switch
			{
				2 => "two planks", 
				1 => "one plank", 
				_ => "three planks", 
			} + " for what you grab in a hurry. Holds anything; only what fits on the planks shows. Hangs on a wall.", null, null, null, 0, Placement.Wall, recipe, "piece_workbench", planks.ToString(CultureInfo.InvariantCulture), null, display: true, planks, new string[2] { "_r5", "_r4" });
		}

		public static bool Buildable(Kind k)
		{
			if (k != Crib)
			{
				return InRelease(k);
			}
			return true;
		}

		public static bool InRelease(Kind k)
		{
			if (k.Slug != null)
			{
				if (!Released.Contains(k.Slug))
				{
					return NextRelease.Contains(k.Slug);
				}
				return true;
			}
			return false;
		}

		private static string GroupSlug(Kind k)
		{
			if (k != Crib)
			{
				return k.Slug;
			}
			return "timbercrib";
		}

		public static string GroupOf(Kind k)
		{
			return Groups.FirstOrDefault(((string Name, string[] Slugs) g) => Array.IndexOf(g.Slugs, GroupSlug(k)) >= 0).Name;
		}

		private static int Rank(Kind k)
		{
			int num = 0;
			(string, string[])[] groups = Groups;
			for (int i = 0; i < groups.Length; i++)
			{
				string[] item = groups[i].Item2;
				for (int j = 0; j < item.Length; j++)
				{
					if (item[j] == GroupSlug(k))
					{
						return num;
					}
					num++;
				}
			}
			return int.MaxValue;
		}

		public static Kind ByToken(string token)
		{
			if (token == null || token.Length < 10 || token[0] != '$')
			{
				return null;
			}
			for (int i = 0; i < All.Length; i++)
			{
				if (All[i].Token == token)
				{
					return All[i];
				}
			}
			return null;
		}
	}
	internal static class Settings
	{
		public enum TooltipStyles
		{
			Sentence,
			Short,
			Custom
		}

		public static ConfigEntry<bool> DumpPrefabs;

		public static ConfigEntry<bool> LiveModels;

		public static ConfigEntry<bool> MatteFrames;

		public static ConfigEntry<bool> ShowLoadedLine;

		public static readonly Dictionary<string, ConfigEntry<float>> TrophyScale = new Dictionary<string, ConfigEntry<float>>(StringComparer.Ordinal);

		internal static readonly string[] TrophyKinds = "TrophyAbomination,TrophyAsksvin,TrophyBarka,TrophyBjorn,TrophyBjornUndead,TrophyBlob,TrophyBlob_Frost,TrophyBlob_Lava,TrophyBlob_Morkhalla,TrophyBoar,TrophyBonemass,TrophyBonemawSerpent,TrophyCharredArcher,TrophyCharredMage,TrophyCharredMelee,TrophyCultist,TrophyCultist_Hildir,TrophyDeathsquito,TrophyDeer,TrophyDeerWhite,TrophyDragonQueen,TrophyDraugr,TrophyDraugrElite,TrophyDraugrFem,TrophyDvergr,TrophyEikthyr,TrophyElaking,TrophyFader,TrophyFallenValkyrie,TrophyFenring,TrophyForestTroll,TrophyFrostTroll,TrophyGhost,TrophyGjall,TrophyGoblin,TrophyGoblinBrute,TrophyGoblinBruteBrosBrute,TrophyGoblinBruteBrosShaman,TrophyGoblinKing,TrophyGoblinShaman,TrophyGreydwarf,TrophyGreydwarfBrute,TrophyGreydwarfShaman,TrophyGrowth,TrophyHare,TrophyHatchling,TrophyJotunWarrior,TrophyJotunWitch,TrophyKvastur,TrophyLeech,TrophyLox,TrophyMole,TrophyMoose,TrophyMorgen,TrophyNeck,TrophySGolem,TrophySeal,TrophySeeker,TrophySeekerBrute,TrophySeekerQueen,TrophySerpent,TrophySkeleton,TrophySkeletonHildir,TrophySkeletonPoison,TrophySurtling,TrophyTheElder,TrophyTick,TrophyUlv,TrophyVolture,TrophyWolf,TrophyWraith,TrophyWrithan".Split(',');

		private static readonly Dictionary<string, float> TrophyDefaults = new Dictionary<string, float>(StringComparer.Ordinal)
		{
			["TrophyAbomination"] = 1.25f,
			["TrophyAsksvin"] = 1.1f,
			["TrophyBarka"] = 1.5f,
			["TrophyBjorn"] = 1.5f,
			["TrophyBjornUndead"] = 1.5f,
			["TrophyBlob_Frost"] = 0.9f,
			["TrophyBlob_Lava"] = 0.9f,
			["TrophyBlob_Morkhalla"] = 0.9f,
			["TrophyBoar"] = 1.3f,
			["TrophyBonemawSerpent"] = 1.2f,
			["TrophyDeathsquito"] = 2f,
			["TrophyDeer"] = 2.3f,
			["TrophyDeerWhite"] = 2.3f,
			["TrophyDragonQueen"] = 2.65f,
			["TrophyDraugr"] = 1.5f,
			["TrophyDraugrElite"] = 1.5f,
			["TrophyDraugrFem"] = 1.5f,
			["TrophyEikthyr"] = 3f,
			["TrophyElaking"] = 1.4f,
			["TrophyFader"] = 1.5f,
			["TrophyFallenValkyrie"] = 1.2f,
			["TrophyFenring"] = 1.4f,
			["TrophyGjall"] = 1.7f,
			["TrophyGoblinKing"] = 1.25f,
			["TrophyGoblinShaman"] = 1.35f,
			["TrophyGreydwarf"] = 1.15f,
			["TrophyGreydwarfBrute"] = 1.15f,
			["TrophyGreydwarfShaman"] = 1.65f,
			["TrophyHare"] = 0.8f,
			["TrophyHatchling"] = 1.7f,
			["TrophyJotunWarrior"] = 1.6f,
			["TrophyJotunWitch"] = 1.3f,
			["TrophyKvastur"] = 1.35f,
			["TrophyLeech"] = 0.95f,
			["TrophyLox"] = 1.7f,
			["TrophyMoose"] = 2.1f,
			["TrophyMorgen"] = 0.9f,
			["TrophyNeck"] = 1.1f,
			["TrophySGolem"] = 1.3f,
			["TrophySeal"] = 1.25f,
			["TrophySeeker"] = 1.15f,
			["TrophySeekerBrute"] = 1.7f,
			["TrophySeekerQueen"] = 1.85f,
			["TrophySerpent"] = 1.7f,
			["TrophySkeleton"] = 0.8f,
			["TrophySkeletonHildir"] = 0.85f,
			["TrophySkeletonPoison"] = 0.9f,
			["TrophySurtling"] = 0.6f,
			["TrophyTheElder"] = 2.35f,
			["TrophyTick"] = 0.75f,
			["TrophyUlv"] = 2.6f,
			["TrophyVolture"] = 1.9f,
			["TrophyWolf"] = 0.94f
		};

		private static readonly HashSet<string> GameBosses = new HashSet<string>(StringComparer.Ordinal) { "TrophyEikthyr", "TrophyTheElder", "TrophyBonemass", "TrophyDragonQueen", "TrophyGoblinKing", "TrophySeekerQueen", "TrophyFader" };

		public static ConfigEntry<string> ExtraBosses;

		public static ConfigEntry<bool> StoreInTooltip;

		public static ConfigEntry<TooltipStyles> TooltipStyle;

		public static ConfigEntry<string> TooltipFormat;

		public static ConfigEntry<string> ModelFolder;

		public static ConfigEntry<bool> SkinChests;

		public static ConfigEntry<KeyboardShortcut> SkinKey;

		public static ConfigEntry<float> DisplaySize;

		public static ConfigEntry<int> SlotsPerPlank;

		public static ConfigEntry<bool> ShowContents;

		public static ConfigEntry<bool> ContentsShadows;

		public static ConfigEntry<bool> Instancing;

		public static ConfigEntry<bool> PerfLog;

		public static ConfigEntry<bool> PerfOverlay;

		public static ConfigEntry<bool> ManyKindBoards;

		public static ConfigEntry<bool> ContentsInShade;

		public static ConfigEntry<bool> PlacementCheck;

		public static ConfigEntry<float> MannequinTurn;

		public static ConfigEntry<float> ContentsDistance;

		public static ConfigEntry<int> MaxTrisPerStack;

		public static ConfigEntry<float> FarContents;

		internal static readonly Dictionary<string, string[]> PastAccepts = new Dictionary<string, string[]>
		{
			["2 - Timber Crib"] = new string[2] { "Wood,FineWood,RoundLog,ElderBark,YggdrasilWood,Blackwood,Frostwood,StorehouseBoard,Root,WrithanRoots,BarkaBranch", "Wood,FineWood,RoundLog,ElderBark,YggdrasilWood,Blackwood,Frostwood" },
			["4 - Apothecary Rack"] = new string[2] { "Feathers,Needle,GreydwarfEye,GiantBloodSack,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale", "Feathers,Needle,GreydwarfEye,RoyalJelly,GiantBloodSack,Ectoplasm,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,SurtlingCore,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale" },
			["4 - Cloth Rack"] = new string[1] { "LinenThread,JuteRed,JuteBlue,NornThread" },
			["4 - Coal & Stone Bunker"] = new string[3] { "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,SulfurStone,StoneRock,PungentPebbles,BFP_Clay,Ice", "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,Crystal,SulfurStone,StoneRock,PungentPebbles,Thunderstone,BFP_Clay,FrozenFuel,Ice", "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,Crystal" },
			["4 - Coopers Row"] = new string[2] { "Tar,Resin,Sap,Guck,Eitr,Ooze,RoyalJelly,Honey,CharcoalResin,OozeMork,HatefulBlood,Voidplasm,BlobVial,Ectoplasm", "Tar,Resin,Sap,Guck,Eitr,Ooze" },
			["4 - Ember Cabinet"] = new string[3] { "Wisp,GlowWorm,FaderEmber,MemorialCoal,MoltenCore,BlackCore,FrostCore,ShieldCore,OrbFrostFire,OrbThunderBlood,SurtlingCore,FrozenFuel,Thunderstone,Crystal", "Wisp,GlowWorm,FaderEmber,MemorialCoal,MoltenCore,BlackCore,FrostCore,ShieldCore,OrbFrostFire,OrbThunderBlood,SurtlingCore,FrozenFuel,Thunderstone", "Wisp,GlowWorm,FaderEmber,MemorialCoal,MoltenCore,BlackCore,FrostCore,ShieldCore,OrbFrostFire,OrbThunderBlood,SurtlingCore" },
			["4 - Fletchers Barrels"] = new string[1] { "Arrow*,Bolt*,Bomb*,Dynamite" },
			["4 - Jewellers Casket"] = new string[1] { "Coins,Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,AncientCoin,SilverNecklace,CrownJewel,Pot_Shard*" },
			["4 - Mead Rack"] = new string[1] { "BarleyWine,MeadBase*" },
			["4 - Open Coal & Stone Bunker"] = new string[3] { "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,SulfurStone,StoneRock,PungentPebbles,BFP_Clay,Ice", "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,Crystal,SulfurStone,StoneRock,PungentPebbles,Thunderstone,BFP_Clay,FrozenFuel,Ice", "Coal,Stone,Flint,Grausten,BlackMarble,Obsidian,Crystal" },
			["4 - Ossuary Bin"] = new string[1] { "BoneFragments,CharredBone,WitheredBone,Charredskull,HardAntler,AsksvinCarrionSkull,BonemawSerpentTooth" },
			["4 - Produce Crates"] = new string[1] { "Turnip,Onion,Carrot,Barley,Flax,Thistle,Dandelion,Fiddleheadfern,*Seeds,Mushroom*,*berries,Raspberry,Cloudberry" },
			["4 - Stacking Coopers Row"] = new string[2] { "Tar,Resin,Sap,Guck,Eitr,Ooze,RoyalJelly,Honey,CharcoalResin,OozeMork,HatefulBlood,Voidplasm,BlobVial,Ectoplasm", "Tar,Resin,Sap,Guck,Eitr,Ooze" },
			["4 - Tall Apothecary Rack"] = new string[1] { "Feathers,Needle,GreydwarfEye,GiantBloodSack,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale" },
			["4 - Treasure Hoard"] = new string[1] { "Coins,Amber,Ruby,AmberPearl" }
		};

		internal static readonly Dictionary<string, string[]> TestAccepts = new Dictionary<string, string[]>
		{
			["4 - Treasure Hoard"] = new string[1] { "Coins,Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,AncientCoin,SilverNecklace,CrownJewel,Pot_Shard*" },
			["4 - Jewellers Casket"] = new string[1] { "Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,AncientCoin,SilverNecklace,CrownJewel,Pot_Shard*" },
			["4 - Apothecary Rack"] = new string[2] { "Feathers,Needle,GreydwarfEye,GiantBloodSack,Ectoplasm,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,SurtlingCore,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale", "Feathers,Needle,GreydwarfEye,GiantBloodSack,Ectoplasm,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale" },
			["4 - Tall Apothecary Rack"] = new string[2] { "Feathers,Needle,GreydwarfEye,GiantBloodSack,Ectoplasm,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,SurtlingCore,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale", "Feathers,Needle,GreydwarfEye,GiantBloodSack,Ectoplasm,AskBladder,WolfFang,Carapace,WolfHairBundle,ElakingHairBundle,Chitin,CelestialFeather,MooseSinew,BjornPaw,WolfClaw,MorgenHeart,Bilebag,MorgenSinew,DragonTear,Mandible,SerpentScale,BonemawSerpentScale" }
		};

		internal static readonly Dictionary<string, int[]> PastRows = new Dictionary<string, int[]>
		{
			["4 - Apothecary Rack"] = new int[1] { 4 },
			["4 - Armory Island"] = new int[1] { 8 },
			["4 - Armory Rail"] = new int[2] { 5, 8 },
			["4 - Arsenal B"] = new int[1] { 8 },
			["4 - Bomb Crate"] = new int[1] { 5 },
			["4 - Coopers Row"] = new int[1] { 4 },
			["4 - Fletchers Barrels"] = new int[1] { 4 },
			["4 - Hide Rack"] = new int[1] { 4 },
			["4 - Larder Hutch"] = new int[1] { 9 },
			["4 - Meat Rack"] = new int[1] { 10 },
			["4 - Ossuary Bin"] = new int[1] { 4 },
			["4 - Short Armory Rail"] = new int[1] { 3 },
			["4 - Treasure Hoard"] = new int[1] { 4 }
		};

		private static readonly HashSet<string> Shrunk = new HashSet<string>(StringComparer.Ordinal) { "arsenalb" };

		private static ConfigFile seen;

		private static ConfigFile fallback;

		private static int bound;

		private static readonly Dictionary<string, string> F1Names = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			["Performance (dev)"] = "General",
			["Shelf"] = "Shelves"
		};

		private static readonly Dictionary<string, string> LookOf = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			["coopersrowstack"] = "coopersrow",
			["apothecarytall"] = "apothecary"
		};

		public static bool IsBossTrophy(string prefab)
		{
			if (!GameBosses.Contains(prefab))
			{
				string text = ExtraBosses?.Value;
				if (text != null && text.Length > 0)
				{
					return text.Split(',').Any((string x) => string.Equals(x.Trim(), prefab, StringComparison.Ordinal));
				}
				return false;
			}
			return true;
		}

		private static ConfigEntry<string> Seen(Kind k, string key)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected O, but got Unknown
			if (seen == null)
			{
				seen = new ConfigFile(Path.Combine(Paths.ConfigPath, "com.joostliebregts.storehouse.defaults.cfg"), true);
			}
			return seen.Bind<string>(k.Section, key, "", "The default Storehouse last met here; it tells an edit from an old default. Leave it be.");
		}

		private static void MigrateRows(Kind k)
		{
			if (k.Rows == null)
			{
				return;
			}
			ConfigEntry<string> obj = Seen(k, "Rows");
			string value = obj.Value;
			obj.Value = k.DefaultRows.ToString();
			if (k.Rows.Value == k.DefaultRows)
			{
				return;
			}
			int value2 = k.Rows.Value;
			int result;
			bool flag = int.TryParse(value, out result);
			if (flag && result == k.DefaultRows)
			{
				return;
			}
			bool num;
			if (!flag)
			{
				if (!PastRows.TryGetValue(k.Section, out var value3))
				{
					goto IL_015a;
				}
				num = value3.Contains(value2);
			}
			else
			{
				num = result == value2;
			}
			if (num)
			{
				if (k.DefaultRows > value2 || Shrunk.Contains(k.Slug))
				{
					k.Rows.Value = k.DefaultRows;
					Plugin.Log.LogInfo((object)(string.Format("Storehouse: {0} Rows {1} -> {2}, the {3} default", k.Title, value2, k.DefaultRows, "0.8.8") + ((k.DefaultRows < value2) ? "; placed ones keep their rows until what they hold fits." : ".")));
				}
				else
				{
					Plugin.Log.LogWarning((object)string.Format("Storehouse: {0} Rows kept at {1}: the {2} default {3} is fewer, and fewer rows would hide what lies in the others.", k.Title, value2, "0.8.8", k.DefaultRows));
				}
				return;
			}
			goto IL_015a;
			IL_015a:
			Plugin.Log.LogWarning((object)string.Format("Storehouse: {0} Rows {1} was edited, kept. The {2} default is {3}.", k.Title, value2, "0.8.8", k.DefaultRows));
		}

		private static void MigrateAccepts(Kind k)
		{
			if (k.Accepts == null || k.DefaultAccepts == null)
			{
				return;
			}
			ConfigEntry<string> obj = Seen(k, "Accepts");
			string value = obj.Value;
			obj.Value = k.DefaultAccepts;
			HashSet<string> have = Names(k.Accepts.Value);
			HashSet<string> want = Names(k.DefaultAccepts);
			if (have.SetEquals(want) || (value.Length > 0 && Names(value).SetEquals(want)))
			{
				return;
			}
			string[] value2;
			string[] value3;
			string[] array = ((value.Length <= 0) ? (PastAccepts.TryGetValue(k.Section, out value2) ? value2 : new string[0]).Concat(TestAccepts.TryGetValue(k.Section, out value3) ? value3 : new string[0]).ToArray() : new string[1] { value });
			if (array.Length == 0)
			{
				array = null;
			}
			if (array != null && array.Any((string v) => Names(v).SetEquals(have)))
			{
				k.Accepts.Value = k.DefaultAccepts;
				Plugin.Log.LogInfo((object)("Storehouse: " + k.Title + " Accepts updated to the 0.8.8 default."));
				return;
			}
			if (array != null && array.Any((string v) => Names(v).IsSubsetOf(have)) && have.IsSubsetOf(want))
			{
				k.Accepts.Value = k.DefaultAccepts;
				Plugin.Log.LogInfo((object)("Storehouse: " + k.Title + " Accepts updated to the 0.8.8 default, which keeps every name added to it."));
				return;
			}
			List<string> list = want.Where((string x) => !have.Contains(x)).ToList();
			List<string> list2 = have.Where((string x) => !want.Contains(x)).ToList();
			Plugin.Log.LogWarning((object)("Storehouse: " + k.Title + " Accepts was edited, kept as it is. The 0.8.8 default would " + ((list.Count > 0) ? ("add " + string.Join(", ", list)) : "add nothing") + ((list2.Count > 0) ? (" and remove " + string.Join(", ", list2)) : "") + "."));
			static HashSet<string> Names(string v)
			{
				return new HashSet<string>(from x in (v ?? "").Split(',')
					select x.Trim() into x
					where x.Length > 0
					select x, StringComparer.OrdinalIgnoreCase);
			}
		}

		internal static ConfigEntry<T> Safe<T>(this ConfigFile config, string section, string key, T value, ConfigDescription description = null)
		{
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ce: Expected O, but got Unknown
			//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0107: Expected O, but got Unknown
			try
			{
				description = InF1(section, key, description);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Storehouse: '" + section + "' / '" + key + "' keeps its plain F1 look (" + ex.Message + ")."));
			}
			try
			{
				return config.Bind<T>(section, key, value, description);
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogError((object)$"Storehouse: setting '{section}' / '{key}' could not be bound; it stays at its default {value} for this session ({ex2.Message}).");
				if (fallback == null)
				{
					fallback = new ConfigFile(Path.Combine(Paths.CachePath ?? Path.GetTempPath(), "Storehouse.fallback.cfg"), false)
					{
						SaveOnConfigSet = false
					};
				}
				return fallback.Bind<T>(Clean(section), Clean(key), value, new ConfigDescription(((description != null) ? description.Description : null) ?? "", (AcceptableValueBase)null, Array.Empty<object>()));
			}
			static string Clean(string s)
			{
				string text = new string((s ?? "").Where((char c) => "=\n\t\\\"'[]".IndexOf(c) < 0).ToArray()).Trim();
				if (text == null || text.Length <= 0)
				{
					return "Setting";
				}
				return text;
			}
		}

		internal static ConfigEntry<T> Safe<T>(this ConfigFile config, string section, string key, T value, string description)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Expected O, but got Unknown
			return config.Safe(section, key, value, new ConfigDescription(description, (AcceptableValueBase)null, Array.Empty<object>()));
		}

		private static ConfigDescription InF1(string section, string key, ConfigDescription description)
		{
			//IL_0179: Unknown result type (might be due to invalid IL or missing references)
			//IL_0216: Unknown result type (might be due to invalid IL or missing references)
			//IL_021c: Expected O, but got Unknown
			Kind kind = Kinds.All.FirstOrDefault((Kind k) => k.Section == section);
			string text = Regex.Replace(section ?? "", "^\\d+ - ", "");
			string value;
			string category = ((kind != null) ? (Kinds.GroupOf(kind) ?? "Test pieces (dev)") : null) ?? ((key == "Display item size") ? "General" : (F1Names.TryGetValue(text, out value) ? value : text));
			string text2;
			if (kind?.Slug == "coopersrow")
			{
				bool flag = ((key == "Item size" || key == "Glass colour") ? true : false);
				text2 = (flag ? "Cooper's Rows (both)" : "Cooper's Row (ground)");
			}
			else
			{
				bool flag = kind?.Slug == "apothecary";
				if (flag)
				{
					bool flag2 = ((key == "Item size" || key == "Glass colour") ? true : false);
					flag = flag2;
				}
				text2 = (flag ? "Apothecary Racks (both)" : ((kind != null) ? (kind.Shown ?? kind.Title) : ((text == "Shelf") ? "Shelf" : null)));
			}
			string text3 = text2;
			object[] source = ((description != null) ? description.Tags : null) ?? new object[0];
			ConfigurationManagerAttributes shown = (ConfigurationManagerAttributes)(((object)source.OfType<ConfigurationManagerAttributes>().FirstOrDefault()) ?? ((object)new ConfigurationManagerAttributes()));
			shown.Category = category;
			shown.Order = -(++bound);
			if (text3 != null)
			{
				shown.DispName = text3 + ": " + key;
			}
			return new ConfigDescription(((description != null) ? description.Description : null) ?? "", (description != null) ? description.AcceptableValues : null, new object[1] { shown }.Concat(source.Where((object t) => t != shown)).ToArray());
		}

		public static void Bind(ConfigFile config)
		{
			//IL_045c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0499: Unknown result type (might be due to invalid IL or missing references)
			//IL_04a3: Expected O, but got Unknown
			//IL_04f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0501: Expected O, but got Unknown
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0139: Expected O, but got Unknown
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected O, but got Unknown
			//IL_01b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_05ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_05f4: Expected O, but got Unknown
			//IL_0340: Unknown result type (might be due to invalid IL or missing references)
			//IL_034a: Expected O, but got Unknown
			//IL_0376: Unknown result type (might be due to invalid IL or missing references)
			//IL_0380: Expected O, but got Unknown
			//IL_01e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f3: Expected O, but got Unknown
			//IL_03d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_03e0: Expected O, but got Unknown
			//IL_02e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ea: Expected O, but got Unknown
			BindGeneral(config);
			Kind[] all = Kinds.All;
			string[] rounds;
			foreach (Kind kind in all)
			{
				if (kind.Planks == 0 && !Mannequins.Is(kind) && !TypedSlots.Is(kind))
				{
					kind.Rows = config.Safe(kind.Section, "Rows", kind.DefaultRows, new ConfigDescription("Rows of 8 slots. The chest UI scrolls past 4 rows. Applies to chests that load after the change. Lowering it hides items in the removed rows (the game keeps them, out of reach) until the rows come back: empty them first.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 16), new object[1] { Synced() }));
				}
				try
				{
					MigrateRows(kind);
				}
				catch (Exception arg)
				{
					Plugin.Log.LogError((object)$"Storehouse: {kind.Title} Rows not migrated, kept as it is ({arg}).");
				}
				if (kind.Slug != null && LookOf.TryGetValue(kind.Slug, out var lookSlug))
				{
					Kind kind2 = Kinds.All.FirstOrDefault((Kind x) => x.Slug == lookSlug);
					if (kind2 != null && kind2.ItemSize != null)
					{
						kind.ItemSize = kind2.ItemSize;
						kind.GlassColour = kind2.GlassColour;
						goto IL_0168;
					}
				}
				if (kind.Planks == 0)
				{
					kind.ItemSize = config.Safe(kind.Section, "Item size", 1f, new ConfigDescription("Live: grows the item models inside this store by this factor, each about its own centre (1 = the layout's own size). Heaps may poke through walls above about 1.2. Only how it looks on your screen.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.8f, 1.6f), Array.Empty<object>()));
					kind.ItemSize.SettingChanged += delegate
					{
						if (Plugin.Live)
						{
							StoreModel.Touch();
						}
					};
				}
				goto IL_0168;
				IL_0168:
				string slug = kind.Slug;
				if ((slug == "coopersrow" || slug == "apothecary") ? true : false)
				{
					ConfigEntry<Color> entry = (kind.GlassColour = config.Safe<Color>(kind.Section, "Glass colour", Glass.Default, new ConfigDescription("Live: the colour of the glass (sight strips, jars), shared with the " + ((kind.Slug == "coopersrow") ? "Stacking Cooper's Row" : "Tall Apothecary Rack") + "; alpha is how opaque it is (at least 4%). Only how it looks on your screen.", (AcceptableValueBase)null, Array.Empty<object>())));
					entry.SettingChanged += delegate
					{
						//IL_005d: Unknown result type (might be due to invalid IL or missing references)
						if (!Plugin.Live)
						{
							return;
						}
						foreach (Kind item in Kinds.All.Where((Kind x) => x.GlassColour == entry))
						{
							StoreModel.Get(item.Slug, item.Piece, item.Title).Retint(entry.Value);
						}
					};
				}
				rounds = kind.Rounds;
				if (rounds != null && rounds.Length > 1 && kind.Slug != null && Kinds.ModelAlternates.TryGetValue(kind.Slug, out var value))
				{
					kind.Model = config.Safe(kind.Section, "Model", "newest", new ConfigDescription("Live (with Live models on): which model this store shows. newest = " + kind.Rounds[0].TrimStart('_') + ". Only how it looks on your screen.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2]
					{
						"newest",
						kind.Rounds[0].TrimStart('_')
					}.Concat(value.Select((string r) => r.TrimStart('_'))).ToArray()), Array.Empty<object>()));
					kind.Model.SettingChanged += delegate
					{
						ModelWatch.Poke();
					};
				}
				kind.Recipe = config.Safe(kind.Section, "Recipe", kind.DefaultRecipe, new ConfigDescription("Build cost: item prefab names with amounts, e.g. BlackMetal:12,Iron:8.", (AcceptableValueBase)null, new object[1] { Synced() }));
				kind.Station = config.Safe(kind.Section, "Crafting station", kind.DefaultStation, new ConfigDescription("Station prefab that must be nearby: piece_workbench, forge, piece_stonecutter, blackforge, piece_artisanstation, or empty for none.", (AcceptableValueBase)null, new object[1] { Synced() }));
				if (kind.DefaultAccepts != null)
				{
					kind.Accepts = config.Safe(kind.Section, "Accepts", kind.DefaultAccepts, new ConfigDescription((kind.Rule == null) ? "Item prefab names this chest takes, comma separated. Anything else is refused. Items already inside always stay." : ("Takes its own kind of item by type (" + kind.FilterLabel + "), plus these item prefab names, comma separated; * stands for any letters (MeadBase* is every mead base). Anything else is refused. Items already inside always stay."), (AcceptableValueBase)null, new object[1] { Synced() }));
					try
					{
						MigrateAccepts(kind);
					}
					catch (Exception arg2)
					{
						Plugin.Log.LogError((object)$"Storehouse: {kind.Title} Accepts not migrated, kept as it is ({arg2}).");
					}
					kind.Filter = new Filter(kind);
				}
			}
			SkinChests = config.Safe("8 - Skins (dev)", "Skin chests", value: true, "Dev, this PC only: look at a vanilla chest and press the hotkey to dress it as one of the Storehouse stores, with its real contents. Only you see it; the chest itself, its size and the network stay vanilla. Off: every chest shows its own look again (choices are kept).");
			SkinKey = config.Safe<KeyboardShortcut>("8 - Skins (dev)", "Hotkey", new KeyboardShortcut((KeyCode)107, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }), "Cycles the skin of the chest you look at: none, then each store, then none again.");
			DisplaySize = config.Safe("8 - Skins (dev)", "Display item size", 0.7f, new ConfigDescription("Dev, live: the size of the items in the display stores (Larder Hutch, Shelf) as a fraction of the item's true in-game size (as it lies in the world when dropped). The shelf's planks have room for up to about 0.8; taller items move on to a plank where they fit or are not shown. Mass stores (crib, racks, bins, bunker) keep their own fit.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.3f, 1.2f), Array.Empty<object>()));
			DisplaySize.SettingChanged += delegate
			{
				if (Plugin.Live)
				{
					StoreModel.Touch();
				}
			};
			SlotsPerPlank = config.Safe("4 - Shelf", "Slots per plank", 6, new ConfigDescription("Container slots per plank of the Shelf (1, 2 or 3 planks: 1, 2 or 3 rows of this many). 6 is what typically fits on a 2 m plank at Display item size 0.7 (meads about 0.2 m, meals about 0.4 m across); a stack that does not fit is not shown but stays in the chest. Applies to shelves that load after the change; lowering it hides the stacks in the removed slots until it goes back up.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 8), new object[1] { Synced() }));
			ShowLoadedLine = config.Safe("6 - Interface", "Show loaded line on the main menu", value: true, "On the main menu, top left: Storehouse <version> loaded: <n> pieces ready. Off hides that line only; the red did not load fully warning always shows. With it off, no line no longer means Storehouse did not load.");
			StoreInTooltip = config.Safe("6 - Interface", "Store in item tooltips", value: true, "An item's tooltip ends with the Storehouse stores that take it, e.g. Storehouse: goes in the Larder Hutch. Items no store takes get no line.");
			TooltipStyle = config.Safe("6 - Interface", "Tooltip style", TooltipStyles.Sentence, "Sentence: Storehouse: goes in the Utility Island or the Utility Wall. Short: Storehouse: Utility Island, Utility Wall. Custom: the Tooltip format below.");
			TooltipFormat = config.Safe("6 - Interface", "Tooltip format", "Storehouse: {stores}", "With Tooltip style Custom: the tooltip line, where {stores} becomes the stores that take the item, joined by commas.");
			ExtraBosses = config.Safe("6 - Interface", "Other boss trophies", "", "Item prefab names of other mods' boss trophies, comma separated. Like the game's seven bosses, they get a gold glow behind their icon in a store's window.");
			rounds = TrophyKinds;
			foreach (string key in rounds)
			{
				float value2;
				ConfigEntry<float> val = (TrophyScale[key] = config.Safe("5 - Trophy sizes", key, TrophyDefaults.TryGetValue(key, out value2) ? value2 : 1f, new ConfigDescription("Live: this trophy's size on a Trophy Hall board, times the hall's own fit (1 = as fitted). Only how it looks on your screen.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.3f, 3f), Array.Empty<object>())));
				val.SettingChanged += delegate
				{
					if (Plugin.Live)
					{
						StoreModel.Touch("trophyhall");
					}
				};
			}
			BindDebug(config);
		}

		private static void BindGeneral(ConfigFile config)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Expected O, but got Unknown
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Expected O, but got Unknown
			//IL_0138: Unknown result type (might be due to invalid IL or missing references)
			//IL_0142: Expected O, but got Unknown
			//IL_0199: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Expected O, but got Unknown
			MannequinTurn = config.Safe("7 - Performance (dev)", "Mannequin extra turn", 0f, new ConfigDescription("Dev, live: extra degrees the worn sets turn on their forms. They face the frame's open side by themselves; 0 normally.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f), Array.Empty<object>()));
			MannequinTurn.SettingChanged += delegate
			{
				if (Plugin.Live)
				{
					StoreModel.Touch();
				}
			};
			ContentsInShade = config.Safe("7 - Performance (dev)", "Contents in the frames shade", value: false, "Live: the items in a store take the shadows of its frame (a bunker's stacking top darkens its heaps). Off: they are lit by the surroundings only, so what lies inside reads (Joost, 25 Oct: the bunker's coal and stone looked black).");
			ManyKindBoards = config.Safe("7 - Performance (dev)", "Boards between three or more kinds", value: true, "Live: a bay that holds three or more kinds side by side (a bunker with more kinds than bays) gets a board between each. Off: those kinds lie as heaps side by side without boards; two kinds in a bay always keep their board.");
			ManyKindBoards.SettingChanged += delegate
			{
				if (Plugin.Live)
				{
					StoreModel.Touch();
				}
			};
			ShowContents = config.Safe("7 - Performance (dev)", "Show contents", value: true, "Dev, live: draw the item models inside the stores. Off: every store shows its frame only, so you can compare the FPS in the same spot.");
			ContentsDistance = config.Safe("7 - Performance (dev)", "Contents distance", 35f, new ConfigDescription("Dev, live: a store's contents are drawn only while the camera is within this many metres of it; further away they cost nothing per frame (and a store out of range rebuilds its contents only once you come near).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 150f), Array.Empty<object>()));
			MaxTrisPerStack = config.Safe("7 - Performance (dev)", "Max triangles per stack", 6000, new ConfigDescription("Live: a stack shows at most this many triangles of item copies (always at least one copy). Light items (ore, arrows, bones) are not affected; heavy ones (Feast platters, trophies of big bosses) show fewer copies. 0 = no limit.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 50000), Array.Empty<object>()));
			MaxTrisPerStack.SettingChanged += delegate
			{
				if (Plugin.Live)
				{
					StoreModel.Touch();
				}
			};
			FarContents = config.Safe("7 - Performance (dev)", "Far contents beyond", 12f, new ConfigDescription("Live: beyond this many metres a store draws its items with the game's own lighter far models (their last LOD), where an item has one. 0 = always the full models.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>()));
			ContentsShadows = config.Safe("7 - Performance (dev)", "Contents cast shadows", value: false, "Dev, live: the item models in the stores cast shadows. Hundreds of small casters are expensive (one extra draw per shadow cascade); the frame keeps its own shadows either way.");
			Instancing = config.Safe("7 - Performance (dev)", "GPU instancing", value: true, "Dev, live: draw the copies of an item whose mesh can't be merged (not readable) with one instanced draw per mesh and material, where the item's shader supports it. Off: one draw per copy. Turn off if copies of an item show up in one spot.");
			PerfLog = config.Safe("7 - Performance (dev)", "Perf log", value: false, "Dev, live: every 10 s write one line to the BepInEx log: FPS (average and 1% low), stores in range and in view, item copies drawn, draws issued by Storehouse, ms per frame spent in Storehouse code, and the last contents rebuild.");
			PerfOverlay = config.Safe("7 - Performance (dev)", "Perf overlay", value: false, "Dev, live: show the latest perf line on screen, top left.");
			PlacementCheck = config.Safe("7 - Performance (dev)", "Placement self-check", value: false, "Dev, live: every answer a typed store's window and display get by replay (which slots show hints, whether a dropped item shows, where each item is drawn) is also solved from scratch the 0.8.0 way, and any difference is logged as a warning (Placement self-check: ...). Costs what 0.8.0 cost, the Spine's lag included: on only to check.");
		}

		private static void BindDebug(ConfigFile config)
		{
			LiveModels = config.Safe("9 - Debug", "Live models", value: false, "Dev: check the model files once a second and swap a changed model into the running game (piece and placed ones).");
			ModelFolder = config.Safe("9 - Debug", "Model folder", "", "Dev: read the models from this folder (with a crib/ subfolder, and <store>_r3 or <store>_r2 subfolders for the dev stores) instead of the mod's own assets, e.g. the Blender export folder.");
			MatteFrames = config.Safe("9 - Debug", "Matte frames", value: true, "Dev, live: every store's frame without gloss, specular, metal, wetness or noise (the default since 25 Oct: straw glints and dark patches). Off: as the wood chest's material, to compare.");
			MatteFrames.SettingChanged += delegate
			{
				if (Plugin.Live)
				{
					Atlas.Matte(MatteFrames.Value);
				}
			};
			DumpPrefabs = config.Safe("9 - Debug", "Dump prefabs", value: true, "At game start, write the hierarchy, meshes, materials, colliders and LODs of the black metal chest and the three pieces to the BepInEx log.");
		}

		private static ConfigurationManagerAttributes Synced()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Expected O, but got Unknown
			return new ConfigurationManagerAttributes
			{
				IsAdminOnly = true
			};
		}

		public static List<(string item, int amount)> ParseRecipe(string s)
		{
			List<(string, int)> list = new List<(string, int)>();
			string[] array = (s ?? "").Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string[] array2 = array[i].Split(':');
				if (array2.Length == 2 && int.TryParse(array2[1].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0)
				{
					string text = array2[0].Trim();
					if (text.Length > 0)
					{
						list.Add((text, result));
					}
				}
			}
			return list;
		}

		public static void ApplyToPieces()
		{
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Expected O, but got Unknown
			Kind[] all = Kinds.All;
			foreach (Kind kind in all)
			{
				kind.Filter?.Invalidate();
				PrefabManager instance = PrefabManager.Instance;
				GameObject val = ((instance != null) ? instance.GetPrefab(kind.Prefab) : null);
				Piece val2 = (Object.op_Implicit((Object)(object)val) ? val.GetComponent<Piece>() : null);
				if (!Object.op_Implicit((Object)(object)val2))
				{
					continue;
				}
				List<Requirement> list = new List<Requirement>();
				bool flag = true;
				foreach (var item3 in ParseRecipe(kind.Recipe.Value))
				{
					string item = item3.item;
					int item2 = item3.amount;
					GameObject val3 = (Object.op_Implicit((Object)(object)ObjectDB.instance) ? ObjectDB.instance.GetItemPrefab(item) : PrefabManager.Instance.GetPrefab(item));
					ItemDrop val4 = (Object.op_Implicit((Object)(object)val3) ? val3.GetComponent<ItemDrop>() : null);
					if (!Object.op_Implicit((Object)(object)val4))
					{
						flag = false;
						Plugin.Log.LogWarning((object)(kind.Title + ": recipe item '" + item + "' not found; keeping the previous recipe."));
						break;
					}
					list.Add(new Requirement
					{
						m_resItem = val4,
						m_amount = item2,
						m_recover = true
					});
				}
				if (flag && list.Count > 0)
				{
					val2.m_resources = list.ToArray();
				}
				string text = kind.Station.Value?.Trim();
				if (string.IsNullOrEmpty(text))
				{
					val2.m_craftingStation = null;
					continue;
				}
				GameObject val5 = (Object.op_Implicit((Object)(object)ZNetScene.instance) ? ZNetScene.instance.GetPrefab(text) : PrefabManager.Instance.GetPrefab(text));
				if (Object.op_Implicit((Object)(object)val5))
				{
					CraftingStation component = val5.GetComponent<CraftingStation>();
					if (component != null)
					{
						val2.m_craftingStation = component;
						continue;
					}
				}
				Plugin.Log.LogWarning((object)(kind.Title + ": crafting station '" + text + "' not found; keeping the previous one."));
			}
		}
	}
	internal static class StorageReviewPolicy
	{
		public const string PantryUncooked = "BakedPoteitrUncooked,FishAndBreadUncooked,HoneyGlazedChickenUncooked,KaleChipsUncooked,LoxPieUncooked,MagicallyStuffedShroomUncooked,MeatPlatterUncooked,MisthareSupremeUncooked,OvenPancakeUncooked,PiquantPieUncooked,RoastedCrustPieUncooked,VikingCupcakeUncooked";

		public const string PantryDefaults = "SpiceForests,SpicePlains,SpiceMountains,SpiceOceans,SpiceMistlands,SpiceDeepNorth,SpiceAshlands,BarleyFlour,OatFlour,BreadDough,FragrantBundle,FeastAshlands_Material,FeastBlackforest_Material,FeastDeepNorth_Material,FeastMeadows_Material,FeastMistlands_Material,FeastMountains_Material,FeastOceans_Material,FeastPlains_Material,FeastSwamps_Material,BakedPoteitrUncooked,FishAndBreadUncooked,HoneyGlazedChickenUncooked,KaleChipsUncooked,LoxPieUncooked,MagicallyStuffedShroomUncooked,MeatPlatterUncooked,MisthareSupremeUncooked,OvenPancakeUncooked,PiquantPieUncooked,RoastedCrustPieUncooked,VikingCupcakeUncooked";

		public const string CooperLegacy = "Tar,Resin,Sap,Guck,Eitr,Ooze,RoyalJelly";

		public const string CooperDefaults = "Tar,Resin,Sap,Guck,Ooze,RoyalJelly,Honey,CharcoalResin,OozeMork,HatefulBlood,Voidplasm,BlobVial,Ectoplasm,ProustitePowder";

		public const string ValuablesDefaults = "Coins,Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,AncientCoin,SilverNecklace,CrownJewel,Pot_Shard*";

		public const string TreasureDefaults = "Coins,AncientCoin,Gold";

		public const string JewellerDefaults = "Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,SilverNecklace,CrownJewel,Pot_Shard*";

		private static readonly HashSet<string> uncookedFoods = new HashSet<string>("BakedPoteitrUncooked,FishAndBreadUncooked,HoneyGlazedChickenUncooked,KaleChipsUncooked,LoxPieUncooked,MagicallyStuffedShroomUncooked,MeatPlatterUncooked,MisthareSupremeUncooked,OvenPancakeUncooked,PiquantPieUncooked,RoastedCrustPieUncooked,VikingCupcakeUncooked".Split(','), StringComparer.OrdinalIgnoreCase);

		private static readonly HashSet<string> valuables = new HashSet<string>(from n in "Coins,Amber,Ruby,AmberPearl,GemstoneBlue,GemstoneGreen,GemstoneRed,AncientGemstone*,AncientCoin,SilverNecklace,CrownJewel,Pot_Shard*".Split(',')
			where !n.Contains('*')
			select n, StringComparer.Ordinal);

		private static readonly HashSet<string> wetParts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "entrails", "bloodclot", "giantbloodsack", "bloodbag", "freezegland" };

		private static readonly string[] notTreasure = new string[4] { "uncooked", "mold", "mould", "ammo" };

		public static bool NonFoodUncooked(string prefab)
		{
			if (!string.IsNullOrEmpty(prefab) && prefab.EndsWith("Uncooked", StringComparison.OrdinalIgnoreCase))
			{
				return !uncookedFoods.Contains(prefab);
			}
			return false;
		}

		public static bool MeadProductName(string prefab)
		{
			if (!string.IsNullOrEmpty(prefab))
			{
				if (!prefab.Equals("BarleyWine", StringComparison.OrdinalIgnoreCase))
				{
					if (prefab.StartsWith("Mead", StringComparison.OrdinalIgnoreCase))
					{
						return !prefab.StartsWith("MeadBase", StringComparison.OrdinalIgnoreCase);
					}
					return false;
				}
				return true;
			}
			return false;
		}

		public static bool MeadProduct(bool consumable, bool food, bool fermenter, string prefab)
		{
			if (!fermenter)
			{
				if (consumable && !food)
				{
					return MeadProductName(prefab);
				}
				return false;
			}
			return true;
		}

		public static bool CoinWellAllows(string store, bool coinHome, string prefab)
		{
			if (!(store != "treasure") && coinHome)
			{
				return prefab == "Coins";
			}
			return true;
		}

		public static bool ValuableName(string prefab)
		{
			if (prefab != null)
			{
				if (!valuables.Contains(prefab) && !prefab.StartsWith("AncientGemstone", StringComparison.Ordinal))
				{
					return prefab.StartsWith("Pot_Shard", StringComparison.Ordinal);
				}
				return true;
			}
			return false;
		}

		public static bool JewelName(string prefab)
		{
			if (ValuableName(prefab))
			{
				return !CoinOrGoldName(prefab);
			}
			return false;
		}

		public static bool CoinOrGoldName(string prefab)
		{
			if (!string.IsNullOrEmpty(prefab) && (prefab.IndexOf("coin", StringComparison.OrdinalIgnoreCase) >= 0 || prefab.IndexOf("gold", StringComparison.OrdinalIgnoreCase) >= 0) && !notTreasure.Any((string w) => prefab.IndexOf(w, StringComparison.OrdinalIgnoreCase) >= 0) && !prefab.EndsWith("Key", StringComparison.OrdinalIgnoreCase))
			{
				return !prefab.EndsWith("Ore", StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		public static int UnrepresentedKinds(IReadOnlyList<string> types, IReadOnlyList<int> places, int processed)
		{
			HashSet<string> hashSet = new HashSet<string>(types, StringComparer.Ordinal);
			for (int i = 0; i < Math.Min(types.Count, Math.Min(places.Count, processed)); i++)
			{
				if (places[i] >= 0)
				{
					hashSet.Remove(types[i]);
				}
			}
			return hashSet.Count;
		}

		public static bool CanHangMeat(string prefab)
		{
			if (string.IsNullOrEmpty(prefab))
			{
				return false;
			}
			if (prefab.IndexOf("egg", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return false;
			}
			string text = (prefab.StartsWith("$item_", StringComparison.OrdinalIgnoreCase) ? prefab.Substring(6) : prefab);
			return !wetParts.Contains(text.Replace("_", ""));
		}
	}
	internal sealed class Filter
	{
		private readonly Kind kind;

		private readonly HashSet<string> prefabs = new HashSet<string>(StringComparer.Ordinal);

		private readonly HashSet<string> shared = new HashSet<string>(StringComparer.Ordinal);

		private readonly List<Regex> patterns = new List<Regex>();

		private string resolvedFrom;

		private ObjectDB resolvedFor;

		private float nextMessage;

		private static readonly HashSet<string> oneKindPerBay = new HashSet<string>(StringComparer.Ordinal) { "bunker", "bunkeropen", "orebin", "jeweller" };

		private string lastMessage;

		private static readonly (string[] Slugs, string Short, string Sentence)[] FamilyTable = new(string[], string, string)[9]
		{
			(new string[2] { "trophyhall13x5", "trophyhall10x7" }, "all Trophy Halls", "the tall and wide Trophy Halls"),
			(new string[2] { "apothecary", "apothecarytall" }, "all Apothecary Racks", "the short and tall Apothecary Racks"),
			(new string[2] { "coopersrow", "coopersrowstack" }, "all Cooper's Rows", "the single and stacking Cooper's Rows"),
			(new string[2] { "bunker", "bunkeropen" }, "all Coal & Stone Bunkers", "the closed and open Coal & Stone Bunkers"),
			(new string[4] { "metalrack", "metalrackhalf", "metalracklow", "metalracklowhalf" }, "all Metal Racks", "every Metal Rack, tall or low, wide or half"),
			(new string[2] { "weaponrack", "weaponrackb" }, "both Weapon Racks", "both Weapon Racks"),
			(new string[2] { "armorstand", "armorstandopen" }, "both Armor Stands", "the Armor Stand, with or without its back rail"),
			(new string[3] { "armoryrail", "armoryrail3", "armoryisland" }, "all Armory Rails", "the long and short Armory Rails and the Armory Island"),
			(new string[5] { "arsenalb", "arsenalkit_spine", "arsenalkit_shield", "arsenalkit_long", "arsenalkit_bow" }, "all Arsenal racks", "the Arsenal and its racks")
		};

		public string Label => kind.FilterLabel;

		public bool KindPerBay
		{
			get
			{
				if (kind.Slug != null)
				{
					return oneKindPerBay.Contains(kind.Slug);
				}
				return false;
			}
		}

		private int Bays
		{
			get
			{
				if (!KindPerBay)
				{
					return 0;
				}
				return StoreModel.BaysOf(kind.Slug, kind.Piece);
			}
		}

		public Filter(Kind kind)
		{
			this.kind = kind;
		}

		private static string KindOf(ItemData it)
		{
			if (!Object.op_Implicit((Object)(object)it.m_dropPrefab))
			{
				return it.m_shared.m_name;
			}
			return ((Object)it.m_dropPrefab).name;
		}

		public (int used, int bays) BayUse(Inventory inv)
		{
			return BayUse(inv, null, null);
		}

		private (int used, int bays) BayUse(Inventory inv, ItemData gone, string added)
		{
			int bays = Bays;
			if (bays <= 0 || inv == null)
			{
				return (used: 0, bays: 0);
			}
			float perBay = (float)(inv.GetWidth() * inv.GetHeight()) / (float)bays;
			IEnumerable<string> source = (from i in inv.GetAllItems()
				where i?.m_shared != null && i != gone
				select i).Select(KindOf);
			if (added != null)
			{
				source = source.Append(added);
			}
			return (used: (from n in source
				group n by n).Sum((IGrouping<string, string> g) => Mathf.Max(1, Mathf.CeilToInt((float)g.Count() / perBay))), bays: bays);
		}

		private static bool NewStack(Inventory inv, ItemData item, int amount, Vector2i? at, out ItemData gone)
		{
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: 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_009e: Unknown result type (might be due to invalid IL or missing references)
			gone = null;
			amount = ((amount > 0) ? Mathf.Min(amount, item.m_stack) : item.m_stack);
			if (inv.ContainsItem(item) && amount >= item.m_stack)
			{
				return false;
			}
			if (at.HasValue)
			{
				Vector2i valueOrDefault = at.GetValueOrDefault();
				if (valueOrDefault.x < 0 || valueOrDefault.y < 0 || valueOrDefault.x >= inv.GetWidth() || valueOrDefault.y >= inv.GetHeight())
				{
					return false;
				}
				ItemData itemAt = inv.GetItemAt(valueOrDefault.x, valueOrDefault.y);
				if (itemAt == null || itemAt == item)
				{
					return itemAt == null;
				}
				if ((itemAt.m_shared.m_name != item.m_shared.m_name || (item.m_shared.m_maxQuality > 1 && itemAt.m_quality != item.m_quality) || itemAt.m_shared.m_maxStackSize == 1) && item.m_stack == amount)
				{
					gone = itemAt;
					return true;
				}
				return false;
			}
			if (item.m_shared.m_maxStackSize <= 1)
			{
				return true;
			}
			return (from i in inv.GetAllItems()
				where i != null && i != item && i.m_shared.m_name == item.m_shared.m_name && i.m_quality == item.m_quality && i.m_worldLevel == item.m_worldLevel
				select i).Sum((ItemData i) => Mathf.Max(0, i.m_shared.m_maxStackSize - i.m_stack)) < amount;
		}

		public bool AllowsIn(Inventory inv, ItemData item, out string why, int amount = -1, Vector2i? at = null)
		{
			why = null;
			if (!Allows(item))
			{
				return false;
			}
			if (Mannequins.Is(kind)