Decompiled source of AutoChestSort v1.0.0

AutoChestSort.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using AutoChestSort.Chests;
using AutoChestSort.Commands;
using AutoChestSort.Config;
using AutoChestSort.Core;
using AutoChestSort.Core.Config;
using AutoChestSort.Core.Groups;
using AutoChestSort.Core.Inventory;
using AutoChestSort.Core.Localization;
using AutoChestSort.Core.Logging;
using AutoChestSort.Core.Network;
using AutoChestSort.Core.Sorting;
using AutoChestSort.Core.Sorting.Distribution;
using AutoChestSort.Core.Ui;
using AutoChestSort.Groups;
using AutoChestSort.Localization;
using AutoChestSort.Network;
using AutoChestSort.Sorting;
using AutoChestSort.Ui;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("Nexxy")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 Nexxy")]
[assembly: AssemblyDescription("Valheim chest auto-sorting mod (BepInEx).")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+1f8132cb0513483b73b1f973e223d866478e81b9")]
[assembly: AssemblyProduct("AutoChestSort")]
[assembly: AssemblyTitle("AutoChestSort")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace AutoChestSort
{
	internal static class AcsLog
	{
		private static readonly LogChangeGate Changes = new LogChangeGate();

		private static ManualLogSource _log;

		private static bool _configKnown;

		internal static bool DebugEnabled { get; private set; }

		internal static string DebugSource
		{
			get
			{
				if (!_configKnown)
				{
					return "default";
				}
				return "config";
			}
		}

		internal static void Install(ManualLogSource log)
		{
			_log = log;
		}

		internal static bool ApplyConfigDebug(bool enabled)
		{
			_configKnown = true;
			if (DebugEnabled != enabled)
			{
				if (!enabled)
				{
					Info("debug logging disabled (config)");
				}
				DebugEnabled = enabled;
				if (enabled)
				{
					Info("debug logging enabled (config)");
				}
			}
			return DebugEnabled;
		}

		internal static void Info(string message)
		{
			ManualLogSource log = _log;
			if (log != null)
			{
				log.LogInfo((object)message);
			}
		}

		internal static bool InfoIfChanged(string key, string state, string message)
		{
			if (!Changes.HasChanged(key, state))
			{
				Debug(message);
				return false;
			}
			Info(message);
			return true;
		}

		internal static bool WarningIfChanged(string key, string state, string message)
		{
			if (!Changes.HasChanged(key, state))
			{
				Debug(message);
				return false;
			}
			Warning(message);
			return true;
		}

		internal static void ForgetChangeStates()
		{
			Changes.Clear();
		}

		internal static void Warning(string message)
		{
			ManualLogSource log = _log;
			if (log != null)
			{
				log.LogWarning((object)message);
			}
		}

		internal static void Error(string message)
		{
			ManualLogSource log = _log;
			if (log != null)
			{
				log.LogError((object)message);
			}
		}

		internal static void Error(string message, Exception error)
		{
			ManualLogSource log = _log;
			if (log != null)
			{
				log.LogError((object)(message + ": " + error));
			}
		}

		internal static void Debug(string message)
		{
			if (DebugEnabled)
			{
				ManualLogSource log = _log;
				if (log != null)
				{
					log.LogInfo((object)message);
				}
			}
		}
	}
	[BepInPlugin("nexxy.valheim.autochestsort", "AutoChestSort", "1.0.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public class AutoChestSortPlugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		private void Awake()
		{
			//IL_0112: Unknown result type (might be due to invalid IL or missing references)
			//IL_012d: Unknown result type (might be due to invalid IL or missing references)
			AcsLog.Install(((BaseUnityPlugin)this).Logger);
			AcsConfigService.Install(((BaseUnityPlugin)this).Config);
			AcsChestCatalog.Install();
			AcsLog.Info("Hello from ACS v1.0.0 on Valheim " + ReadGameVersion());
			AcsLog.Debug("Core reserved chest names: " + string.Join(", ", ReservedNames.All));
			AcsLocalization.Register(((BaseUnityPlugin)this).Info.Metadata, ((BaseUnityPlugin)this).Logger);
			string text = "ACS localization: registered=" + AcsLocalization.IsRegistered + ", tokens=" + AcsTokens.Count + ", languages=" + string.Join("/", ToArray(AcsTokens.Languages)) + ", batchMode=" + Application.isBatchMode;
			if (AcsLocalization.IsRegistered)
			{
				AcsLog.Debug(text);
			}
			else
			{
				AcsLog.Warning(text + " - texts fall back to their raw tokens");
			}
			bool allowVanillaClients = AcsConfigService.Current.AllowVanillaClients;
			AcsLog.Info("ACS network gate: " + ((object)(CompatibilityLevel)5/*cast due to .constrained prefix*/).ToString() + " / " + ((object)(VersionStrictness)3/*cast due to .constrained prefix*/).ToString() + ", plugin version 1.0.0, allowVanillaClients=" + (allowVanillaClients ? "true" : "false") + " (a client with ACS must match this version; a client without ACS " + (allowVanillaClients ? "may join" : "is turned away at the handshake") + ")");
			AcsCommands.Install();
			_harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "nexxy.valheim.autochestsort");
			ContainerOpenSignal.Install(_harmony);
			VanillaClientGate.Install(_harmony);
			AcsLocalization.CatchUpIfAlreadyInitialized();
		}

		private static string[] ToArray(IReadOnlyList<string> values)
		{
			string[] array = new string[values.Count];
			for (int i = 0; i < values.Count; i++)
			{
				array[i] = values[i];
			}
			return array;
		}

		private void OnDestroy()
		{
			AcsConfigService.Shutdown();
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
				_harmony = null;
			}
		}

		private static string ReadGameVersion()
		{
			try
			{
				return GetVersionStringDirect();
			}
			catch (Exception ex)
			{
				try
				{
					return GetVersionStringReflected();
				}
				catch (Exception ex2)
				{
					return "unknown (" + ex.GetType().Name + " / " + ex2.GetType().Name + ")";
				}
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static string GetVersionStringDirect()
		{
			return Version.GetVersionString(false);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static string GetVersionStringReflected()
		{
			Type type = AccessTools.TypeByName("Version");
			if (type == null)
			{
				return "unknown (type Version not found)";
			}
			MethodInfo method = type.GetMethod("GetVersionString", BindingFlags.Static | BindingFlags.Public);
			if (method != null)
			{
				ParameterInfo[] parameters = method.GetParameters();
				object[] array = new object[parameters.Length];
				for (int i = 0; i < parameters.Length; i++)
				{
					array[i] = parameters[i].DefaultValue;
				}
				return Convert.ToString(method.Invoke(null, array));
			}
			PropertyInfo property = type.GetProperty("CurrentVersion", BindingFlags.Static | BindingFlags.Public);
			if (property != null)
			{
				return Convert.ToString(property.GetValue(null, null));
			}
			return "unknown (no version API on type Version)";
		}
	}
	public static class PluginInfo
	{
		public const string Guid = "nexxy.valheim.autochestsort";

		public const string Name = "AutoChestSort";

		public const string Version = "1.0.0";
	}
	internal static class AcsVersion
	{
		public const string Value = "1.0.0";
	}
}
namespace AutoChestSort.Ui
{
	internal sealed class AcsDialog
	{
		private const float Width = 520f;

		private const float Height = 300f;

		private GameObject _root;

		private Text _title;

		private Text _body;

		private Text _warning;

		private Button _yes;

		private Button _no;

		private Button _cancel;

		private Text _yesLabel;

		private Text _noLabel;

		private Text _cancelLabel;

		private Action _onYes;

		private Action _onNo;

		private Action _onCancel;

		internal bool IsOpen
		{
			get
			{
				if ((Object)(object)_root != (Object)null)
				{
					return _root.activeSelf;
				}
				return false;
			}
		}

		internal bool Show(string title, string body, string warning, string yes, string no, string cancel, Action onYes, Action onNo, Action onCancel)
		{
			if (!EnsureBuilt())
			{
				return false;
			}
			_onYes = onYes;
			_onNo = onNo;
			_onCancel = onCancel;
			_title.text = title ?? string.Empty;
			_body.text = body ?? string.Empty;
			bool flag = !string.IsNullOrEmpty(warning);
			((Component)_warning).gameObject.SetActive(flag);
			_warning.text = (flag ? warning : string.Empty);
			SetButton(_yes, _yesLabel, yes);
			SetButton(_no, _noLabel, no);
			SetButton(_cancel, _cancelLabel, cancel);
			_root.SetActive(true);
			_root.transform.SetAsLastSibling();
			return true;
		}

		internal void Hide()
		{
			_onYes = null;
			_onNo = null;
			_onCancel = null;
			if ((Object)(object)_root != (Object)null)
			{
				_root.SetActive(false);
			}
		}

		internal void Destroy()
		{
			Hide();
			if ((Object)(object)_root != (Object)null)
			{
				Object.Destroy((Object)(object)_root);
				_root = null;
			}
		}

		private static void SetButton(Button button, Text label, string caption)
		{
			bool flag = !string.IsNullOrEmpty(caption);
			((Component)button).gameObject.SetActive(flag);
			if (flag)
			{
				label.text = caption;
			}
		}

		private bool EnsureBuilt()
		{
			if ((Object)(object)_root != (Object)null && (Object)(object)GUIManager.CustomGUIFront != (Object)null && (Object)(object)_root.transform.parent == (Object)(object)GUIManager.CustomGUIFront.transform)
			{
				return true;
			}
			try
			{
				if ((Object)(object)_root != (Object)null)
				{
					Object.Destroy((Object)(object)_root);
					_root = null;
				}
				GUIManager instance = GUIManager.Instance;
				GameObject customGUIFront = GUIManager.CustomGUIFront;
				if (instance == null || (Object)(object)customGUIFront == (Object)null || (Object)(object)instance.AveriaSerifBold == (Object)null)
				{
					return false;
				}
				Build(instance, customGUIFront);
				return true;
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: building the ACS dialog failed", error);
				_root = null;
				return false;
			}
		}

		private void Build(GUIManager manager, GameObject parent)
		{
			//IL_0012: 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_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: 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)
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: 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_011a: Expected O, but got Unknown
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_014d: Expected O, but got Unknown
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			//IL_0176: Unknown result type (might be due to invalid IL or missing references)
			//IL_0180: Expected O, but got Unknown
			_root = manager.CreateWoodpanel(parent.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 520f, 300f, false);
			((Object)_root).name = "ACS_Dialog";
			_title = MakeText(manager, "ACS_DialogTitle", 18, manager.ValheimOrange, new Vector2(0f, 122f), 480f, 26f, (TextAnchor)4);
			_body = MakeText(manager, "ACS_DialogBody", 16, manager.ValheimBeige, new Vector2(0f, 52f), 480f, 84f, (TextAnchor)4);
			_warning = MakeText(manager, "ACS_DialogWarning", 14, manager.ValheimYellow, new Vector2(0f, -48f), 480f, 68f, (TextAnchor)4);
			_yes = MakeButton(manager, "ACS_DialogYes", new Vector2(-150f, -120f), out _yesLabel, (UnityAction)delegate
			{
				Fire(_onYes);
			});
			_no = MakeButton(manager, "ACS_DialogNo", new Vector2(0f, -120f), out _noLabel, (UnityAction)delegate
			{
				Fire(_onNo);
			});
			_cancel = MakeButton(manager, "ACS_DialogCancel", new Vector2(150f, -120f), out _cancelLabel, (UnityAction)delegate
			{
				Fire(_onCancel);
			});
			_root.SetActive(false);
		}

		private Text MakeText(GUIManager manager, string name, int size, Color color, Vector2 position, float width, float height, TextAnchor anchor)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			GameObject obj = manager.CreateText(string.Empty, _root.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), position, manager.AveriaSerifBold, size, color, true, Color.black, width, height, false);
			((Object)obj).name = name;
			Text component = obj.GetComponent<Text>();
			component.alignment = anchor;
			component.horizontalOverflow = (HorizontalWrapMode)0;
			component.verticalOverflow = (VerticalWrapMode)1;
			return component;
		}

		private Button MakeButton(GUIManager manager, string name, Vector2 position, out Text label, UnityAction action)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = manager.CreateButton(string.Empty, _root.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), position, 130f, 34f);
			((Object)val).name = name;
			label = val.GetComponentInChildren<Text>();
			Button component = val.GetComponent<Button>();
			((UnityEvent)component.onClick).AddListener(action);
			return component;
		}

		private void Fire(Action action)
		{
			try
			{
				action?.Invoke();
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: a dialog button failed", error);
				Hide();
				AcsUiText.MessageToken("$acs_err_server");
			}
		}
	}
	internal static class AcsUiText
	{
		internal static string Status(GroupResponse response)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			if (response == null)
			{
				return string.Empty;
			}
			string token = AcsStatusTokens.TokenForOrFallback(response.Status);
			return AcsStatusTokens.PlaceholderCount(response.Status) switch
			{
				2 => AcsLocalization.Format(token, Display(response), response.AffectedChests), 
				1 => AcsLocalization.Format(token, Display(response)), 
				_ => AcsLocalization.Get(token), 
			};
		}

		internal static string Learned(GroupResponse response)
		{
			string token = ChestPanelTokens.LearnedToken(response.SkippedItems);
			if (response.SkippedItems <= 0)
			{
				return AcsLocalization.Format(token, Display(response), response.Items.Count);
			}
			return AcsLocalization.Format(token, Display(response), response.Items.Count, response.SkippedItems);
		}

		internal static string Display(GroupResponse response)
		{
			if (response == null)
			{
				return string.Empty;
			}
			if (response.DisplayName.Length <= 0)
			{
				return response.GroupName;
			}
			return response.DisplayName;
		}

		internal static string JoinItems(IReadOnlyList<string> values)
		{
			if (values == null || values.Count == 0)
			{
				return AcsLocalization.Get("$acs_ui_info_empty");
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < values.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(", ");
				}
				stringBuilder.Append(values[i]);
			}
			return stringBuilder.ToString();
		}

		internal static void Message(string text)
		{
			if (!string.IsNullOrEmpty(text))
			{
				MessageHud instance = MessageHud.instance;
				if ((Object)(object)instance == (Object)null)
				{
					AcsLog.Debug("ui: no MessageHud for \"" + text + "\"");
				}
				else
				{
					instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
				}
			}
		}

		internal static void MessageToken(string token)
		{
			if (!string.IsNullOrEmpty(token))
			{
				Message(AcsLocalization.Get(token));
			}
		}
	}
	internal sealed class ChestPanelWidgets
	{
		internal const float PanelWidth = 430f;

		internal const float CollapsedHeight = 92f;

		private const float NameWidth = 250f;

		private const float RowHeight = 30f;

		private const float ButtonSize = 36f;

		private const float ButtonPitch = 42f;

		private const float Margin = 8f;

		private const float LineHeight = 18f;

		private const float InfoTop = 42f;

		private const float StatusTop = 60f;

		private const float ItemsTop = 80f;

		private const float ItemsMinHeight = 20f;

		private const float ItemsMaxHeight = 200f;

		private static Vector2 _offset = new Vector2(0f, -6f);

		private float _itemsHeight = 20f;

		internal GameObject Root { get; private set; }

		internal InputField NameField { get; private set; }

		internal Button ResetButton { get; private set; }

		internal Button InfoButton { get; private set; }

		internal Button LearnButton { get; private set; }

		internal Button DeleteButton { get; private set; }

		internal Text InfoLine { get; private set; }

		internal Text StatusLine { get; private set; }

		internal GameObject ItemsPanel { get; private set; }

		internal Text ItemsText { get; private set; }

		internal UITooltip LearnTooltip { get; private set; }

		internal UITooltip DeleteTooltip { get; private set; }

		internal UITooltip ResetTooltip { get; private set; }

		internal UITooltip InfoTooltip { get; private set; }

		internal static Vector2 Offset => _offset;

		internal bool AreItemsVisible
		{
			get
			{
				if ((Object)(object)ItemsPanel != (Object)null)
				{
					return ItemsPanel.activeSelf;
				}
				return false;
			}
		}

		private ChestPanelWidgets()
		{
		}

		internal static ChestPanelWidgets TryBuild(RectTransform parent)
		{
			if ((Object)(object)parent == (Object)null)
			{
				return null;
			}
			try
			{
				GUIManager instance = GUIManager.Instance;
				if (instance == null || (Object)(object)instance.AveriaSerifBold == (Object)null)
				{
					return null;
				}
				ChestPanelWidgets chestPanelWidgets = new ChestPanelWidgets();
				chestPanelWidgets.Build(instance, parent);
				return chestPanelWidgets;
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: building the chest panel failed", error);
				return null;
			}
		}

		internal void MoveTo(Vector2 offset)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			_offset = offset;
			if ((Object)(object)Root != (Object)null)
			{
				Root.GetComponent<RectTransform>().anchoredPosition = offset;
			}
		}

		internal void SetItemsVisible(bool visible)
		{
			if (!((Object)(object)ItemsPanel == (Object)null) && !((Object)(object)Root == (Object)null))
			{
				ItemsPanel.SetActive(visible);
				Resize(visible);
			}
		}

		internal void SetItemsText(string text)
		{
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)ItemsText == (Object)null)
			{
				return;
			}
			ItemsText.text = text ?? string.Empty;
			float num = 0f;
			try
			{
				num = ItemsText.preferredHeight;
			}
			catch (Exception ex)
			{
				AcsLog.Debug("ui: measuring the item list failed (" + ex.GetType().Name + ")");
			}
			float num2 = Mathf.Clamp(Mathf.Max(num, Estimate(ItemsText.text)), 20f, 200f);
			if (!(Mathf.Abs(num2 - _itemsHeight) < 1f))
			{
				_itemsHeight = num2;
				if ((Object)(object)ItemsPanel != (Object)null)
				{
					ItemsPanel.GetComponent<RectTransform>().sizeDelta = new Vector2(414f, _itemsHeight);
				}
				Resize(AreItemsVisible);
			}
		}

		private static float Estimate(string text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return 20f;
			}
			return (float)(1 + text.Length / 58) * 16f + 4f;
		}

		private void Resize(bool itemsVisible)
		{
			if (!((Object)(object)Root == (Object)null))
			{
				float num = (itemsVisible ? (80f + _itemsHeight + 8f) : 92f);
				Root.GetComponent<RectTransform>().SetSizeWithCurrentAnchors((Axis)1, num);
			}
		}

		internal void Destroy()
		{
			if ((Object)(object)Root != (Object)null)
			{
				Object.Destroy((Object)(object)Root);
				Root = null;
			}
		}

		private void Build(GUIManager manager, RectTransform parent)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//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_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0218: Unknown result type (might be due to invalid IL or missing references)
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_024a: Expected O, but got Unknown
			//IL_027c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0291: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0304: Unknown result type (might be due to invalid IL or missing references)
			//IL_0309: Unknown result type (might be due to invalid IL or missing references)
			//IL_0317: Unknown result type (might be due to invalid IL or missing references)
			//IL_031d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0383: Unknown result type (might be due to invalid IL or missing references)
			//IL_0398: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_03b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c2: Unknown result type (might be due to invalid IL or missing references)
			Root = manager.CreateWoodpanel((Transform)(object)parent, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), _offset, 430f, 92f, false);
			((Object)Root).name = "ACS_ChestPanel";
			RectTransform component = Root.GetComponent<RectTransform>();
			component.pivot = new Vector2(0.5f, 1f);
			component.anchoredPosition = _offset;
			float num = -215f;
			float num2 = -23f;
			GameObject val = manager.CreateInputField(Root.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(num + 8f + 125f, num2), (ContentType)0, AcsLocalization.Get("$acs_ui_name_placeholder"), 16, 250f, 30f);
			((Object)val).name = "ACS_Name";
			NameField = val.GetComponent<InputField>();
			NameField.characterLimit = 32;
			NameField.lineType = (LineType)0;
			AddTooltip(val, "$acs_ui_tooltip_name");
			float num3 = num + 8f + 250f + 42f - 18f;
			ResetButton = MakeButton(manager, "Ø", "ACS_Reset", num3, num2, "$acs_ui_tooltip_reset", out var tooltip);
			ResetTooltip = tooltip;
			num3 += 42f;
			InfoButton = MakeButton(manager, "i", "ACS_Info", num3, num2, "$acs_ui_tooltip_info", out var tooltip2);
			InfoTooltip = tooltip2;
			num3 += 42f;
			LearnButton = MakeButton(manager, "+", "ACS_Learn", num3, num2, "$acs_ui_tooltip_learn", out var tooltip3);
			LearnTooltip = tooltip3;
			num3 += 42f;
			DeleteButton = MakeButton(manager, "−", "ACS_Delete", num3, num2, "$acs_ui_tooltip_delete", out var tooltip4);
			DeleteTooltip = tooltip4;
			InfoLine = MakeLine(manager, "ACS_InfoLine", 42f, 14, manager.ValheimBeige);
			StatusLine = MakeLine(manager, "ACS_StatusLine", 60f, 13, manager.ValheimYellow);
			ItemsPanel = new GameObject("ACS_Items", new Type[1] { typeof(RectTransform) });
			ItemsPanel.transform.SetParent(Root.transform, false);
			RectTransform component2 = ItemsPanel.GetComponent<RectTransform>();
			component2.anchorMin = new Vector2(0.5f, 1f);
			component2.anchorMax = new Vector2(0.5f, 1f);
			component2.pivot = new Vector2(0.5f, 1f);
			component2.anchoredPosition = new Vector2(0f, -80f);
			component2.sizeDelta = new Vector2(414f, _itemsHeight);
			GameObject val2 = manager.CreateText(string.Empty, ItemsPanel.transform, new Vector2(0f, 0f), new Vector2(1f, 1f), Vector2.zero, manager.AveriaSerifBold, 13, manager.ValheimOrange, true, Color.black, 0f, 0f, false);
			((Object)val2).name = "ACS_ItemsText";
			ItemsText = val2.GetComponent<Text>();
			ItemsText.alignment = (TextAnchor)0;
			ItemsText.horizontalOverflow = (HorizontalWrapMode)0;
			ItemsText.verticalOverflow = (VerticalWrapMode)1;
			RectTransform component3 = val2.GetComponent<RectTransform>();
			component3.anchorMin = new Vector2(0f, 0f);
			component3.anchorMax = new Vector2(1f, 1f);
			component3.pivot = new Vector2(0.5f, 0.5f);
			component3.offsetMin = Vector2.zero;
			component3.offsetMax = Vector2.zero;
			ItemsPanel.SetActive(false);
		}

		private Text MakeLine(GUIManager manager, string name, float top, int size, Color color)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			GameObject obj = manager.CreateText(string.Empty, Root.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, 0f - (top + 9f)), manager.AveriaSerifBold, size, color, true, Color.black, 414f, 18f, false);
			((Object)obj).name = name;
			RectTransform component = obj.GetComponent<RectTransform>();
			component.anchorMin = new Vector2(0.5f, 1f);
			component.anchorMax = new Vector2(0.5f, 1f);
			component.pivot = new Vector2(0.5f, 0.5f);
			component.sizeDelta = new Vector2(414f, 18f);
			component.anchoredPosition = new Vector2(0f, 0f - (top + 9f));
			Text component2 = obj.GetComponent<Text>();
			component2.alignment = (TextAnchor)3;
			component2.horizontalOverflow = (HorizontalWrapMode)1;
			component2.verticalOverflow = (VerticalWrapMode)1;
			return component2;
		}

		private Button MakeButton(GUIManager manager, string label, string name, float x, float y, string tooltipToken, out UITooltip tooltip)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = manager.CreateButton(label, Root.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(x, y), 36f, 30f);
			((Object)val).name = name;
			tooltip = AddTooltip(val, tooltipToken);
			return val.GetComponent<Button>();
		}

		internal static void SetTooltipText(UITooltip tooltip, string text)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)tooltip == (Object)null)
			{
				return;
			}
			string text2 = text ?? string.Empty;
			if (string.Equals(tooltip.m_text, text2, StringComparison.Ordinal))
			{
				return;
			}
			try
			{
				tooltip.Set(tooltip.m_topic, text2, (RectTransform)null, default(Vector2));
			}
			catch (Exception ex)
			{
				tooltip.m_text = text2;
				AcsLog.Debug("ui: refreshing a tooltip failed (" + ex.GetType().Name + ")");
			}
		}

		private static UITooltip AddTooltip(GameObject target, string token)
		{
			try
			{
				GameObject val = TooltipPrefab();
				if ((Object)(object)val == (Object)null)
				{
					return null;
				}
				UITooltip val2 = target.GetComponent<UITooltip>();
				if ((Object)(object)val2 == (Object)null)
				{
					val2 = target.AddComponent<UITooltip>();
				}
				val2.m_tooltipPrefab = val;
				val2.m_text = AcsLocalization.Get(token);
				return val2;
			}
			catch (Exception ex)
			{
				AcsLog.Debug("ui: no tooltip for " + ((Object)target).name + " (" + ex.GetType().Name + ")");
				return null;
			}
		}

		private static GameObject TooltipPrefab()
		{
			InventoryGui instance = InventoryGui.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return null;
			}
			UITooltip[] componentsInChildren = ((Component)instance).GetComponentsInChildren<UITooltip>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				if ((Object)(object)componentsInChildren[i] != (Object)null && (Object)(object)componentsInChildren[i].m_tooltipPrefab != (Object)null)
				{
					return componentsInChildren[i].m_tooltipPrefab;
				}
			}
			return null;
		}
	}
	internal sealed class ChestUiController : MonoBehaviour
	{
		private struct Sent
		{
			internal GroupOpcode Opcode { get; }

			internal ZDOID Chest { get; }

			internal Sent(GroupOpcode opcode, ZDOID chest)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0002: Unknown result type (might be due to invalid IL or missing references)
				//IL_0008: Unknown result type (might be due to invalid IL or missing references)
				//IL_0009: Unknown result type (might be due to invalid IL or missing references)
				Opcode = opcode;
				Chest = chest;
			}
		}

		internal const float PollSeconds = 2f;

		internal const float WriteTimeoutSeconds = 6f;

		internal const float StatusSeconds = 6f;

		private readonly AcsDialog _dialog = new AcsDialog();

		private readonly Dictionary<int, Sent> _mine = new Dictionary<int, Sent>();

		private ChestPanelWidgets _widgets;

		private bool _handlersWired;

		private Container _chest;

		private ZDOID _chestId;

		private ChestPanelFacts _facts;

		private ChestPanelState _state;

		private float _pollTimer;

		private int _pendingWrite;

		private float _pendingSince;

		private int _storeVersion;

		private int _lockRevision = -1;

		private GroupStatus _lastQueryStatus;

		private GroupResponse _preview;

		private ConflictQueue _queue;

		private LearnMode _commitMode;

		private bool _modeChosen;

		private string _status = string.Empty;

		private float _statusUntil;

		private bool _itemsDirty = true;

		private bool _nameFocused;

		private bool _known;

		private int _buildAttempts;

		private readonly List<string> _items = new List<string>();

		internal static ChestUiController Instance { get; private set; }

		internal ZDOID OpenChestId
		{
			get
			{
				//IL_0015: Unknown result type (might be due to invalid IL or missing references)
				//IL_000e: Unknown result type (might be due to invalid IL or missing references)
				if (!((Object)(object)_chest != (Object)null))
				{
					return ZDOID.None;
				}
				return _chestId;
			}
		}

		internal static void Install()
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			if (!((Object)(object)Instance != (Object)null))
			{
				if (GUIManager.IsHeadless())
				{
					AcsLog.Debug("ui: headless process, no chest panel");
					return;
				}
				GameObject val = new GameObject("ACS_ChestUi");
				Object.DontDestroyOnLoad((Object)val);
				Instance = val.AddComponent<ChestUiController>();
				AcsLog.Info("ui: chest panel installed (poll " + 2f + " s)");
			}
		}

		internal string Describe()
		{
			return "chest=" + (((Object)(object)_chest != (Object)null) ? ((object)Unsafe.As<ZDOID, ZDOID>(ref _chestId)/*cast due to .constrained prefix*/).ToString() : "none") + " name=\"" + _facts.DisplayName + "\" role=" + ((object)Unsafe.As<GroupRole, GroupRole>(ref _facts.Role)/*cast due to .constrained prefix*/).ToString() + " stacks=" + _facts.ChestStacks + " groupItems=" + _facts.GroupItems + " chests=" + _facts.ChestCount + " open(others)=" + _facts.OpenChests + " known=" + _facts.Known + " busy=" + (_pendingWrite != 0) + " dialog=" + _dialog.IsOpen + " commitMode=" + ((object)Unsafe.As<LearnMode, LearnMode>(ref _commitMode)/*cast due to .constrained prefix*/).ToString() + " modeChosen=" + _modeChosen + " storeVersion=" + _storeVersion + " lockRevision=" + _lockRevision + " widgets=" + (_widgets != null) + " " + InputBlockGuard.Describe();
		}

		internal void MoveTo(Vector2 offset)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			_widgets?.MoveTo(offset);
		}

		internal unsafe void OnPanelGone(string reason)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_chest == (Object)null)
			{
				InputBlockGuard.Release(reason);
				return;
			}
			ZDOID chestId = _chestId;
			AcsLog.Debug("ui: chest panel closed (" + reason + "), chest " + ((object)(*(ZDOID*)(&chestId))/*cast due to .constrained prefix*/).ToString());
			Teardown(reason);
		}

		private void OnDestroy()
		{
			try
			{
				Teardown("controller destroyed");
				_dialog.Destroy();
				_widgets?.Destroy();
				_widgets = null;
				ItemNames.Clear();
				GroupRpc.ResponseReceived -= OnResponse;
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: tearing the chest panel down failed", error);
			}
			finally
			{
				if ((Object)(object)Instance == (Object)(object)this)
				{
					Instance = null;
				}
			}
		}

		private void Awake()
		{
			GroupRpc.ResponseReceived += OnResponse;
		}

		private void Update()
		{
			try
			{
				Container val = CurrentContainer();
				if ((Object)(object)val != (Object)(object)_chest)
				{
					if ((Object)(object)_chest != (Object)null)
					{
						Teardown("another chest");
					}
					if ((Object)(object)val != (Object)null)
					{
						Setup(val);
					}
				}
				if ((Object)(object)_chest == (Object)null)
				{
					InputBlockGuard.Release("no chest open");
					return;
				}
				UpdateNameFocus();
				ExpirePendingWrite();
				Poll();
				RefreshState();
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: the chest panel update failed, closing it", error);
				try
				{
					Teardown("update failed");
				}
				catch (Exception error2)
				{
					AcsLog.Error("ui: emergency teardown failed", error2);
					InputBlockGuard.Release("emergency");
				}
			}
		}

		private static Container CurrentContainer()
		{
			InventoryGui instance = InventoryGui.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null || ((Character)Player.m_localPlayer).IsDead())
			{
				return null;
			}
			Container currentContainer = instance.m_currentContainer;
			if ((Object)(object)currentContainer == (Object)null || !currentContainer.IsOwner())
			{
				return null;
			}
			ZDO zdo = ChestRegistry.GetZdo(currentContainer);
			if (zdo == null || !AcsChestCatalog.Current.IsSupported(zdo.GetPrefab()))
			{
				return null;
			}
			return currentContainer;
		}

		private void Setup(Container container)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: 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_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			ZDO zdo = ChestRegistry.GetZdo(container);
			if (zdo != null && EnsureWidgets())
			{
				_chest = container;
				_chestId = zdo.m_uid;
				_facts = default(ChestPanelFacts);
				_facts.DisplayName = ChestName.GetDisplay(zdo);
				_facts.Role = GroupRoles.Of(_facts.DisplayName);
				_facts.ChestStacks = CountStacks(container);
				_known = false;
				_pendingWrite = 0;
				_storeVersion = 0;
				_lockRevision = -1;
				_lastQueryStatus = (GroupStatus)0;
				_preview = null;
				_queue = null;
				_modeChosen = false;
				_commitMode = (LearnMode)0;
				_items.Clear();
				_itemsDirty = true;
				_status = string.Empty;
				_statusUntil = 0f;
				_mine.Clear();
				SetNameFieldText(_facts.DisplayName);
				_widgets.Root.SetActive(true);
				_widgets.SetItemsVisible(AcsConfigService.Current.EnableDebug);
				_pollTimer = 0f;
				SendQuery();
				RefreshState();
				AcsLog.Debug("ui: chest panel opened for " + ChestName.Describe(zdo) + ", name \"" + _facts.DisplayName + "\", stacks " + _facts.ChestStacks);
			}
		}

		private void Teardown(string reason)
		{
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			InputBlockGuard.Release(reason);
			_dialog.Hide();
			_preview = null;
			_queue = null;
			_modeChosen = false;
			_status = string.Empty;
			_statusUntil = 0f;
			_mine.Clear();
			_pendingWrite = 0;
			_chest = null;
			_chestId = ZDOID.None;
			_known = false;
			if (_widgets != null)
			{
				if ((Object)(object)_widgets.NameField != (Object)null)
				{
					_widgets.NameField.DeactivateInputField();
				}
				_widgets.SetItemsVisible(visible: false);
				if ((Object)(object)_widgets.Root != (Object)null)
				{
					_widgets.Root.SetActive(false);
				}
			}
			_nameFocused = false;
		}

		private bool EnsureWidgets()
		{
			InventoryGui instance = InventoryGui.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)instance.m_container == (Object)null)
			{
				return false;
			}
			if (_widgets != null && (Object)(object)_widgets.Root != (Object)null && (Object)(object)_widgets.Root.transform.parent == (Object)(object)((Component)instance.m_container).transform)
			{
				return true;
			}
			_handlersWired = false;
			_widgets?.Destroy();
			_widgets = ChestPanelWidgets.TryBuild(instance.m_container);
			if (_widgets == null)
			{
				_buildAttempts++;
				if (_buildAttempts == 1 || _buildAttempts % 300 == 0)
				{
					AcsLog.Warning("ui: the chest panel could not be built yet (attempt " + _buildAttempts + "), retrying");
				}
				return false;
			}
			WireHandlers();
			AcsLog.Debug("ui: chest panel built under InventoryGui.m_container");
			return true;
		}

		private void WireHandlers()
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Expected O, but got Unknown
			//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Expected O, but got Unknown
			if (!_handlersWired && _widgets != null)
			{
				_handlersWired = true;
				((UnityEvent<string>)(object)_widgets.NameField.onEndEdit).AddListener((UnityAction<string>)OnNameSubmitted);
				((UnityEvent)_widgets.ResetButton.onClick).AddListener(new UnityAction(OnResetClicked));
				((UnityEvent)_widgets.InfoButton.onClick).AddListener(new UnityAction(OnInfoClicked));
				((UnityEvent)_widgets.LearnButton.onClick).AddListener(new UnityAction(OnLearnClicked));
				((UnityEvent)_widgets.DeleteButton.onClick).AddListener(new UnityAction(OnDeleteClicked));
			}
		}

		private void UpdateNameFocus()
		{
			bool flag = _widgets != null && (Object)(object)_widgets.NameField != (Object)null && _widgets.NameField.isFocused;
			if (flag != _nameFocused)
			{
				_nameFocused = flag;
				if (flag)
				{
					InputBlockGuard.Block();
				}
				else
				{
					InputBlockGuard.Release("name field lost focus");
				}
			}
		}

		private void ExpirePendingWrite()
		{
			if (_pendingWrite != 0 && !(Time.unscaledTime - _pendingSince < 6f))
			{
				AcsLog.Warning("ui: no answer to request #" + _pendingWrite + " within " + 6f + " s, the panel is usable again");
				_mine.Remove(_pendingWrite);
				_pendingWrite = 0;
				if (_dialog.IsOpen)
				{
					_dialog.Hide();
					_preview = null;
					_queue = null;
				}
				AnnounceToken("$acs_err_server");
			}
		}

		private void Poll()
		{
			_pollTimer -= Time.unscaledDeltaTime;
			if (!(_pollTimer > 0f))
			{
				_pollTimer = 2f;
				SendQuery();
			}
		}

		private void RefreshState()
		{
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
			if (_widgets == null || (Object)(object)_chest == (Object)null)
			{
				return;
			}
			ZDO zdo = ChestRegistry.GetZdo(_chest);
			if (zdo == null)
			{
				Teardown("chest zdo gone");
				return;
			}
			string display = ChestName.GetDisplay(zdo);
			if (!_nameFocused && !string.Equals(display, _facts.DisplayName, StringComparison.Ordinal))
			{
				_facts.DisplayName = display;
				_facts.Role = GroupRoles.Of(display);
				SetNameFieldText(display);
			}
			_facts.ChestStacks = CountStacks(_chest);
			_facts.CtrlHeld = ZInput.GetKey((KeyCode)306, false) || ZInput.GetKey((KeyCode)305, false);
			_facts.Known = _known;
			_facts.Busy = _pendingWrite != 0;
			_facts.DialogOpen = _dialog.IsOpen;
			_state = ChestPanelState.Evaluate(_facts);
			((Selectable)_widgets.NameField).interactable = _state.NameFieldEnabled;
			_widgets.NameField.readOnly = !_state.NameFieldEnabled;
			if (!_state.NameFieldEnabled && _widgets.NameField.isFocused)
			{
				_widgets.NameField.DeactivateInputField();
			}
			((Selectable)_widgets.ResetButton).interactable = _state.ResetEnabled;
			((Selectable)_widgets.InfoButton).interactable = _state.InfoEnabled;
			((Selectable)_widgets.LearnButton).interactable = _state.LearnEnabled;
			((Selectable)_widgets.DeleteButton).interactable = _state.DeleteEnabled;
			if (!_state.InfoEnabled && _widgets.AreItemsVisible)
			{
				_widgets.SetItemsVisible(visible: false);
			}
			ChestPanelWidgets.SetTooltipText(_widgets.LearnTooltip, AcsLocalization.Get((_state.LearnBlockedToken.Length > 0) ? _state.LearnBlockedToken : "$acs_ui_tooltip_learn"));
			ChestPanelWidgets.SetTooltipText(_widgets.DeleteTooltip, AcsLocalization.Get((_state.DeleteEnabled || _state.DeleteBlockedToken.Length == 0) ? "$acs_ui_tooltip_delete" : _state.DeleteBlockedToken));
			ChestPanelWidgets.SetTooltipText(_widgets.ResetTooltip, AcsLocalization.Get((!_state.ResetEnabled && _state.Locked) ? "$acs_msg_group_locked" : "$acs_ui_tooltip_reset"));
			ChestPanelWidgets.SetTooltipText(_widgets.InfoTooltip, AcsLocalization.Get((_state.InfoBlockedToken.Length > 0) ? _state.InfoBlockedToken : "$acs_ui_tooltip_info"));
			_widgets.InfoLine.text = BuildInfoLine();
			_widgets.StatusLine.text = CurrentStatus();
			if (_widgets.AreItemsVisible && _itemsDirty)
			{
				_widgets.SetItemsText(BuildItemsText());
				_itemsDirty = false;
			}
		}

		private string CurrentStatus()
		{
			if (_status.Length == 0)
			{
				return string.Empty;
			}
			if (Time.unscaledTime >= _statusUntil)
			{
				_status = string.Empty;
				return string.Empty;
			}
			return _status;
		}

		private void Announce(string text)
		{
			if (!string.IsNullOrEmpty(text))
			{
				AcsUiText.Message(text);
				_status = text;
				_statusUntil = Time.unscaledTime + 6f;
				if ((Object)(object)_widgets?.StatusLine != (Object)null)
				{
					_widgets.StatusLine.text = text;
				}
			}
		}

		private void AnnounceToken(string token)
		{
			if (!string.IsNullOrEmpty(token))
			{
				Announce(AcsLocalization.Get(token));
			}
		}

		private string BuildInfoLine()
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_010b: Unknown result type (might be due to invalid IL or missing references)
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(AcsLocalization.Get(ChestPanelTokens.RoleToken(_facts.Role)));
			if (((ChestPanelFacts)(ref _facts)).IsGroup)
			{
				stringBuilder.Append(": ").Append(_facts.DisplayName);
				stringBuilder.Append("  ").Append(AcsLocalization.Format("$acs_ui_info_chests", _facts.ChestCount));
			}
			if (!_known)
			{
				stringBuilder.Append("  ").Append(GroupStatusExtensions.IsOk(_lastQueryStatus) ? AcsLocalization.Get("$acs_ui_waiting") : AcsLocalization.Get(AcsStatusTokens.TokenForOrFallback(_lastQueryStatus)));
			}
			else if (_state != null && _state.Locked)
			{
				stringBuilder.Append("  ").Append(AcsLocalization.Get("$acs_msg_group_locked"));
			}
			if (AcsConfigService.Current.EnableDebug)
			{
				stringBuilder.Append("  [").Append(_chestId).Append(" v")
					.Append(_storeVersion)
					.Append(" lr")
					.Append(_lockRevision)
					.Append(" open")
					.Append(_facts.OpenChests)
					.Append("]");
			}
			return stringBuilder.ToString();
		}

		private string BuildItemsText()
		{
			return AcsLocalization.Get("$acs_ui_info_header") + ": " + AcsUiText.JoinItems(ItemNames.Display(_items));
		}

		private void SetItems(List<string> items)
		{
			if (items != null && !SameItems(items))
			{
				_items.Clear();
				_items.AddRange(items);
				_itemsDirty = true;
			}
		}

		private bool SameItems(List<string> items)
		{
			if (items.Count != _items.Count)
			{
				return false;
			}
			for (int i = 0; i < items.Count; i++)
			{
				if (!string.Equals(items[i], _items[i], StringComparison.Ordinal))
				{
					return false;
				}
			}
			return true;
		}

		private static int CountStacks(Container container)
		{
			try
			{
				Inventory val = ((container != null) ? container.GetInventory() : null);
				return (val != null) ? val.NrOfItems() : 0;
			}
			catch (Exception ex)
			{
				AcsLog.Debug("ui: reading the chest inventory failed (" + ex.GetType().Name + ")");
				return 0;
			}
		}

		private void SetNameFieldText(string text)
		{
			if (!((Object)(object)_widgets?.NameField == (Object)null))
			{
				_widgets.NameField.text = text ?? string.Empty;
			}
		}

		private void OnNameSubmitted(string typed)
		{
			try
			{
				if ((Object)(object)_chest == (Object)null || _state == null)
				{
					return;
				}
				string text = ChestNameNormalizer.ToRaw(typed);
				if (string.Equals(text, ChestNameNormalizer.ToRaw(_facts.DisplayName), StringComparison.Ordinal))
				{
					SetNameFieldText(_facts.DisplayName);
				}
				else if (!_state.NameFieldEnabled)
				{
					SetNameFieldText(_facts.DisplayName);
					if (_state.Locked)
					{
						AnnounceToken("$acs_msg_group_locked");
					}
				}
				else
				{
					Send((GroupOpcode)4, (LearnMode)0, text);
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: submitting the chest name failed", error);
			}
		}

		private void OnResetClicked()
		{
			try
			{
				if ((Object)(object)_chest == (Object)null || _state == null || !_state.ResetEnabled)
				{
					Explain((_state != null && _state.Locked) ? "$acs_msg_group_locked" : string.Empty);
				}
				else
				{
					Send((GroupOpcode)5, (LearnMode)0, string.Empty);
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: the reset button failed", error);
			}
		}

		private void OnInfoClicked()
		{
			try
			{
				if (_widgets != null && _state != null && _state.InfoEnabled)
				{
					_widgets.SetItemsVisible(!_widgets.AreItemsVisible);
					if (_widgets.AreItemsVisible)
					{
						_widgets.SetItemsText(BuildItemsText());
						_itemsDirty = false;
					}
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: the info button failed", error);
			}
		}

		private void OnLearnClicked()
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!((Object)(object)_chest == (Object)null) && _state != null)
				{
					if (!_state.LearnEnabled)
					{
						Explain(_state.LearnBlockedToken);
						return;
					}
					if (_state.LearnIsMiscFastpath)
					{
						Send((GroupOpcode)6, (LearnMode)0, string.Empty);
						return;
					}
					_preview = null;
					_queue = null;
					_modeChosen = false;
					_commitMode = (LearnMode)0;
					Send((GroupOpcode)1, (LearnMode)0, string.Empty);
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: the learn button failed", error);
			}
		}

		private void OnDeleteClicked()
		{
			try
			{
				if ((Object)(object)_chest == (Object)null || _state == null)
				{
					return;
				}
				if (!_state.DeleteEnabled)
				{
					Explain(_state.DeleteBlockedToken);
					return;
				}
				if (!_state.DeleteArmed)
				{
					AnnounceToken("$acs_msg_delete_needs_ctrl");
					return;
				}
				string displayName = _facts.DisplayName;
				if (!_dialog.Show(AcsLocalization.Format("$acs_dlg_delete_title", displayName), AcsLocalization.Format("$acs_dlg_delete_text", _facts.ChestCount), string.Empty, AcsLocalization.Get("$acs_dlg_button_yes"), string.Empty, AcsLocalization.Get("$acs_dlg_button_cancel"), delegate
				{
					_dialog.Hide();
					Send((GroupOpcode)3, (LearnMode)0, string.Empty);
				}, null, Cancelled))
				{
					AcsLog.Warning("ui: the delete confirmation could not be shown, nothing was deleted");
					AnnounceToken("$acs_err_server");
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: the delete button failed", error);
			}
		}

		private void Cancelled()
		{
			_dialog.Hide();
			_preview = null;
			_queue = null;
			_modeChosen = false;
			AnnounceToken("$acs_msg_cancelled");
		}

		private void Explain(string token)
		{
			AnnounceToken(token);
		}

		private void ShowModeStep()
		{
			if (_preview != null && _queue != null)
			{
				LearnDialogPlan plan = LearnDialogPlan.ForPreview(_preview.Removed.Count);
				string text = AcsUiText.Display(_preview);
				string body = AcsLocalization.Format("$acs_dlg_confirm_learn", text) + "\n" + AcsLocalization.Format("$acs_dlg_learn_new", AcsUiText.JoinItems(ItemNames.Display(_preview.Accepted)));
				string warning = (plan.ShowsRemovedWarning ? (RemovedWarning((LearnMode)0) + "\n" + AcsLocalization.Get("$acs_dlg_learn_merge_hint")) : string.Empty);
				if (!_dialog.Show(AcsLocalization.Format("$acs_dlg_learn_title", text), body, warning, AcsLocalization.Get(plan.PrimaryToken), plan.HasSecondary ? AcsLocalization.Get(plan.SecondaryToken) : string.Empty, AcsLocalization.Get(plan.CancelToken), delegate
				{
					//IL_000c: Unknown result type (might be due to invalid IL or missing references)
					ChooseMode(plan.PrimaryMode);
				}, plan.HasSecondary ? ((Action)delegate
				{
					//IL_000c: Unknown result type (might be due to invalid IL or missing references)
					ChooseMode(plan.SecondaryMode);
				}) : null, Cancelled))
				{
					AcsLog.Warning("ui: the learn dialog could not be shown, the learn run is dropped - nothing was changed");
					Cancelled();
				}
			}
		}

		private unsafe void ChooseMode(LearnMode mode)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			_commitMode = mode;
			_modeChosen = true;
			_dialog.Hide();
			string[] obj = new string[7]
			{
				"ui: learn mode ",
				((object)(*(LearnMode*)(&mode))/*cast due to .constrained prefix*/).ToString(),
				" chosen for \"",
				AcsUiText.Display(_preview),
				"\", ",
				null,
				null
			};
			ConflictQueue queue = _queue;
			obj[5] = ((queue != null) ? queue.Total : 0).ToString();
			obj[6] = " conflict(s) to answer";
			AcsLog.Debug(string.Concat(obj));
			ShowNextLearnStep();
		}

		private void ShowNextLearnStep()
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			if (_preview == null || _queue == null)
			{
				return;
			}
			if (_queue.IsCancelled)
			{
				Cancelled();
				return;
			}
			if (!_modeChosen)
			{
				ShowModeStep();
				return;
			}
			string text = AcsUiText.Display(_preview);
			string warning = RemovedWarning(_commitMode);
			LearnConflict current = _queue.Current;
			if (current != null)
			{
				string text2 = ((_queue.Total > 1) ? (AcsLocalization.Format("$acs_dlg_conflict_progress", _queue.Position, _queue.Total) + "\n") : string.Empty);
				if (!_dialog.Show(AcsLocalization.Format("$acs_dlg_learn_title", text), text2 + AcsLocalization.Format("$acs_dlg_conflict_text", ItemNames.Display(current.Item.Name), current.CurrentGroup), warning, AcsLocalization.Get("$acs_dlg_button_yes"), AcsLocalization.Get("$acs_dlg_button_no"), AcsLocalization.Get("$acs_dlg_button_cancel"), delegate
				{
					AnswerConflict(takeover: true);
				}, delegate
				{
					AnswerConflict(takeover: false);
				}, Cancelled))
				{
					AcsLog.Warning("ui: the conflict dialog could not be shown, the learn run is dropped - nothing was changed");
					Cancelled();
				}
			}
			else
			{
				SendCommit();
			}
		}

		private string RemovedWarning(LearnMode mode)
		{
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			if (_preview == null || _preview.Removed.Count == 0 || (int)mode != 0)
			{
				return string.Empty;
			}
			return AcsLocalization.Format("$acs_dlg_removed_header", AcsUiText.JoinItems(ItemNames.Display(_preview.Removed))) + "\n" + AcsLocalization.Get("$acs_dlg_removed_hint");
		}

		private void AnswerConflict(bool takeover)
		{
			if (_queue != null)
			{
				_queue.Answer(takeover);
				_dialog.Hide();
				ShowNextLearnStep();
			}
		}

		private void SendQuery()
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			if (!((Object)(object)_chest == (Object)null))
			{
				int num = GroupRpc.NextRequestId();
				_mine[num] = new Sent((GroupOpcode)7, _chestId);
				TrimPending();
				if (!GroupRpc.Send(new GroupRequest((GroupOpcode)7, num, ((ZDOID)(ref _chestId)).UserID, ((ZDOID)(ref _chestId)).ID, (LearnMode)0, (string)null, 0, (IEnumerable<string>)null, (IEnumerable<string>)null)))
				{
					_mine.Remove(num);
				}
			}
		}

		private void Send(GroupOpcode opcode, LearnMode mode, string text)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Expected O, but got Unknown
			if (!((Object)(object)_chest == (Object)null))
			{
				int num = GroupRpc.NextRequestId();
				_mine[num] = new Sent(opcode, _chestId);
				TrimPending();
				_pendingWrite = num;
				_pendingSince = Time.unscaledTime;
				if (!GroupRpc.Send(new GroupRequest(opcode, num, ((ZDOID)(ref _chestId)).UserID, ((ZDOID)(ref _chestId)).ID, mode, text, 0, (IEnumerable<string>)null, (IEnumerable<string>)null)))
				{
					_mine.Remove(num);
					_pendingWrite = 0;
					AnnounceToken("$acs_err_server");
				}
				else
				{
					RefreshState();
				}
			}
		}

		private void SendCommit()
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Expected O, but got Unknown
			if (!((Object)(object)_chest == (Object)null) && _preview != null && _queue != null && _modeChosen)
			{
				int num = GroupRpc.NextRequestId();
				_mine[num] = new Sent((GroupOpcode)2, _chestId);
				TrimPending();
				_pendingWrite = num;
				_pendingSince = Time.unscaledTime;
				if (!GroupRpc.Send(new GroupRequest((GroupOpcode)2, num, ((ZDOID)(ref _chestId)).UserID, ((ZDOID)(ref _chestId)).ID, _commitMode, (string)null, _preview.StoreVersion, (IEnumerable<string>)_queue.Takeover, (IEnumerable<string>)_queue.Skip)))
				{
					_mine.Remove(num);
					_pendingWrite = 0;
					AnnounceToken("$acs_err_server");
				}
				else
				{
					RefreshState();
				}
			}
		}

		private void TrimPending()
		{
			if (_mine.Count > 32)
			{
				_mine.Clear();
				_pendingWrite = 0;
			}
		}

		private void OnResponse(GroupResponse response)
		{
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ae: Invalid comparison between Unknown and I4
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Invalid comparison between Unknown and I4
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Invalid comparison between Unknown and I4
			try
			{
				if (response == null || !_mine.TryGetValue(response.RequestId, out var value))
				{
					return;
				}
				_mine.Remove(response.RequestId);
				if (_pendingWrite == response.RequestId)
				{
					_pendingWrite = 0;
				}
				if ((Object)(object)_chest == (Object)null || value.Chest != _chestId)
				{
					AcsLog.Debug("ui: dropped a late " + ((object)response.Opcode/*cast due to .constrained prefix*/).ToString() + " answer for " + ((object)value.Chest/*cast due to .constrained prefix*/).ToString());
					return;
				}
				GroupOpcode opcode = response.Opcode;
				if ((int)opcode != 1)
				{
					if ((int)opcode != 2)
					{
						if ((int)opcode == 7)
						{
							OnQueryAnswer(response);
						}
						else
						{
							OnWriteAnswer(response);
						}
					}
					else
					{
						OnCommitAnswer(response);
					}
				}
				else
				{
					OnPreviewAnswer(response);
				}
				RefreshState();
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: handling a group answer failed", error);
			}
		}

		private void OnQueryAnswer(GroupResponse response)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			_lastQueryStatus = response.Status;
			if (!response.IsOk)
			{
				_known = false;
				return;
			}
			_known = true;
			_facts.Role = response.Role;
			_facts.GroupItems = response.Items.Count;
			_facts.ChestCount = response.ChestCount;
			_facts.OpenChests = response.OpenChests;
			_storeVersion = response.StoreVersion;
			_lockRevision = response.LockRevision;
			SetItems(response.Items);
			if (response.DisplayName.Length > 0 && !_nameFocused && !string.Equals(response.DisplayName, _facts.DisplayName, StringComparison.Ordinal))
			{
				_facts.DisplayName = response.DisplayName;
				SetNameFieldText(response.DisplayName);
			}
		}

		private void OnPreviewAnswer(GroupResponse response)
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected O, but got Unknown
			if (!response.IsOk)
			{
				_preview = null;
				_queue = null;
				_modeChosen = false;
				Announce(AcsUiText.Status(response));
				return;
			}
			_preview = response;
			_queue = new ConflictQueue((IEnumerable<LearnConflict>)response.Conflicts);
			_storeVersion = response.StoreVersion;
			_modeChosen = false;
			if (response.SkippedItems > 0)
			{
				AcsLog.Debug("ui: the preview skipped " + response.SkippedItems + " stack(s) the server cannot resolve");
			}
			ShowModeStep();
		}

		private void OnCommitAnswer(GroupResponse response)
		{
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Invalid comparison between Unknown and I4
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Invalid comparison between Unknown and I4
			if (!response.IsOk)
			{
				if ((int)response.Status == 25 || (int)response.Status == 35)
				{
					_preview = null;
					_queue = null;
					_modeChosen = false;
					_dialog.Hide();
				}
				Announce(AcsUiText.Status(response));
			}
			else
			{
				_preview = null;
				_queue = null;
				_modeChosen = false;
				_dialog.Hide();
				_storeVersion = response.StoreVersion;
				SetItems(response.Items);
				_facts.GroupItems = response.Items.Count;
				Announce(AcsUiText.Learned(response));
				_pollTimer = 0f;
			}
		}

		private void OnWriteAnswer(GroupResponse response)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			Announce(AcsUiText.Status(response));
			if (response.IsOk)
			{
				_facts.DisplayName = response.DisplayName;
				_facts.Role = response.Role;
				SetNameFieldText(response.DisplayName);
				_storeVersion = response.StoreVersion;
			}
			else
			{
				ZDO zdo = ChestRegistry.GetZdo(_chest);
				if (zdo != null)
				{
					_facts.DisplayName = ChestName.GetDisplay(zdo);
					_facts.Role = GroupRoles.Of(_facts.DisplayName);
					SetNameFieldText(_facts.DisplayName);
				}
			}
			_known = false;
			_pollTimer = 2f;
			SendQuery();
		}
	}
	[HarmonyPatch(typeof(Container), "GetHoverText")]
	internal static class ContainerHoverPatch
	{
		[HarmonyPostfix]
		[HarmonyWrapSafe]
		private static void Postfix(Container __instance, ref string __result)
		{
			try
			{
				if (!AcsConfigService.Current.ShowChestNameOnHover || (Object)(object)__instance == (Object)null)
				{
					return;
				}
				ZDO zdo = ChestRegistry.GetZdo(__instance);
				if (zdo != null && AcsChestCatalog.Current.IsSupported(zdo.GetPrefab()))
				{
					string display = ChestName.GetDisplay(zdo);
					if (display.Length != 0)
					{
						string line = AcsLocalization.Format("$acs_ui_hover_name", display);
						__result = Insert(__result, line);
					}
				}
			}
			catch (Exception ex)
			{
				AcsLog.Debug("ui: the hover text of a chest could not be extended (" + ex.GetType().Name + ": " + ex.Message + ")");
			}
		}

		private static string Insert(string hover, string line)
		{
			if (string.IsNullOrEmpty(hover))
			{
				return line;
			}
			int num = hover.IndexOf('\n');
			if (num < 0)
			{
				return hover + "\n" + line;
			}
			return hover.Substring(0, num) + "\n" + line + hover.Substring(num);
		}
	}
	internal static class InputBlockGuard
	{
		internal static bool IsBlocking { get; private set; }

		internal static int Taken { get; private set; }

		internal static int Released { get; private set; }

		internal static void Block()
		{
			if (IsBlocking)
			{
				return;
			}
			try
			{
				GUIManager.BlockInput(true);
				IsBlocking = true;
				Taken++;
				AcsLog.Debug("ui: input blocked (name field focused), taken=" + Taken);
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: blocking the input failed", error);
			}
		}

		internal static void Release(string reason)
		{
			if (!IsBlocking)
			{
				return;
			}
			try
			{
				GUIManager.BlockInput(false);
			}
			catch (Exception error)
			{
				AcsLog.Error("ui: releasing the input failed (" + reason + ")", error);
			}
			finally
			{
				IsBlocking = false;
				Released++;
				AcsLog.Debug("ui: input released (" + reason + "), released=" + Released);
			}
		}

		internal static string Describe()
		{
			return "blocking=" + IsBlocking + " taken=" + Taken + " released=" + Released;
		}
	}
	[HarmonyPatch(typeof(InventoryGui))]
	internal static class InventoryGuiPatches
	{
		[HarmonyPostfix]
		[HarmonyWrapSafe]
		[HarmonyPatch("Hide")]
		private static void HidePostfix()
		{
			ChestUiController.Instance?.OnPanelGone("InventoryGui.Hide");
		}

		[HarmonyPostfix]
		[HarmonyWrapSafe]
		[HarmonyPatch("CloseContainer")]
		private static void CloseContainerPostfix()
		{
			ChestUiController.Instance?.OnPanelGone("InventoryGui.CloseContainer");
		}

		[HarmonyPostfix]
		[HarmonyWrapSafe]
		[HarmonyPatch("UpdateContainer")]
		private static void UpdateContainerPostfix(InventoryGui __instance)
		{
			if ((Object)(object)__instance.m_currentContainer == (Object)null || !__instance.m_currentContainer.IsOwner())
			{
				ChestUiController.Instance?.OnPanelGone("InventoryGui.UpdateContainer (no owned container)");
			}
		}
	}
	internal static class ItemNames
	{
		private static readonly Dictionary<string, string> Cache = new Dictionary<string, string>(StringComparer.Ordinal);

		internal static void Clear()
		{
			Cache.Clear();
		}

		internal static string Display(string prefabName)
		{
			if (string.IsNullOrEmpty(prefabName))
			{
				return string.Empty;
			}
			if (Cache.TryGetValue(prefabName, out var value))
			{
				return value;
			}
			string text = Resolve(prefabName);
			if (!string.Equals(text, prefabName, StringComparison.Ordinal))
			{
				Cache[prefabName] = text;
			}
			return text;
		}

		internal static List<string> Display(IReadOnlyList<string> prefabNames)
		{
			List<string> list = new List<string>(prefabNames?.Count ?? 0);
			if (prefabNames == null)
			{
				return list;
			}
			for (int i = 0; i < prefabNames.Count; i++)
			{
				list.Add(Display(prefabNames[i]));
			}
			return list;
		}

		private static string Resolve(string prefabName)
		{
			try
			{
				string text = SharedName(prefabName);
				if (string.IsNullOrEmpty(text))
				{
					return prefabName;
				}
				Localization instance = Localization.instance;
				if (instance == null)
				{
					return prefabName;
				}
				string text2 = instance.Localize(text);
				return string.IsNullOrWhiteSpace(text2) ? prefabName : text2.Trim();
			}
			catch (Exception ex)
			{
				AcsLog.Debug("ui: resolving the item name of \"" + prefabName + "\" failed (" + ex.GetType().Name + ")");
				return prefabName;
			}
		}

		private static string SharedName(string prefabName)
		{
			ObjectDB instance = ObjectDB.instance;
			GameObject val = default(GameObject);
			if ((Object)(object)instance != (Object)null && instance.TryGetItemPrefab(prefabName, ref val) && (Object)(object)val != (Object)null)
			{
				string text = TokenOf(val);
				if (text.Length > 0)
				{
					return text;
				}
			}
			ZNetScene instance2 = ZNetScene.instance;
			if ((Object)(object)instance2 != (Object)null)
			{
				GameObject prefab = instance2.GetPrefab(prefabName);
				if ((Object)(object)prefab != (Object)null)
				{
					return TokenOf(prefab);
				}
			}
			return string.Empty;
		}

		private static string TokenOf(GameObject prefab)
		{
			ItemDrop component = prefab.GetComponent<ItemDrop>();
			if ((Object)(object)component == (Object)null || component.m_itemData == null || component.m_itemData.m_shared == null)
			{
				return string.Empty;
			}
			return component.m_itemData.m_shared.m_name ?? string.Empty;
		}
	}
}
namespace AutoChestSort.Sorting
{
	internal static class ContainerOpenSignal
	{
		internal const string RequestOpenRpc = "RPC_RequestOpen";

		internal const string OpenResponseRpc = "RPC_OpenResponse";

		private static readonly int RequestOpenHash = StableHash.Compute("RPC_RequestOpen");

		private static readonly int OpenResponseHash = StableHash.Compute("RPC_OpenResponse");

		private static bool _installed;

		internal static int Seen { get; private set; }

		internal static bool IsInstalled => _installed;

		internal static void Install(Harmony harmony)
		{
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Expected O, but got Unknown
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Expected O, but got Unknown
			if (_installed || harmony == null)
			{
				return;
			}
			try
			{
				Type? typeFromHandle = typeof(ZRoutedRpc);
				MethodInfo methodInfo = AccessTools.Method(typeFromHandle, "HandleRoutedRPC", (Type[])null, (Type[])null);
				MethodInfo methodInfo2 = AccessTools.Method(typeFromHandle, "RouteRPC", (Type[])null, (Type[])null);
				MethodInfo methodInfo3 = AccessTools.Method(typeof(ContainerOpenSignal), "OnRoutedRpc", (Type[])null, (Type[])null);
				if (methodInfo == null || methodInfo2 == null)
				{
					AcsLog.Warning("sorter: the open signal snoop is off - ZRoutedRpc." + ((methodInfo == null) ? "HandleRoutedRPC" : "RouteRPC") + " was not found; InUse, the owner revision jump and the revision stability window still protect every move");
					return;
				}
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(methodInfo3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_installed = true;
				AcsLog.Debug("sorter: open signal snoop installed on ZRoutedRpc.HandleRoutedRPC and ZRoutedRpc.RouteRPC (decision X2)");
			}
			catch (Exception error)
			{
				AcsLog.Error("sorter: installing the open signal snoop failed, falling back to InUse plus the owner revision jump", error);
			}
		}

		private static void OnRoutedRpc(RoutedRPCData __0)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (__0 != null && (__0.m_methodHash == RequestOpenHash || __0.m_methodHash == OpenResponseHash) && !((ZDOID)(ref __0.m_targetZDO)).IsNone())
				{
					Sorter.NoteChestOpened(__0.m_targetZDO, __0.m_senderPeerID);
					Seen++;
				}
			}
			catch (Exception error)
			{
				AcsLog.Error("sorter: reading an open signal failed", error);
			}
		}
	}
	internal static class ItemProgressionRuntimeCheck
	{
		private static bool _loggedThisSession;

		internal static bool IsObjectDbReady
		{
			get
			{
				ObjectDB instance = ObjectDB.instance;
				if ((Object)(object)instance != (Object)null && instance.m_items != null)
				{
					return instance.m_items.Count > 0;
				}
				return false;
			}
		}

		internal static void Reset()
		{
			_loggedThisSession = false;
		}

		internal static bool LogOnce()
		{
			if (_loggedThisSession || !IsObjectDbReady)
			{
				return false;
			}
			_loggedThisSession = true;
			foreach (string item in Report())
			{
				AcsLog.Debug(item);
			}
			return true;
		}

		internal static IReadOnlyList<string> Report()
		{
			try
			{
				if (!IsObjectDbReady)
				{
					return new List<string> { "sorter progression: the item database is not up yet, ask again in a moment" };
				}
				List<string> list = new List<string>();
				List<string> list2 = new List<string>();
				Collect(list, list2);
				return ItemProgressionAudit.Check((IEnumerable<string>)list, (IEnumerable<string>)list2).Describe(12);
			}
			catch (Exception error)
			{
				AcsLog.Error("sorter progression: checking the progression table failed", error);
				return new List<string> { "sorter progression: the check failed, see the server log" };
			}
		}

		private static void Collect(List<string> names, List<string> stackables)
		{
			List<GameObject> items = ObjectDB.instance.m_items;
			for (int i = 0; i < items.Count; i++)
			{
				GameObject val = items[i];
				if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(((Object)val).name))
				{
					names.Add(((Object)val).name);
					ItemDrop component = val.GetComponent<ItemDrop>();
					if ((Object)(object)component != (Object)null && component.m_itemData != null && component.m_itemData.m_shared != null && component.m_itemData.m_shared.m_maxStackSize > 1)
					{
						stackables.Add(((Object)val).name);
					}
				}
			}
		}
	}
	internal enum RelocationKind
	{
		UnassignedOut = 1,
		ForeignGroupOut,
		MiscIn
	}
	internal static class RelocationKindText
	{
		internal static string RoleOf(RelocationKind kind)
		{
			if (kind != RelocationKind.MiscIn)
			{
				return "group chest";
			}
			return "misc chest";
		}

		internal static string StageOf(RelocationKind kind)
		{
			return kind switch
			{
				RelocationKind.UnassignedOut => "unassigned", 
				RelocationKind.ForeignGroupOut => "foreign group", 
				_ => "misc", 
			};
		}

		internal static bool FromGroupChest(RelocationKind kind)
		{
			return kind != RelocationKind.MiscIn;
		}

		internal static MisplacedKind WantedOf(RelocationKind kind)
		{
			return (MisplacedKind)(kind switch
			{
				RelocationKind.UnassignedOut => 2, 
				RelocationKind.ForeignGroupOut => 1, 
				_ => 0, 
			});
		}
	}
	internal sealed class Sorter : MonoBehaviour
	{
		private struct CandidateMeter
		{
			internal static readonly CandidateMeter Empty;

			private long _scans;

			private long _total;

			private int _max;

			private int _last;

			internal void Note(int candidates)
			{
				_scans++;
				_total += candidates;
				_last = candidates;
				if (candidates > _max)
				{
					_max = candidates;
				}
			}

			internal string Describe()
			{
				if (_scans == 0L)
				{
					return "no scan yet";
				}
				double num = (double)_total / (double)_scans;
				return "last " + _last + ", avg " + num.ToString("F1", CultureInfo.InvariantCulture) + ", max " + _max + " over " + _scans + " scan(s)";
			}
		}

		private readonly struct WaitKey : IEquatable<WaitKey>
		{
			internal ChestId Chest { get; }

			internal int PrefabHash { get; }

			internal WaitKey(ChestId chest, int prefabHash)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0002: Unknown result type (might be due to invalid IL or missing references)
				Chest = chest;
				PrefabHash = prefabHash;
			}

			public bool Equals(WaitKey other)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0008: Unknown result type (might be due to invalid IL or missing references)
				if (Chest == other.Chest)
				{
					return PrefabHash == other.PrefabHash;
				}
				return false;
			}

			public override bool Equals(object obj)
			{
				if (obj is WaitKey other)
				{
					return Equals(other);
				}
				return false;
			}

			public override int GetHashCode()
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				return (((object)Chest/*cast due to .constrained prefix*/).GetHashCode() * 397) ^ PrefabHash;
			}
		}

		private struct PendingVerification
		{
			internal ChestId SourceId;

			internal ChestId TargetId;

			internal int PrefabHash;

			internal int Amount;

			internal int ExpectedSourceAfter;

			internal int ExpectedTargetAfter;

			internal bool InPlaceMerge;

			internal bool Beautify;

			internal int ExpectedRecordsAfter;

			internal int ExpectedFingerprint;

			internal IReadOnlyList<ItemRecord> ExpectedLayout;

			internal byte[] BytesBefore;

			internal byte[] BytesAfter;

			internal uint TargetRevision;

			internal uint SourceRevision;

			internal string Item;

			internal int DelaySeconds;

			internal double DueAt;
		}

		private readonly struct ItemInfo
		{
			internal string PrefabName { get; }

			internal int MaxStack { get; }

			internal ItemInfo(string prefabName, int maxStack)
			{
				PrefabName = prefabName ?? string.Empty;
				MaxStack = maxStack;
			}
		}

		private const double SummarySeconds = 60.0;

		private const double RelocationQuietFactor = 2.0;

		private const double BeautifyQuietFactor = 60.0;

		private const double BeautifyBackoffSeconds = 600.0;

		private const int BeautifyLossEvidencePerSession = 3;

		private static GameObject _host;

		private readonly SortRotation _rotation = new SortRotation();

		private readonly SortRotation _distributionSources = new SortRotation();

		private readonly SortRotation _unassignedSources = new SortRotation();

		private readonly SortRotation _foreignSources = new SortRotation();

		private readonly SortRotation _miscSources = new SortRotation();

		private readonly SortRotation _beautifySources = new SortRotation();

		private readonly ChestTidyMemo _tidyAt = new ChestTidyMemo();

		private readonly BeautifyBackoff _beautifyBackoff = new BeautifyBackoff(600.0);

		private readonly SortCursorTable _cursors = new SortCursorTable();

		private readonly SortCursorTable _unassignedCursors = new SortCursorTable();

		private readonly SortCursorTable _foreignCursors = new SortCursorTable();

		private readonly SortCursorTable _miscCursors = new SortCursorTable();

		private readonly UnsortableTracker _unsortable = new UnsortableTracker();

		private readonly ChestStabilityTracker _stability = new ChestStabilityTracker();

		private readonly ConflictCounters _counters = new ConflictCounters();

		private readonly BlockedPauseTracker _pause = new BlockedPauseTracker();

		private readonly MovedLedger _ledger = new MovedLedger();

		private readonly InventoryCache _inventories = new InventoryCache();

		private readonly SortBudget _budget = new SortBudget();

		private readonly TickStatistics _tickStats = new TickStatistics();

		private readonly SorterSummaryWindow _summary = new SorterSummaryWindow();

		private readonly RelocationRateMeter _relocations = new RelocationRateMeter();

		private readonly LogChangeGate _gridGuard = new LogChangeGate();

		private readonly Dictionary<int, ItemInfo> _itemCache = new Dictionary<int, ItemInfo>();

		private readonly HashSet<long> _reportedUnknownItems = new HashSet<long>();

		private readonly List<ChestId> _activeInputs = new List<ChestId>();

		private readonly List<ChestId> _activeRelocationSources = new List<ChestId>();

		private readonly List<ChestId> _activeDistributionSources = new List<ChestId>();

		private readonly List<ChestId> _activeBeautifySources = new List<ChestId>();

		private readonly List<ChestSnapshot> _candidates = new List<ChestSnapshot>();

		private readonly HashSet<ChestId> _activeClusters = new HashSet<ChestId>();

		private double _activeClustersAt = -1.0;

		private CandidateMeter _inputCandidates = CandidateMeter.Empty;

		private CandidateMeter _relocationCandidates = CandidateMeter.Empty;

		private CandidateMeter _distributionCandidates = CandidateMeter.Empty;

		private readonly List<ChestId> _blockedThisRound = new List<ChestId>();

		private readonly List<string> _recentMoves = new List<string>();

		private readonly List<PendingVerification> _pendingVerifications = new List<PendingVerification>();

		private readonly PendingWriteSet _pendingWrites = new PendingWriteSet();

		private readonly Dictionary<WaitKey, string> _lastWaitReason = new Dictionary<WaitKey, string>();

		private const string StageBeautify = "beautify";

		private const string StageInput = "input";

		private readonly StageBlockLog _stageBlocks = new StageBlockLog();

		private bool _isServer;

		private bool _checkedRole;

		private double _nextTickAt;

		private double _nextSummaryAt;

		private bool _pausedByOperator;

		private int _miscCursor;

		private int _parsesThisTick;

		private int _cacheHitsThisTick;

		private SortOutcome _lastOutcome = (SortOutcome)1;

		private string _lastLine = "nothing yet";

		private bool _beautifyWasEnabled;

		private bool _distributionWasEnabled;

		private int _beautifyLossEvidenceLeft = 3;

		private ChestId _lastBeautified = ChestId.None;

		private long _beautifyNotReady;

		private long _beautifyUnsafe;

		private long _beautifyRewrites;

		private const string StageDistribution = "distribution";

		private readonly DistributionPlanCache _plans = new DistributionPlanCache();

		private readonly DistributionCycleGuard _cycles = new DistributionCycleGuard(4, 300.0);

		private readonly DistributionNoticeLog _distributionNotices = new DistributionNoticeLog();

		private readonly RelocationRateMeter _planRebuilds = new RelocationRateMeter();

		private long _distributionDeferrals;

		private long _distributionStuck;

		private long _distributionCycleStops;

		private string _lastDistributionUnit = string.Empty;

		internal static Sorter Instance { get; private set; }

		private static double Now => Time.realtimeSinceStartup;

		internal bool IsPausedByOperator => _pausedByOperator;

		private static int VerifyDelayOf(AcsConfig cfg)
		{
			int num = ((cfg == null) ? AcsConfigKeys.SortIntervalSeconds.FallbackInt : cfg.SortIntervalSeconds);
			if (num < AcsConfigKeys.SortIntervalSeconds.MinimumInt)
			{
				num = AcsConfigKeys.SortIntervalSeconds.MinimumInt;
			}
			else if (num > AcsConfigKeys.SortIntervalSeconds.MaximumInt)
			{
				num = AcsConfigKeys.SortIntervalSeconds.MaximumInt;
			}
			return ReadBackDelay.Seconds(num);
		}

		internal static void Install()
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected O, but got Unknown
			if ((Object)(object)_host == (Object)null)
			{
				_host = new GameObject("ACS_Sorter");
				Object.DontDestroyOnLoad((Object)(object)_host);
				Instance = _host.AddComponent<Sorter>();
			}
			Instance.BeginSession();
		}

		private void BeginSession()
		{
			if (_checkedRole && _isServer)
			{
				ClearWorldState("a new session started before the last one was seen to end");
			}
			_checkedRole = false;
			_isServer = false;
			((Behaviour)this).enabled = true;
		}

		private void ClearWorldState(string reason)
		{
			//IL_01b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d5: Unknown result type (might be due to invalid IL or missing references)
			_rotation.Clear();
			_unassignedSources.Clear();
			_foreignSources.Clear();
			_miscSources.Clear();
			_beautifySources.Clear();
			_tidyAt.Clear();
			_beautifyBackoff.Clear();
			_cursors.Clear();
			_unassignedCursors.Clear();
			_foreignCursors.Clear();
			_miscCursors.Clear();
			_unsortable.Clear();
			ClearDistributionState();
			_stability.Clear();
			_counters.Clear();
			_pause.Reset();
			_ledger.Clear();
			_inventories.Clear();
			_budget.Reset();
			_tickStats.Clear();
			_summary.Clear();
			_relocations.Clear();
			_gridGuard.Clear();
			_itemCache.Clear();
			_reportedUnknownItems.Clear();
			_activeInputs.Clear();
			_activeRelocationSources.Clear();
			_activeBeautifySources.Clear();
			_candidates.Clear();
			_blockedThisRound.Clear();
			_recentMoves.Clear();
			_pendingVerifications.Clear();
			_pendingWrites.Clear();
			_lastWaitReason.Clear();
			_stageBlocks.Clear();
			_nextTickAt = 0.0;
			_nextSummaryAt = 0.0;
			_miscCursor = 0;
			_parsesThisTick = 0;
			_cacheHitsThisTick = 0;
			_lastOutcome = (SortOutcome)1;
			_lastLine = "nothing yet";
			_beautifyWasEnabled = false;
			_beautifyLossEvidenceLeft = 3;
			_lastBeautified = ChestId.None;
			_beautifyNotReady = 0L;
			_beautifyUnsafe = 0L;
			_beautifyRewrites = 0L;
			ItemProgressionRuntimeCheck.Reset();
			AcsLog.Info("sorter: session ended, state cleared (" + reason + ")");
		}

		internal static void NoteChestOpened(ZDOID target, long sender)
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			Sorter instance = Instance;
			if (!((Object)(object)instance == (Object)null) && instance._isServer && !((ZDOID)(ref target)).IsNone())
			{
				ZDO val = ((ZDOMan.instance == null) ? null : ZDOMan.instance.GetZDO(target));
				if (val != null && AcsChestCatalog.Current.IsSupported(val.GetPrefab()))
				{
					ChestId val2 = default(ChestId);
					((ChestId)(ref val2))..ctor(((ZDOID)(ref target)).UserID, ((ZDOID)(ref target)).ID);
					double num = AcsConfigService.Current.CloseCooldownSeconds;
					instance._stability.MarkOpened(val2, Now, num);
					AcsLog.Debug("sorter: open signal for " + ChestLookup.Describe(val) + " from peer " + sender + ", locked for " + num.ToString("F1", CultureInfo.InvariantCulture) + " s");
				}
			}
		}

		internal void ForgetChestsNotIn(ChestCatalog catalog, string change)
		{
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			if (!_isServer)
			{
				AcsLog.Debug("sorter: the chest prefab list changed (" + change + "), nothing to forget in a client session");
				return;
			}
			ChestCatalog val = catalog ?? ChestCatalog.Empty;
			HashSet<ChestId> hashSet = new HashSet<ChestId>();
			IReadOnlyList<ChestEntry> readOnlyList;
			if (!((Object)(object)ChestIndex.Instance == (Object)null))
			{
				readOnlyList = ChestIndex.Instance.Model.Entries();
			}
			else
			{
				IReadOnlyList<ChestEntry> readOnlyList2 = (IReadOnlyList<ChestEntry>)(object)new ChestEntry[0];
				readOnlyList = readOnlyList2;
			}
			IReadOnlyList<ChestEntry> readOnlyList3 = readOnlyList;
			for (int i = 0; i < readOnlyList3.Count; i++)
			{
				if (val.IsSupported(readOnlyList3[i].PrefabHash))
				{
					hashSet.Add(readOnlyList3[i].Id);
				}
			}
			int num = _stability.PruneExcept((ICollection<ChestId>)hashSet);
			int num2 = _inventories.PruneExcept((ICollection<ChestId>)hashSet);
			int num3 = _unsortable.PruneExcept((ICollection<ChestId>)hashSet);
			int num4 = _cursors.PruneExcept((ICollection<ChestId>)hashSet) + _unassignedCursors.PruneExcept((ICollection<ChestId>)hashSet) + _foreignCursors.PruneExcept((ICollection<ChestId>)hashSet) + _miscCursors.PruneExcept((ICollection<ChestId>)hashSet);
			int num5 = PruneDistributionState(hashSet);
			int num6 = PruneWaitReasonsExcept(hashSet) + _stageBlocks.PruneExcept((ICollection<ChestId>)hashSet);
			int num7 = DropVerificationsExcept(hashSet);
			int num8 = _pendingWrites.PruneExcept((ICollection<ChestId>)hashSet);
			if (num8 > 0)
			{
				AcsLog.Warning("sorter: " + num8 + " read back lock(s) outlived their queue entry and were dropped with the catalog change. This is an ACS bug, please report it.");
			}
			int num9 = _tidyAt.PruneExcept((ICollection<ChestId>)hashSet) + _beautifyBackoff.PruneExcept((ICollection<ChestId>)hashSet);
			_rotation.Reset();
			_distributionSources.Reset();
			_unassignedSources.Reset();
			_foreignSources.Reset();
			_miscSources.Reset();
			_beautifySources.Reset();
			_blockedThisRound.Clear();
			_gridGuard.Clear();
			AcsLog.Info("sorter: the chest prefab list changed (" + change + "), forgot " + num + " stability, " + num2 + " inventory, " + num3 + " suppression, " + num4 + " cursor, " + num6 + " wait reason, " + num9 + " beautify, " + num5 + " distribution and " + num7 + " pending read back entry(ies); " + hashSet.Count + " indexed chest(s) are still supported");
		}

		internal void Pause()
		{
			_pausedByOperator = true;
			AcsLog.Info("sorter: paused by operator");
		}

		internal void Resume()
		{
			_pausedByOperator = false;
			_pause.Reset();
			_blockedThisRound.Clear();
			_rotation.Reset();
			_distributionSources.Reset();
			_unassignedSources.Reset();
			_foreignSources.Reset();
			_miscSources.Reset();
			AcsLog.Info("sorter: resumed by operator");
		}

		internal unsafe List<string> Status()
		{
			//IL_081a: Unknown result type (might be due to invalid IL or missing references)
			//IL_081f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0830: Unknown result type (might be due to invalid IL or missing references)
			//IL_0835: Unknown result type (might be due to invalid IL or missing references)
			//IL_08fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0901: Unknown result type (might be due to invalid IL or missing references)
			//IL_0912: Unknown result type (might be due to invalid IL or missing references)
			//IL_0917: Unknown result type (might be due to invalid IL or missing references)
			//IL_097f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0984: Unknown result type (might be due to invalid IL or missing references)
			//IL_0995: Unknown result type (might be due to invalid IL or missing references)
			//IL_099a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a05: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a0a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a1b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a20: Unknown result type (might be due to invalid IL or missing references)
			//IL_0cbf: Unknown result type (might be due to invalid IL or missing references)
			//IL_0cc4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0cc6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0d44: Unknown result type (might be due to invalid IL or missing references)
			//IL_0d71: Unknown result type (might be due to invalid IL or missing references)
			//IL_0db1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0db9: Unknown result type (might be due to invalid IL or missing references)
			double now = Now;
			List<string> list = new List<string>();
			AcsConfig current = AcsConfigService.Current;
			list.Add("sorter: " + (_isServer ? "server session" : "not a server, idle") + ", " + (_pausedByOperator ? "PAUSED by operator" : "running") + (_pause.IsPaused ? (", blocked pause for another " + _pause.SecondsLeft(now).ToString("F1", CultureInfo.InvariantCulture) + " s") : string.Empty) + ", last outcome " + ((object)Unsafe.As<SortOutcome, SortOutcome>(ref _lastOutcome)/*cast due to .constrained prefix*/).ToString());
			list.Add("sorter config: interval " + AcsConfigKeys.SortIntervalSeconds.Format((double)current.SortIntervalSeconds) + " s, graphEdgeRadius " + AcsConfigKeys.GraphEdgeRadius.Format(current.GraphEdgeRadius) + " m, activationRadius " + AcsConfigKeys.ActivationRadius.Format(current.ActivationRadius) + " m, closeCooldown " + current.CloseCooldownSeconds + " s, blockedPause " + current.BlockedPauseSeconds + " s, ignoreInUseWhenOwnerDisconnected " + current.IgnoreInUseWhenOwnerDisconnected + ", conflictWarnThreshold " + current.ConflictWarnThreshold + ", relocateMisplacedItems " + current.RelocateMisplacedItems + ", relocateUnassignedToMisc " + current.RelocateUnassignedToMisc + ", relocateItemStacks " + current.RelocateItemStacks + ", beautifyChestItemOrder " + current.BeautifyChestItemOrder + ", sortBudgetMsPerSecond " + AcsConfigKeys.SortBudgetMsPerSecond.Format(current.SortBudgetMsPerSecond));
			ChestCatalog current2 = AcsChestCatalog.Current;
			list.Add("sorter catalog: " + current2.Count + " chest prefab(s) (" + current2.CountOf((ChestPrefabOrigin)0) + " vanilla, " + current2.CountOf((ChestPrefabOrigin)1) + " from config)" + (current2.IsEmpty ? " - EMPTY, nothing is sorted" : string.Empty) + ", " + AcsChestCatalog.Rejections.Count + " prefab(s) rejected, builds " + AcsChestCatalog.BuildCount + ", last trigger " + AcsChestCatalog.LastReason);
			list.Add("sorter counters: " + ((_counters.Describe().Length == 0) ? "nothing yet" : _counters.Describe()));
			ChestClusters val = (((Object)(object)ChestIndex.Instance == (Object)null) ? ChestClusters.Empty : ChestIndex.Instance.Clusters);
			ChestCluster largest = val.Largest;
			list.Add("sorter clusters: " + val.Describe() + ", " + _activeClusters.Count + " with a player inside activationRadius" + ((largest != null && (largest.Count > 40 || largest.BoundingBoxDiameter > 80f)) ? (" - the largest one is above the diagnostics threshold (" + 40 + " chests / " + 80 + " m), acs_dump lists its members") : string.Empty));
			list.Add("sorter cluster sizes: " + DescribeClusterSizes(val));
			list.Add("sorter candidates per attempt: input " + _inputCandidates.Describe() + " | relocation " + _relocationCandidates.Describe() + " | distribution unit " + _distributionCandidates.Describe());
			list.Add("sorter suppression: input/relocation " + _unsortable.DescribeHitRate());
			list.Add("sorter budget: " + ((object)_budget)?.ToString() + (_budget.IsEnabled ? (", " + _budget.SecondsUntilAffordable(now).ToString("F1", CultureInfo.InvariantCulture) + " s until the next attempt") : " (no time limit)") + ", budgetSkips " + _counters.BudgetSkips);
			list.Add("sorter cost: " + _tickStats.Describe(now) + ", " + _summary.Summaries + " release summary(ies) written, next in " + _summary.SecondsUntilDue(now).ToString("F0", CultureInfo.InvariantCulture) + " s - sortIntervalSeconds (" + AcsConfigKeys.SortIntervalSeconds.Format((double)current.SortIntervalSeconds) + " s) is only the upper bound of the pace; the time quota sets the real one");
			list.Add("sorter relocation: " + (current.RelocateMisplacedItems ? "on" : "OFF") + ", tidy-up rate " + _relocations.Describe(now) + " (warns above " + 30 + "/min), relocated " + _counters.Relocated + " of " + _counters.Moves + " move(s)");
			DescribeDistribution(list, current, now);
			list.Add("sorter beautify: " + ((current.RelocateMisplacedItems && current.BeautifyChestItemOrder) ? "on" : (current.BeautifyChestItemOrder ? "OFF (relocateMisplacedItems is off, which switches every tidy-up stage off)" : "OFF (beautifyChestItemOrder)")) + ", beautified " + _counters.Beautified + " of " + _counters.Moves + " move(s), quiet window " + ((double)current.CloseCooldownSeconds * 60.0).ToString("F0", CultureInfo.InvariantCulture) + " s, " + _tidyAt.Count + " chest(s) remembered tidy, " + _beautifyBackoff.BackedOff(now) + " chest(s) braked after " + 3 + " undone rewrite(s) (" + _beautifyBackoff.Armed + " brake(s) armed this session), skipped " + _beautifyNotReady + " x no item database, " + _beautifyUnsafe + " x unsafe chest");
			string[] obj = new string[11]
			{
				"sorter beautify queue: ",
				_beautifySources.Count.ToString(),
				" group chest(s), round ",
				_beautifySources.Rounds.ToString(),
				", last ",
				null,
				null,
				null,
				null,
				null,
				null
			};
			ChestId last = _beautifySources.Last;
			object obj2;
			if (!((ChestId)(ref last)).IsNone)
			{
				last = _beautifySources.Last;
				obj2 = ((object)(*(ChestId*)(&last))/*cast due to .constrained prefix*/).ToString();
			}
			else
			{
				obj2 = "none";
			}
			obj[5] = (string)obj2;
			obj[6] = ", last rewrite ";
			obj[7] = (((ChestId)(ref _lastBeautified)).IsNone ? "none" : ((object)Unsafe.As<ChestId, ChestId>(ref _lastBeautified)/*cast due to .constrained prefix*/).ToString());
			obj[8] = " (";
			obj[9] = _beautifyRewrites.ToString();
			obj[10] = " this session)";
			list.Add(string.Concat(obj));
			string[] obj3 = new string[21]
			{
				"sorter relocation queue: unassigned ",
				_unassignedSources.Count.ToString(),
				" group chest(s), round ",
				_unassignedSources.Rounds.ToString(),
				", last ",
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null,
				null
			};
			last = _unassignedSources.Last;
			object obj4;
			if (!((ChestId)(ref last)).IsNone)
			{
				last = _unassignedSources.Last;
				obj4 = ((object)(*(ChestId*)(&last))/*cast due to .constrained prefix*/).ToString();
			}
			else
			{
				obj4 = "none";
			}
			obj3[5] = (string)obj4;
			obj3[6] = " | foreign group ";
			obj3[7] = _foreignSources.Count.ToString();
			obj3[8] = " group chest(s), round ";
			obj3[9] = _foreignSources.Rounds.ToString();
			obj3[10] = ", last ";
			last = _foreignSources.Last;
			object obj5;
			if (!((ChestId)(ref last)).IsNone)
			{
				last = _foreignSources.Last;
				obj5 = ((object)(*(ChestId*)(&last))/*cast due to .constrained prefix*/).ToString();
			}
			else
			{
				obj5 = "none";
			}
			obj3[11] = (string)obj5;
			obj3[12] = " | misc ";
			obj3[13] = _miscSources.Count.ToString();
			obj3[14] = " misc chest(s), round ";
			obj3[15] = _miscSources.Rounds.ToString();
			obj3[16] = ", last ";
			last = _miscSources.Last;
			object obj6;
			if (!((ChestId)(ref last)).IsNone)
			{
				last = _miscSources.Last;
				obj6 = ((object)(*(ChestId*)(&last))/*cast due to .constrained prefix*/).ToString();
			}
			else
			{
				obj6 = "none";
			}
			obj3[17] = (string)obj6;
			obj3[18] = ", ";
			obj3[19] = _inventories.Count.ToString();
			obj3[20] = " cached inventory(ies)";
			list.Add(string.Concat(obj3));
			string text = _ledger.Describe((Func<int, string>)NameOf, 12);
			list.Add("sorter moved: " + ((text.Length == 0) ? "nothing yet" : text) + " (total " + _ledger.Total + " unit(s) in " + _ledger.TypeCount + " type(s), " + _pendingVerifications.Count + " move(s) waiting for their read back, " + _pendingWrites.Describe() + ")");
			list.Add("sorter queue: " + _rotation.Count + " active input chest(s), round " + _rotation.Rounds + ", misc cursor " + _miscCursor + ", blocked rounds " + _pause.BlockedRounds + "/" + 3 + ", " + _unsortable.Count + " suppressed stack(s) in " + _unsortable.ChestCount + " chest(s), " + _stability.Count + " chest(s) tracked, open signals seen " + ContainerOpenSignal.Seen + " (snoop " + (ContainerOpenSignal.IsInstalled ? "on" : "off") + ")");
			list.Add("acs client gate: " + VanillaClientGate.Describe());
			if (_rotation.Count == 0)
			{
				list.Add("sorter queue: no input chest is in a cluster wi

AutoChestSort.Core.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using AutoChestSort.Core.Groups;
using AutoChestSort.Core.Inventory;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("Nexxy")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 Nexxy")]
[assembly: AssemblyDescription("Pure logic of the AutoChestSort mod, free of Unity and BepInEx references.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+1f8132cb0513483b73b1f973e223d866478e81b9")]
[assembly: AssemblyProduct("AutoChestSort")]
[assembly: AssemblyTitle("AutoChestSort.Core")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace AutoChestSort.Core
{
	public readonly struct AcsVector3 : IEquatable<AcsVector3>
	{
		public static readonly AcsVector3 Zero;

		public float X { get; }

		public float Y { get; }

		public float Z { get; }

		public AcsVector3(float x, float y, float z)
		{
			X = x;
			Y = y;
			Z = z;
		}

		public float SqrDistanceTo(AcsVector3 other)
		{
			float num = X - other.X;
			float num2 = Y - other.Y;
			float num3 = Z - other.Z;
			return num * num + num2 * num2 + num3 * num3;
		}

		public float DistanceTo(AcsVector3 other)
		{
			return (float)Math.Sqrt(SqrDistanceTo(other));
		}

		public bool Equals(AcsVector3 other)
		{
			if (X.Equals(other.X) && Y.Equals(other.Y))
			{
				return Z.Equals(other.Z);
			}
			return false;
		}

		public override bool Equals(object? obj)
		{
			if (obj is AcsVector3 other)
			{
				return Equals(other);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return (((X.GetHashCode() * 397) ^ Y.GetHashCode()) * 397) ^ Z.GetHashCode();
		}

		public override string ToString()
		{
			return "(" + X.ToString("F1", CultureInfo.InvariantCulture) + ", " + Y.ToString("F1", CultureInfo.InvariantCulture) + ", " + Z.ToString("F1", CultureInfo.InvariantCulture) + ")";
		}
	}
	public sealed class CatalogDiff
	{
		private readonly string[] _added;

		private readonly string[] _removed;

		private readonly string[] _regridded;

		private readonly int[] _removedHashes;

		private readonly HashSet<int> _removedHashSet;

		public static CatalogDiff None { get; } = new CatalogDiff(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<int>());

		public IReadOnlyList<string> Added => _added;

		public IReadOnlyList<string> Removed => _removed;

		public IReadOnlyList<string> Regridded => _regridded;

		public IReadOnlyList<int> RemovedHashes => _removedHashes;

		public bool HasChanges
		{
			get
			{
				if (_added.Length == 0 && _removed.Length == 0)
				{
					return _regridded.Length != 0;
				}
				return true;
			}
		}

		private CatalogDiff(string[] added, string[] removed, string[] regridded, int[] removedHashes)
		{
			_added = added;
			_removed = removed;
			_regridded = regridded;
			_removedHashes = removedHashes;
			_removedHashSet = new HashSet<int>(removedHashes);
		}

		public static CatalogDiff Between(ChestCatalog? previous, ChestCatalog? next)
		{
			ChestCatalog chestCatalog = previous ?? ChestCatalog.Empty;
			ChestCatalog chestCatalog2 = next ?? ChestCatalog.Empty;
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			List<string> list3 = new List<string>();
			List<int> list4 = new List<int>();
			for (int i = 0; i < chestCatalog2.Entries.Count; i++)
			{
				ChestPrefabEntry chestPrefabEntry = chestCatalog2.Entries[i];
				ChestPrefabEntry chestPrefabEntry2 = chestCatalog.Find(chestPrefabEntry.PrefabHash);
				if (chestPrefabEntry2 == null)
				{
					list.Add(chestPrefabEntry.Name);
				}
				else if (chestPrefabEntry2.Width != chestPrefabEntry.Width || chestPrefabEntry2.Height != chestPrefabEntry.Height)
				{
					list3.Add(chestPrefabEntry.Name + " " + chestPrefabEntry2.Width + "x" + chestPrefabEntry2.Height + " -> " + chestPrefabEntry.Width + "x" + chestPrefabEntry.Height);
				}
			}
			for (int j = 0; j < chestCatalog.Entries.Count; j++)
			{
				ChestPrefabEntry chestPrefabEntry3 = chestCatalog.Entries[j];
				if (chestCatalog2.Find(chestPrefabEntry3.PrefabHash) == null)
				{
					list2.Add(chestPrefabEntry3.Name);
					list4.Add(chestPrefabEntry3.PrefabHash);
				}
			}
			if (list.Count == 0 && list2.Count == 0 && list3.Count == 0)
			{
				return None;
			}
			return new CatalogDiff(list.ToArray(), list2.ToArray(), list3.ToArray(), list4.ToArray());
		}

		public bool IsRemoved(int prefabHash)
		{
			return _removedHashSet.Contains(prefabHash);
		}

		public override string ToString()
		{
			return Describe();
		}

		public string Describe()
		{
			if (!HasChanges)
			{
				return "no change";
			}
			StringBuilder stringBuilder = new StringBuilder();
			Append(stringBuilder, "added", _added);
			Append(stringBuilder, "removed", _removed);
			Append(stringBuilder, "regridded", _regridded);
			return stringBuilder.ToString();
		}

		private static void Append(StringBuilder text, string label, string[] values)
		{
			if (values.Length != 0)
			{
				if (text.Length > 0)
				{
					text.Append(", ");
				}
				text.Append(values.Length).Append(' ').Append(label)
					.Append(" (")
					.Append(string.Join(", ", values))
					.Append(')');
			}
		}
	}
	public sealed class ChestCatalog
	{
		private readonly Dictionary<int, ChestPrefabEntry> _byHash;

		private readonly ChestPrefabEntry[] _entries;

		private readonly string[] _names;

		private readonly int[] _hashes;

		public static ChestCatalog Empty { get; } = new ChestCatalog(Array.Empty<ChestPrefabEntry>());

		public int Count => _entries.Length;

		public bool IsEmpty => _entries.Length == 0;

		public IReadOnlyList<ChestPrefabEntry> Entries => _entries;

		public IReadOnlyList<string> Names => _names;

		public IReadOnlyList<int> Hashes => _hashes;

		private ChestCatalog(ChestPrefabEntry[] entries)
		{
			_entries = entries;
			_byHash = new Dictionary<int, ChestPrefabEntry>(entries.Length);
			_names = new string[entries.Length];
			_hashes = new int[entries.Length];
			for (int i = 0; i < entries.Length; i++)
			{
				ChestPrefabEntry chestPrefabEntry = entries[i];
				_byHash[chestPrefabEntry.PrefabHash] = chestPrefabEntry;
				_names[i] = chestPrefabEntry.Name;
				_hashes[i] = chestPrefabEntry.PrefabHash;
			}
		}

		public static ChestCatalog Build(IEnumerable<ChestPrefabEntry?>? entries)
		{
			if (entries == null)
			{
				return Empty;
			}
			List<ChestPrefabEntry> list = new List<ChestPrefabEntry>();
			HashSet<int> hashSet = new HashSet<int>();
			foreach (ChestPrefabEntry entry in entries)
			{
				if (entry != null && hashSet.Add(entry.PrefabHash))
				{
					list.Add(entry);
				}
			}
			if (list.Count != 0)
			{
				return new ChestCatalog(list.ToArray());
			}
			return Empty;
		}

		public bool IsSupported(int prefabHash)
		{
			return _byHash.ContainsKey(prefabHash);
		}

		public ChestPrefabEntry? Find(int prefabHash)
		{
			if (!_byHash.TryGetValue(prefabHash, out ChestPrefabEntry value))
			{
				return null;
			}
			return value;
		}

		public string? NameOf(int prefabHash)
		{
			if (!_byHash.TryGetValue(prefabHash, out ChestPrefabEntry value))
			{
				return null;
			}
			return value.Name;
		}

		public string Describe(int prefabHash)
		{
			return NameOf(prefabHash) ?? prefabHash.ToString();
		}

		public bool TryGetGrid(int prefabHash, out int width, out int height)
		{
			if (_byHash.TryGetValue(prefabHash, out ChestPrefabEntry value))
			{
				width = value.Width;
				height = value.Height;
				return true;
			}
			width = 0;
			height = 0;
			return false;
		}

		public int SlotCountOf(int prefabHash)
		{
			if (!_byHash.TryGetValue(prefabHash, out ChestPrefabEntry value))
			{
				return 0;
			}
			return value.Slots;
		}

		public int CountOf(ChestPrefabOrigin origin)
		{
			int num = 0;
			for (int i = 0; i < _entries.Length; i++)
			{
				if (_entries[i].Origin == origin)
				{
					num++;
				}
			}
			return num;
		}

		public override string ToString()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(Count).Append(" chest prefab(s): ").Append(CountOf(ChestPrefabOrigin.Vanilla))
				.Append(" vanilla, ")
				.Append(CountOf(ChestPrefabOrigin.Config))
				.Append(" from config");
			for (int i = 0; i < _entries.Length; i++)
			{
				stringBuilder.Append((i == 0) ? " - " : ", ").Append(_entries[i].Name);
			}
			return stringBuilder.ToString();
		}
	}
	public sealed class ChestCluster
	{
		public ChestId Id { get; }

		public IReadOnlyList<ChestId> Members { get; }

		public int Count => Members.Count;

		public AcsVector3 Min { get; }

		public AcsVector3 Max { get; }

		public float BoundingBoxDiameter
		{
			get
			{
				float num = Max.X - Min.X;
				float num2 = Max.Y - Min.Y;
				float num3 = Max.Z - Min.Z;
				return (float)Math.Sqrt((double)num * (double)num + (double)num2 * (double)num2 + (double)num3 * (double)num3);
			}
		}

		internal ChestCluster(ChestId id, IReadOnlyList<ChestId> members, AcsVector3 min, AcsVector3 max)
		{
			Id = id;
			Members = members;
			Min = min;
			Max = max;
		}

		public bool Contains(ChestId id)
		{
			for (int i = 0; i < Members.Count; i++)
			{
				if (Members[i] == id)
				{
					return true;
				}
			}
			return false;
		}

		public override string ToString()
		{
			return "cluster " + Id.ToString() + ": " + Count + " chest(s), " + BoundingBoxDiameter.ToString("F1", CultureInfo.InvariantCulture) + " m across";
		}
	}
	public sealed class ChestClusters
	{
		private readonly struct CellKey : IEquatable<CellKey>
		{
			private readonly int _x;

			private readonly int _y;

			private readonly int _z;

			private CellKey(int x, int y, int z)
			{
				_x = x;
				_y = y;
				_z = z;
			}

			internal static CellKey Of(AcsVector3 position, double edge)
			{
				return new CellKey(Floor((double)position.X / edge), Floor((double)position.Y / edge), Floor((double)position.Z / edge));
			}

			internal CellKey Offset(int dx, int dy, int dz)
			{
				return new CellKey(_x + dx, _y + dy, _z + dz);
			}

			public bool Equals(CellKey other)
			{
				if (_x == other._x && _y == other._y)
				{
					return _z == other._z;
				}
				return false;
			}

			public override bool Equals(object? obj)
			{
				if (obj is CellKey other)
				{
					return Equals(other);
				}
				return false;
			}

			public override int GetHashCode()
			{
				return (((_x * 397) ^ _y) * 397) ^ _z;
			}

			private static int Floor(double value)
			{
				if (double.IsNaN(value))
				{
					return 0;
				}
				double num = Math.Floor(value);
				if (num > 2147483647.0)
				{
					return int.MaxValue;
				}
				if (!(num < -2147483648.0))
				{
					return (int)num;
				}
				return int.MinValue;
			}
		}

		private static readonly ChestId[] NoMembers = new ChestId[0];

		private static readonly ChestCluster[] NoClusters = new ChestCluster[0];

		public static readonly ChestClusters Empty = new ChestClusters(NoClusters, new Dictionary<ChestId, ChestCluster>(), 0, 0.0, 0, fromIncompleteScan: false, 0.0);

		private readonly Dictionary<ChestId, ChestCluster> _byChest;

		public IReadOnlyList<ChestCluster> All { get; }

		public int Count => All.Count;

		public int ChestCount { get; }

		public int Generation { get; }

		public double EdgeRadius { get; }

		public bool FromIncompleteScan { get; }

		public bool IsTrustworthy
		{
			get
			{
				if (Generation > 0)
				{
					return !FromIncompleteScan;
				}
				return false;
			}
		}

		public double BuildMilliseconds { get; }

		public ChestCluster? Largest
		{
			get
			{
				ChestCluster chestCluster = null;
				for (int i = 0; i < All.Count; i++)
				{
					if (chestCluster == null || All[i].Count > chestCluster.Count)
					{
						chestCluster = All[i];
					}
				}
				return chestCluster;
			}
		}

		private ChestClusters(IReadOnlyList<ChestCluster> all, Dictionary<ChestId, ChestCluster> byChest, int generation, double edgeRadius, int chestCount, bool fromIncompleteScan, double buildMilliseconds)
		{
			All = all;
			_byChest = byChest;
			Generation = generation;
			EdgeRadius = edgeRadius;
			ChestCount = chestCount;
			FromIncompleteScan = fromIncompleteScan;
			BuildMilliseconds = buildMilliseconds;
		}

		public ChestCluster? Of(ChestId id)
		{
			if (!_byChest.TryGetValue(id, out ChestCluster value))
			{
				return null;
			}
			return value;
		}

		public ChestId ClusterIdOf(ChestId id)
		{
			if (!_byChest.TryGetValue(id, out ChestCluster value))
			{
				return ChestId.None;
			}
			return value.Id;
		}

		public IReadOnlyList<ChestId> MembersOf(ChestId id)
		{
			if (!_byChest.TryGetValue(id, out ChestCluster value))
			{
				return NoMembers;
			}
			return value.Members;
		}

		public bool SameCluster(ChestId left, ChestId right)
		{
			if (!_byChest.TryGetValue(left, out ChestCluster value) || !_byChest.TryGetValue(right, out ChestCluster value2))
			{
				return false;
			}
			return value == value2;
		}

		public int SizeOfClusterWith(ChestId id)
		{
			if (!_byChest.TryGetValue(id, out ChestCluster value))
			{
				return 0;
			}
			return value.Count;
		}

		public static ChestClusters Build(IEnumerable<ChestEntry>? entries, double edgeRadius, int generation, bool fromIncompleteScan = false)
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			List<ChestEntry> list = new List<ChestEntry>();
			if (entries != null)
			{
				foreach (ChestEntry entry in entries)
				{
					if (entry != null && !entry.IsUnnamed)
					{
						list.Add(entry);
					}
				}
			}
			list.Sort((ChestEntry a, ChestEntry b) => ChestIdOrder.Compare(a.Id, b.Id));
			int[] array = new int[list.Count];
			int[] array2 = new int[list.Count];
			for (int num = 0; num < list.Count; num++)
			{
				array[num] = num;
				array2[num] = 1;
			}
			double num2 = edgeRadius;
			if (double.IsNaN(num2) || double.IsInfinity(num2) || num2 <= 0.0)
			{
				num2 = 0.0;
			}
			if (num2 > 0.0 && list.Count > 1)
			{
				UnionNeighbours(list, array, array2, num2);
			}
			IReadOnlyList<ChestCluster> readOnlyList = Collect(list, array);
			Dictionary<ChestId, ChestCluster> dictionary = new Dictionary<ChestId, ChestCluster>(list.Count);
			for (int num3 = 0; num3 < readOnlyList.Count; num3++)
			{
				ChestCluster chestCluster = readOnlyList[num3];
				for (int num4 = 0; num4 < chestCluster.Members.Count; num4++)
				{
					dictionary[chestCluster.Members[num4]] = chestCluster;
				}
			}
			stopwatch.Stop();
			return new ChestClusters(readOnlyList, dictionary, generation, num2, list.Count, fromIncompleteScan, stopwatch.Elapsed.TotalMilliseconds);
		}

		public ChestClusters MarkedAsFromIncompleteScan()
		{
			if (FromIncompleteScan)
			{
				return this;
			}
			return new ChestClusters(All, _byChest, Generation, EdgeRadius, ChestCount, fromIncompleteScan: true, BuildMilliseconds);
		}

		public static IReadOnlyList<string> Changes(ChestClusters? previous, ChestClusters? current)
		{
			List<string> list = new List<string>();
			if (previous == null || current == null || previous.Count == 0 || current.Count == 0)
			{
				return list;
			}
			for (int i = 0; i < current.All.Count; i++)
			{
				ChestCluster chestCluster = current.All[i];
				List<ChestId> list2 = DistinctClusterIds(previous, chestCluster.Members);
				if (list2.Count > 1)
				{
					list.Add("merge: " + Describe(previous, list2) + " -> cluster " + chestCluster.Id.ToString() + " (" + chestCluster.Count + " chest(s))");
				}
			}
			for (int j = 0; j < previous.All.Count; j++)
			{
				ChestCluster chestCluster2 = previous.All[j];
				List<ChestId> list3 = DistinctClusterIds(current, chestCluster2.Members);
				if (list3.Count > 1)
				{
					list.Add("split: cluster " + chestCluster2.Id.ToString() + " (" + chestCluster2.Count + " chest(s)) -> " + Describe(current, list3));
				}
			}
			return list;
		}

		public string Describe()
		{
			ChestCluster largest = Largest;
			return ChestCount + " chest(s) in " + Count + " cluster(s), largest " + ((largest == null) ? "none" : (largest.Id.ToString() + " with " + largest.Count + " chest(s), " + largest.BoundingBoxDiameter.ToString("F1", CultureInfo.InvariantCulture) + " m across")) + ", graphEdgeRadius " + EdgeRadius.ToString("F0", CultureInfo.InvariantCulture) + " m, generation " + Generation + ", build " + BuildMilliseconds.ToString("F2", CultureInfo.InvariantCulture) + " ms" + (FromIncompleteScan ? " (FROM AN INCOMPLETE SCAN)" : string.Empty);
		}

		private static List<ChestId> DistinctClusterIds(ChestClusters set, IReadOnlyList<ChestId> members)
		{
			List<ChestId> list = new List<ChestId>();
			for (int i = 0; i < members.Count; i++)
			{
				ChestId item = set.ClusterIdOf(members[i]);
				if (!item.IsNone && !list.Contains(item))
				{
					list.Add(item);
				}
			}
			list.Sort(ChestIdOrder.Comparer);
			return list;
		}

		private static string Describe(ChestClusters set, List<ChestId> ids)
		{
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < ids.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(" + ");
				}
				ChestCluster chestCluster = set.Of(ids[i]);
				stringBuilder.Append("cluster ").Append(ids[i].ToString()).Append(" (")
					.Append(chestCluster?.Count ?? 0)
					.Append(" chest(s))");
			}
			return stringBuilder.ToString();
		}

		private static void UnionNeighbours(List<ChestEntry> nodes, int[] parent, int[] size, double radius)
		{
			Dictionary<CellKey, List<int>> dictionary = new Dictionary<CellKey, List<int>>(nodes.Count);
			CellKey[] array = new CellKey[nodes.Count];
			for (int i = 0; i < nodes.Count; i++)
			{
				CellKey key = (array[i] = CellKey.Of(nodes[i].Position, radius));
				if (!dictionary.TryGetValue(key, out var value))
				{
					value = (dictionary[key] = new List<int>(4));
				}
				value.Add(i);
			}
			float num = (float)(radius * radius);
			for (int j = 0; j < nodes.Count; j++)
			{
				CellKey cellKey = array[j];
				AcsVector3 position = nodes[j].Position;
				for (int k = -1; k <= 1; k++)
				{
					for (int l = -1; l <= 1; l++)
					{
						for (int m = -1; m <= 1; m++)
						{
							if (!dictionary.TryGetValue(cellKey.Offset(k, l, m), out var value2))
							{
								continue;
							}
							for (int n = 0; n < value2.Count; n++)
							{
								int num2 = value2[n];
								if (num2 > j && position.SqrDistanceTo(nodes[num2].Position) <= num)
								{
									Union(parent, size, j, num2);
								}
							}
						}
					}
				}
			}
		}

		private static IReadOnlyList<ChestCluster> Collect(List<ChestEntry> nodes, int[] parent)
		{
			if (nodes.Count == 0)
			{
				return NoClusters;
			}
			Dictionary<int, List<int>> dictionary = new Dictionary<int, List<int>>();
			for (int i = 0; i < nodes.Count; i++)
			{
				int key = Find(parent, i);
				if (!dictionary.TryGetValue(key, out var value))
				{
					value = (dictionary[key] = new List<int>(4));
				}
				value.Add(i);
			}
			List<ChestCluster> list2 = new List<ChestCluster>(dictionary.Count);
			foreach (List<int> value2 in dictionary.Values)
			{
				ChestId[] array = new ChestId[value2.Count];
				AcsVector3 position = nodes[value2[0]].Position;
				float x = position.X;
				float y = position.Y;
				float z = position.Z;
				float x2 = position.X;
				float y2 = position.Y;
				float z2 = position.Z;
				for (int j = 0; j < value2.Count; j++)
				{
					ChestEntry chestEntry = nodes[value2[j]];
					array[j] = chestEntry.Id;
					if (chestEntry.Position.X < x)
					{
						x = chestEntry.Position.X;
					}
					if (chestEntry.Position.Y < y)
					{
						y = chestEntry.Position.Y;
					}
					if (chestEntry.Position.Z < z)
					{
						z = chestEntry.Position.Z;
					}
					if (chestEntry.Position.X > x2)
					{
						x2 = chestEntry.Position.X;
					}
					if (chestEntry.Position.Y > y2)
					{
						y2 = chestEntry.Position.Y;
					}
					if (chestEntry.Position.Z > z2)
					{
						z2 = chestEntry.Position.Z;
					}
				}
				list2.Add(new ChestCluster(array[0], array, new AcsVector3(x, y, z), new AcsVector3(x2, y2, z2)));
			}
			list2.Sort((ChestCluster a, ChestCluster b) => ChestIdOrder.Compare(a.Id, b.Id));
			return list2;
		}

		private static int Find(int[] parent, int index)
		{
			while (parent[index] != index)
			{
				parent[index] = parent[parent[index]];
				index = parent[index];
			}
			return index;
		}

		private static void Union(int[] parent, int[] size, int left, int right)
		{
			int num = Find(parent, left);
			int num2 = Find(parent, right);
			if (num != num2)
			{
				if (size[num] < size[num2])
				{
					int num3 = num2;
					num2 = num;
					num = num3;
				}
				parent[num2] = num;
				size[num] += size[num2];
			}
		}
	}
	public sealed class ChestEntry
	{
		public ChestId Id { get; }

		public int PrefabHash { get; }

		public AcsVector3 Position { get; }

		public string Name { get; }

		public bool IsUnnamed => Name.Length == 0;

		public bool IsInput => ChestNameNormalizer.IsInput(Name);

		public bool IsMisc => ChestNameNormalizer.IsMisc(Name);

		public bool IsGroupMember
		{
			get
			{
				if (!IsUnnamed)
				{
					return !ChestNameNormalizer.IsReserved(Name);
				}
				return false;
			}
		}

		public ChestEntry(ChestId id, int prefabHash, AcsVector3 position, string? name)
		{
			Id = id;
			PrefabHash = prefabHash;
			Position = position;
			Name = ChestNameNormalizer.Normalize(name);
		}

		public ChestEntry WithName(string? name)
		{
			return new ChestEntry(Id, PrefabHash, Position, name);
		}

		public override string ToString()
		{
			return PrefabHash + " " + Id.ToString() + " \"" + Name + "\" " + Position;
		}

		public string Describe(ChestCatalog? catalog)
		{
			string text = catalog?.NameOf(PrefabHash) ?? PrefabHash.ToString();
			return text + " " + Id.ToString() + " \"" + Name + "\" " + Position;
		}
	}
	public readonly struct ChestId : IEquatable<ChestId>
	{
		public static readonly ChestId None;

		public long UserId { get; }

		public uint Id { get; }

		public bool IsNone
		{
			get
			{
				if (UserId == 0L)
				{
					return Id == 0;
				}
				return false;
			}
		}

		public ChestId(long userId, uint id)
		{
			UserId = userId;
			Id = id;
		}

		public static bool operator ==(ChestId left, ChestId right)
		{
			return left.Equals(right);
		}

		public static bool operator !=(ChestId left, ChestId right)
		{
			return !left.Equals(right);
		}

		public bool Equals(ChestId other)
		{
			if (UserId == other.UserId)
			{
				return Id == other.Id;
			}
			return false;
		}

		public override bool Equals(object? obj)
		{
			if (obj is ChestId other)
			{
				return Equals(other);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return (UserId.GetHashCode() * 397) ^ (int)Id;
		}

		public override string ToString()
		{
			return UserId.ToString(CultureInfo.InvariantCulture) + ":" + Id.ToString(CultureInfo.InvariantCulture);
		}
	}
	public static class ChestIdOrder
	{
		private sealed class ChestIdComparer : IComparer<ChestId>
		{
			public int Compare(ChestId x, ChestId y)
			{
				return ChestIdOrder.Compare(x, y);
			}
		}

		public static readonly IComparer<ChestId> Comparer = new ChestIdComparer();

		public static int Compare(ChestId left, ChestId right)
		{
			int num = left.UserId.CompareTo(right.UserId);
			if (num == 0)
			{
				return left.Id.CompareTo(right.Id);
			}
			return num;
		}

		public static ChestId Min(ChestId left, ChestId right)
		{
			if (Compare(left, right) > 0)
			{
				return right;
			}
			return left;
		}
	}
	public sealed class ChestIndexModel
	{
		private static readonly ChestEntry[] NoEntries = new ChestEntry[0];

		private readonly Dictionary<ChestId, ChestEntry> _byId = new Dictionary<ChestId, ChestEntry>();

		private readonly Dictionary<string, List<ChestEntry>> _byName = new Dictionary<string, List<ChestEntry>>(StringComparer.Ordinal);

		public int Count => _byId.Count;

		public IReadOnlyList<ChestEntry> Entries()
		{
			List<ChestEntry> list = new List<ChestEntry>(_byId.Count);
			foreach (ChestEntry value in _byId.Values)
			{
				list.Add(value);
			}
			return list;
		}

		public ChestEntry? ById(ChestId id)
		{
			if (!_byId.TryGetValue(id, out ChestEntry value))
			{
				return null;
			}
			return value;
		}

		public IReadOnlyList<ChestEntry> ByName(string? name)
		{
			string text = ChestNameNormalizer.Normalize(name);
			if (text.Length == 0 || !_byName.TryGetValue(text, out List<ChestEntry> value))
			{
				return NoEntries;
			}
			return value.ToArray();
		}

		public int CountOf(string? name)
		{
			string text = ChestNameNormalizer.Normalize(name);
			if (text.Length == 0 || !_byName.TryGetValue(text, out List<ChestEntry> value))
			{
				return 0;
			}
			return value.Count;
		}

		public IReadOnlyList<string> Names()
		{
			List<string> list = new List<string>(_byName.Count);
			foreach (string key in _byName.Keys)
			{
				list.Add(key);
			}
			list.Sort(StringComparer.Ordinal);
			return list;
		}

		public IReadOnlyList<string> GroupNames()
		{
			List<string> list = new List<string>(_byName.Count);
			foreach (string key in _byName.Keys)
			{
				if (!ChestNameNormalizer.IsReserved(key))
				{
					list.Add(key);
				}
			}
			list.Sort(StringComparer.Ordinal);
			return list;
		}

		public bool IsLastChestOfGroup(ChestId id)
		{
			ChestEntry chestEntry = ById(id);
			if (chestEntry == null || !chestEntry.IsGroupMember)
			{
				return false;
			}
			return CountOf(chestEntry.Name) == 1;
		}

		public bool Upsert(ChestEntry entry)
		{
			if (entry == null)
			{
				throw new ArgumentNullException("entry");
			}
			if (_byId.TryGetValue(entry.Id, out ChestEntry value))
			{
				if (SameContent(value, entry))
				{
					return false;
				}
				DetachName(value);
			}
			_byId[entry.Id] = entry;
			AttachName(entry);
			return true;
		}

		public bool Remove(ChestId id)
		{
			if (!_byId.TryGetValue(id, out ChestEntry value))
			{
				return false;
			}
			DetachName(value);
			_byId.Remove(id);
			return true;
		}

		public int ReplaceAll(IEnumerable<ChestEntry> entries)
		{
			if (entries == null)
			{
				throw new ArgumentNullException("entries");
			}
			_byId.Clear();
			_byName.Clear();
			int num = 0;
			foreach (ChestEntry entry in entries)
			{
				if (entry != null)
				{
					if (_byId.TryGetValue(entry.Id, out ChestEntry value))
					{
						num++;
						DetachName(value);
					}
					_byId[entry.Id] = entry;
					AttachName(entry);
				}
			}
			return num;
		}

		public void Clear()
		{
			_byId.Clear();
			_byName.Clear();
		}

		public IReadOnlyList<ChestId> RenameGroup(string? oldName, string? newName)
		{
			string text = ChestNameNormalizer.Normalize(oldName);
			string text2 = ChestNameNormalizer.Normalize(newName);
			if (text.Length == 0 || string.Equals(text, text2, StringComparison.Ordinal))
			{
				return Array.Empty<ChestId>();
			}
			IReadOnlyList<ChestEntry> readOnlyList = ByName(text);
			List<ChestId> list = new List<ChestId>(readOnlyList.Count);
			foreach (ChestEntry item in readOnlyList)
			{
				Upsert(item.WithName(text2));
				list.Add(item.Id);
			}
			return list;
		}

		private static bool SameContent(ChestEntry a, ChestEntry b)
		{
			if (a.PrefabHash == b.PrefabHash && a.Position.Equals(b.Position))
			{
				return string.Equals(a.Name, b.Name, StringComparison.Ordinal);
			}
			return false;
		}

		private void AttachName(ChestEntry entry)
		{
			if (!entry.IsUnnamed)
			{
				if (!_byName.TryGetValue(entry.Name, out List<ChestEntry> value))
				{
					value = new List<ChestEntry>(1);
					_byName[entry.Name] = value;
				}
				value.Add(entry);
			}
		}

		private void DetachName(ChestEntry entry)
		{
			if (entry.IsUnnamed || !_byName.TryGetValue(entry.Name, out List<ChestEntry> value))
			{
				return;
			}
			for (int i = 0; i < value.Count; i++)
			{
				if (value[i].Id == entry.Id)
				{
					value.RemoveAt(i);
					break;
				}
			}
			if (value.Count == 0)
			{
				_byName.Remove(entry.Name);
			}
		}
	}
	public static class ChestNameNormalizer
	{
		public const int MaxLength = 32;

		public static string Normalize(string? raw)
		{
			if (string.IsNullOrEmpty(raw))
			{
				return string.Empty;
			}
			string text = raw.Trim();
			if (text.Length == 0)
			{
				return string.Empty;
			}
			string text2 = text.ToLowerInvariant();
			if (text2.Length <= 32)
			{
				return text2;
			}
			return text2.Substring(0, 32).TrimEnd(Array.Empty<char>());
		}

		public static string ToRaw(string? raw)
		{
			if (string.IsNullOrEmpty(raw))
			{
				return string.Empty;
			}
			string text = raw.Trim();
			if (text.Length == 0 || text.Length <= 32)
			{
				return text;
			}
			return text.Substring(0, 32).TrimEnd(Array.Empty<char>());
		}

		public static bool IsReserved(string? normalized)
		{
			if (!IsInput(normalized))
			{
				return IsMisc(normalized);
			}
			return true;
		}

		public static bool IsInput(string? normalized)
		{
			return Matches(normalized, "input");
		}

		public static bool IsMisc(string? normalized)
		{
			return Matches(normalized, "misc");
		}

		public static bool IsEmpty(string? normalized)
		{
			return string.IsNullOrWhiteSpace(normalized);
		}

		private static bool Matches(string? candidate, string reserved)
		{
			if (candidate != null)
			{
				return string.Equals(candidate, reserved, StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}
	}
	public sealed class ChestPrefabEntry
	{
		public const int MaxGridSide = 16;

		public string Name { get; }

		public int PrefabHash { get; }

		public int Width { get; }

		public int Height { get; }

		public int Slots => Width * Height;

		public ChestPrefabOrigin Origin { get; }

		public bool IsVanilla => Origin == ChestPrefabOrigin.Vanilla;

		private ChestPrefabEntry(string name, int prefabHash, int width, int height, ChestPrefabOrigin origin)
		{
			Name = name;
			PrefabHash = prefabHash;
			Width = width;
			Height = height;
			Origin = origin;
		}

		public static bool TryCreate(string? name, int width, int height, ChestPrefabOrigin origin, out ChestPrefabEntry? entry, out string problem)
		{
			entry = null;
			string text = (name ?? string.Empty).Trim();
			if (text.Length == 0)
			{
				problem = "the prefab name is empty";
				return false;
			}
			if (width < 1 || height < 1 || width > 16 || height > 16)
			{
				problem = text + ": grid " + width + "x" + height + " is not plausible for a chest (expected 1 .. " + 16 + " in both directions)";
				return false;
			}
			problem = string.Empty;
			entry = new ChestPrefabEntry(text, StableHash.Compute(text), width, height, origin);
			return true;
		}

		public static ChestPrefabEntry Create(string? name, int width, int height, ChestPrefabOrigin origin = ChestPrefabOrigin.Vanilla)
		{
			if (!TryCreate(name, width, height, origin, out ChestPrefabEntry entry, out string problem))
			{
				throw new ArgumentException(problem, "name");
			}
			return entry;
		}

		public string Describe()
		{
			return Name + " hash=" + PrefabHash + " grid=" + Width + "x" + Height + " slots=" + Slots + " (" + (IsVanilla ? "vanilla" : "config") + ")";
		}

		public override string ToString()
		{
			return Describe();
		}
	}
	public sealed class ChestPrefabList
	{
		public const int MaxEntries = 32;

		private static readonly char[] Separators = new char[2] { ',', ';' };

		private readonly string[] _names;

		private readonly string[] _notices;

		public static ChestPrefabList Empty { get; } = new ChestPrefabList(Array.Empty<string>(), Array.Empty<string>());

		public IReadOnlyList<string> Names => _names;

		public IReadOnlyList<string> Notices => _notices;

		public int Count => _names.Length;

		public bool IsEmpty => _names.Length == 0;

		public bool HasNotices => _notices.Length != 0;

		private ChestPrefabList(string[] names, string[] notices)
		{
			_names = names;
			_notices = notices;
		}

		public static ChestPrefabList Parse(string? text)
		{
			if (string.IsNullOrWhiteSpace(text))
			{
				return Empty;
			}
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			int num = 0;
			string[] array = text.Split(Separators);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length == 0)
				{
					continue;
				}
				if (ChestPrefabs.IsVanillaName(text2))
				{
					list2.Add(Prefix(text2) + "is a vanilla chest and always sorted, the entry is ignored");
					continue;
				}
				if (!hashSet.Add(text2))
				{
					list2.Add(Prefix(text2) + "is listed more than once, the duplicate is ignored");
					continue;
				}
				if (list.Count >= 32)
				{
					num++;
					continue;
				}
				string text3 = ChestPrefabs.FindVanillaNameIgnoringCase(text2);
				if (text3 != null)
				{
					list2.Add(Prefix(text2) + "differs from the vanilla chest \"" + text3 + "\" only in case; prefab names are case sensitive, so this is a different prefab and most likely a typo");
				}
				list.Add(text2);
			}
			if (num > 0)
			{
				list2.Add("additionalChestPrefabs: more than " + 32 + " prefab names, only the first " + 32 + " are used (" + num + " ignored)");
			}
			if (list.Count != 0 || list2.Count != 0)
			{
				return new ChestPrefabList(list.ToArray(), list2.ToArray());
			}
			return Empty;
		}

		public override string ToString()
		{
			if (Count != 0)
			{
				return "additionalChestPrefabs: " + Count + " name(s) " + string.Join(", ", _names) + (HasNotices ? (", " + _notices.Length + " notice(s)") : string.Empty);
			}
			return "additionalChestPrefabs: none" + (HasNotices ? (", " + _notices.Length + " notice(s)") : string.Empty);
		}

		private static string Prefix(string name)
		{
			return "additionalChestPrefabs: \"" + name + "\" ";
		}
	}
	public enum ChestPrefabOrigin
	{
		Vanilla,
		Config
	}
	public static class ChestPrefabs
	{
		public const string WoodName = "piece_chest_wood";

		public const string BarrelName = "piece_chest_barrel";

		public const string ReinforcedName = "piece_chest";

		public const string BlackMetalName = "piece_chest_blackmetal";

		public const string GraustenName = "piece_chest_grausten";

		public const string WardrobeName = "piece_chest_warderobe";

		public const int WoodHash = 328745978;

		public const int ReinforcedHash = -1443983522;

		public const int BlackMetalHash = 1417076401;

		public const int WoodGridWidth = 5;

		public const int WoodGridHeight = 2;

		public const int ReinforcedGridWidth = 6;

		public const int ReinforcedGridHeight = 4;

		public const int BlackMetalGridWidth = 8;

		public const int BlackMetalGridHeight = 4;

		public const int GraustenGridWidth = 8;

		public const int GraustenGridHeight = 5;

		public const int BarrelGridWidth = 6;

		public const int BarrelGridHeight = 2;

		public const int WardrobeGridWidth = 5;

		public const int WardrobeGridHeight = 10;

		private static readonly string[] VanillaNameList = new string[6] { "piece_chest_wood", "piece_chest_barrel", "piece_chest", "piece_chest_blackmetal", "piece_chest_grausten", "piece_chest_warderobe" };

		public static IReadOnlyList<string> VanillaNames => VanillaNameList;

		public static ChestCatalog VerifiedVanillaCatalog { get; } = ChestCatalog.Build(new ChestPrefabEntry[3]
		{
			ChestPrefabEntry.Create("piece_chest_wood", 5, 2),
			ChestPrefabEntry.Create("piece_chest", 6, 4),
			ChestPrefabEntry.Create("piece_chest_blackmetal", 8, 4)
		});

		public static bool IsVanillaName(string? prefabName)
		{
			if (string.IsNullOrEmpty(prefabName))
			{
				return false;
			}
			for (int i = 0; i < VanillaNameList.Length; i++)
			{
				if (string.Equals(VanillaNameList[i], prefabName, StringComparison.Ordinal))
				{
					return true;
				}
			}
			return false;
		}

		public static string? FindVanillaNameIgnoringCase(string? prefabName)
		{
			if (string.IsNullOrEmpty(prefabName))
			{
				return null;
			}
			for (int i = 0; i < VanillaNameList.Length; i++)
			{
				if (string.Equals(VanillaNameList[i], prefabName, StringComparison.OrdinalIgnoreCase))
				{
					return VanillaNameList[i];
				}
			}
			return null;
		}

		public static bool TryGetExpectedGrid(string? prefabName, out int width, out int height)
		{
			switch (prefabName)
			{
			case "piece_chest_wood":
				width = 5;
				height = 2;
				return true;
			case "piece_chest":
				width = 6;
				height = 4;
				return true;
			case "piece_chest_blackmetal":
				width = 8;
				height = 4;
				return true;
			case "piece_chest_grausten":
				width = 8;
				height = 5;
				return true;
			case "piece_chest_barrel":
				width = 6;
				height = 2;
				return true;
			case "piece_chest_warderobe":
				width = 5;
				height = 10;
				return true;
			default:
				width = 0;
				height = 0;
				return false;
			}
		}

		public static bool HasExpectedGrid(string? prefabName)
		{
			int width;
			int height;
			return TryGetExpectedGrid(prefabName, out width, out height);
		}
	}
	public enum LargeClusterNotice
	{
		Quiet,
		Warning,
		BackBelow
	}
	public sealed class LargeClusterWatch
	{
		private readonly int _maxChests;

		private readonly float _maxMetres;

		private bool _warned;

		public bool HasWarned => _warned;

		public LargeClusterWatch(int maxChests, float maxMetres)
		{
			_maxChests = maxChests;
			_maxMetres = maxMetres;
		}

		public LargeClusterNotice Check(ChestCluster? largest)
		{
			if (largest != null && (largest.Count > _maxChests || largest.BoundingBoxDiameter > _maxMetres))
			{
				_warned = true;
				return LargeClusterNotice.Warning;
			}
			if (!_warned)
			{
				return LargeClusterNotice.Quiet;
			}
			_warned = false;
			return LargeClusterNotice.BackBelow;
		}

		public void Reset()
		{
			_warned = false;
		}
	}
	public static class ReservedNames
	{
		public const string Input = "input";

		public const string Misc = "misc";

		public static IReadOnlyList<string> All { get; } = new string[2] { "input", "misc" };
	}
	public static class StableHash
	{
		public static int Compute(string? value)
		{
			string text = value ?? string.Empty;
			int num = 5381;
			int num2 = num;
			for (int i = 0; i < text.Length && text[i] != 0; i += 2)
			{
				num = ((num << 5) + num) ^ text[i];
				if (i == text.Length - 1 || text[i + 1] == '\0')
				{
					break;
				}
				num2 = ((num2 << 5) + num2) ^ text[i + 1];
			}
			return num + num2 * 1566083941;
		}
	}
	public static class WorldFileName
	{
		public const int MaxLength = 64;

		public const string Fallback = "world";

		public const string ReservedPrefix = "world_";

		private static readonly string[] ReservedDeviceNames = new string[22]
		{
			"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6",
			"COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
			"LPT8", "LPT9"
		};

		public static string Sanitize(string? worldName)
		{
			if (string.IsNullOrWhiteSpace(worldName))
			{
				return "world";
			}
			StringBuilder stringBuilder = new StringBuilder(worldName.Length);
			bool flag = false;
			string text = worldName.Trim();
			foreach (char c in text)
			{
				if (IsAllowed(c))
				{
					stringBuilder.Append(c);
					flag = false;
				}
				else if (!flag)
				{
					stringBuilder.Append('_');
					flag = true;
				}
			}
			string text2 = Trim(stringBuilder.ToString());
			if (text2.Length > 64)
			{
				text2 = Trim(text2.Substring(0, 64));
			}
			if (text2.Length == 0)
			{
				return "world";
			}
			if (!IsReservedDeviceName(text2))
			{
				return text2;
			}
			return "world_" + text2;
		}

		public static bool IsReservedDeviceName(string? name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return false;
			}
			string[] reservedDeviceNames = ReservedDeviceNames;
			foreach (string b in reservedDeviceNames)
			{
				if (string.Equals(name, b, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			return false;
		}

		private static bool IsAllowed(char c)
		{
			if (!char.IsLetterOrDigit(c) && c != '-' && c != '_')
			{
				return c == '.';
			}
			return true;
		}

		private static string Trim(string value)
		{
			return value.Trim('_', '.', '-', ' ');
		}
	}
	public static class ZdoKeys
	{
		public const string AcsNameString = "ACS_name";

		public const int AcsName = 784906341;

		public const string ItemsString = "items";

		public const int Items = -938864442;

		public const string InUseString = "InUse";

		public const int InUse = -58355724;
	}
}
namespace AutoChestSort.Core.Ui
{
	public struct ChestPanelFacts
	{
		public string DisplayName;

		public GroupRole Role;

		public int ChestStacks;

		public int GroupItems;

		public int ChestCount;

		public int OpenChests;

		public bool CtrlHeld;

		public bool Known;

		public bool Busy;

		public bool DialogOpen;

		public string NormalizedName => ChestNameNormalizer.Normalize(DisplayName);

		public bool IsUnnamed => NormalizedName.Length == 0;

		public bool IsReserved => ChestNameNormalizer.IsReserved(NormalizedName);

		public bool IsGroup
		{
			get
			{
				if (!IsUnnamed)
				{
					return !IsReserved;
				}
				return false;
			}
		}
	}
	public sealed class ChestPanelState
	{
		public bool Locked { get; private set; }

		public bool WritingBlocked { get; private set; }

		public bool NameFieldEnabled { get; private set; }

		public bool ResetEnabled { get; private set; }

		public bool InfoEnabled { get; private set; }

		public bool LearnEnabled { get; private set; }

		public bool LearnIsMiscFastpath { get; private set; }

		public bool DeleteEnabled { get; private set; }

		public bool DeleteArmed { get; private set; }

		public string LearnBlockedToken { get; private set; } = string.Empty;

		public string DeleteBlockedToken { get; private set; } = string.Empty;

		public string InfoBlockedToken { get; private set; } = string.Empty;

		private ChestPanelState()
		{
		}

		public static ChestPanelState Evaluate(ChestPanelFacts facts)
		{
			bool flag = facts.OpenChests > 0;
			bool flag2 = !facts.Known || facts.Busy || facts.DialogOpen;
			bool flag3 = flag || flag2;
			ChestPanelState chestPanelState = new ChestPanelState
			{
				Locked = flag,
				WritingBlocked = flag3,
				NameFieldEnabled = !flag3,
				ResetEnabled = (!flag3 && !facts.IsUnnamed),
				InfoEnabled = facts.IsGroup
			};
			if (facts.IsGroup)
			{
				chestPanelState.InfoBlockedToken = string.Empty;
			}
			else
			{
				chestPanelState.InfoBlockedToken = (facts.IsReserved ? "$acs_err_name_reserved" : "$acs_err_group_not_found");
			}
			bool flag4 = (chestPanelState.LearnIsMiscFastpath = facts.IsUnnamed && facts.ChestStacks == 0);
			if (facts.IsReserved)
			{
				chestPanelState.LearnEnabled = false;
				chestPanelState.LearnBlockedToken = "$acs_err_name_reserved";
			}
			else if (facts.IsUnnamed && !flag4)
			{
				chestPanelState.LearnEnabled = false;
				chestPanelState.LearnBlockedToken = "$acs_msg_name_required";
			}
			else if (flag)
			{
				chestPanelState.LearnEnabled = false;
				chestPanelState.LearnBlockedToken = "$acs_msg_group_locked";
			}
			else
			{
				chestPanelState.LearnEnabled = !flag2;
				chestPanelState.LearnBlockedToken = string.Empty;
			}
			if (!facts.IsGroup)
			{
				chestPanelState.DeleteEnabled = false;
				chestPanelState.DeleteArmed = false;
				chestPanelState.DeleteBlockedToken = (facts.IsReserved ? "$acs_err_name_reserved" : "$acs_err_group_not_found");
			}
			else if (flag)
			{
				chestPanelState.DeleteEnabled = false;
				chestPanelState.DeleteArmed = false;
				chestPanelState.DeleteBlockedToken = "$acs_msg_group_locked";
			}
			else if (flag2)
			{
				chestPanelState.DeleteEnabled = false;
				chestPanelState.DeleteArmed = false;
				chestPanelState.DeleteBlockedToken = string.Empty;
			}
			else
			{
				chestPanelState.DeleteEnabled = true;
				chestPanelState.DeleteArmed = facts.CtrlHeld;
				chestPanelState.DeleteBlockedToken = (facts.CtrlHeld ? string.Empty : "$acs_msg_delete_needs_ctrl");
			}
			return chestPanelState;
		}
	}
	public static class ChestPanelTokens
	{
		public static string RoleToken(GroupRole role)
		{
			return role switch
			{
				GroupRole.Group => "$acs_ui_role_group", 
				GroupRole.Input => "$acs_ui_role_input", 
				GroupRole.Misc => "$acs_ui_role_misc", 
				_ => "$acs_ui_role_none", 
			};
		}

		public static string LearnedToken(int skippedItems)
		{
			if (skippedItems <= 0)
			{
				return "$acs_msg_learned";
			}
			return "$acs_msg_learned_skipped";
		}
	}
	public sealed class ConflictQueue
	{
		private readonly List<LearnConflict> _conflicts = new List<LearnConflict>();

		private readonly List<string> _takeover = new List<string>();

		private readonly List<string> _skip = new List<string>();

		private int _index;

		private bool _cancelled;

		public int Total => _conflicts.Count;

		public int Position
		{
			get
			{
				if (!IsDone)
				{
					return _index + 1;
				}
				return 0;
			}
		}

		public bool IsDone
		{
			get
			{
				if (!_cancelled)
				{
					return _index >= _conflicts.Count;
				}
				return true;
			}
		}

		public bool IsCancelled => _cancelled;

		public LearnConflict? Current
		{
			get
			{
				if (!IsDone)
				{
					return _conflicts[_index];
				}
				return null;
			}
		}

		public IReadOnlyList<string> Takeover => _takeover;

		public IReadOnlyList<string> Skip => _skip;

		public ConflictQueue(IEnumerable<LearnConflict>? conflicts)
		{
			if (conflicts == null)
			{
				return;
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (LearnConflict conflict in conflicts)
			{
				if (conflict.Item.Name != null && conflict.Item.Name.Length > 0 && hashSet.Add(conflict.Item.Name))
				{
					_conflicts.Add(conflict);
				}
			}
		}

		public bool Yes()
		{
			return Answer(takeover: true);
		}

		public bool No()
		{
			return Answer(takeover: false);
		}

		public bool Answer(bool takeover)
		{
			if (IsDone)
			{
				return false;
			}
			string name = _conflicts[_index].Item.Name;
			(takeover ? _takeover : _skip).Add(name);
			_index++;
			return true;
		}

		public void Cancel()
		{
			_cancelled = true;
			_takeover.Clear();
			_skip.Clear();
		}

		public override string ToString()
		{
			return "conflicts " + Position + "/" + Total + " takeover=" + _takeover.Count + " skip=" + _skip.Count + (_cancelled ? " cancelled" : (IsDone ? " done" : string.Empty));
		}
	}
	public sealed class LearnDialogPlan
	{
		public bool AsksForMode { get; private set; }

		public bool ShowsRemovedWarning { get; private set; }

		public bool ShowsMergeHint { get; private set; }

		public string PrimaryToken { get; private set; } = string.Empty;

		public LearnMode PrimaryMode { get; private set; }

		public string SecondaryToken { get; private set; } = string.Empty;

		public LearnMode SecondaryMode { get; private set; }

		public string CancelToken { get; private set; } = string.Empty;

		public bool HasSecondary => SecondaryToken.Length > 0;

		private LearnDialogPlan()
		{
		}

		public static LearnDialogPlan ForPreview(int removedCount)
		{
			bool flag = removedCount > 0;
			return new LearnDialogPlan
			{
				AsksForMode = flag,
				ShowsRemovedWarning = flag,
				ShowsMergeHint = flag,
				PrimaryToken = (flag ? "$acs_dlg_button_replace" : "$acs_dlg_button_learn"),
				PrimaryMode = LearnMode.Replace,
				SecondaryToken = (flag ? "$acs_dlg_button_merge" : string.Empty),
				SecondaryMode = LearnMode.Merge,
				CancelToken = "$acs_dlg_button_cancel"
			};
		}

		public override string ToString()
		{
			return "learn dialog: " + (AsksForMode ? "replace|merge|cancel" : "learn|cancel");
		}
	}
}
namespace AutoChestSort.Core.Sorting
{
	public sealed class BeautifyBackoff
	{
		private struct Entry
		{
			internal int Streak;

			internal double Until;
		}

		public const int UndoneLimit = 3;

		private readonly Dictionary<ChestId, Entry> _entries = new Dictionary<ChestId, Entry>();

		private readonly double _seconds;

		public double Seconds => _seconds;

		public int Count => _entries.Count;

		public long Armed { get; private set; }

		public BeautifyBackoff(double seconds)
		{
			_seconds = ((seconds > 0.0) ? seconds : 0.0);
		}

		public int BackedOff(double now)
		{
			int num = 0;
			foreach (Entry value in _entries.Values)
			{
				if (now < value.Until)
				{
					num++;
				}
			}
			return num;
		}

		public bool IsBackedOff(ChestId chest, double now)
		{
			if (_entries.TryGetValue(chest, out var value))
			{
				return now < value.Until;
			}
			return false;
		}

		public double SecondsLeft(ChestId chest, double now)
		{
			if (!_entries.TryGetValue(chest, out var value) || now >= value.Until)
			{
				return 0.0;
			}
			return value.Until - now;
		}

		public int UndoneStreak(ChestId chest)
		{
			if (!_entries.TryGetValue(chest, out var value))
			{
				return 0;
			}
			return value.Streak;
		}

		public bool NoteVerdict(ChestId chest, ChestLayoutVerdict verdict, double now)
		{
			switch (verdict)
			{
			case ChestLayoutVerdict.Unverifiable:
				return false;
			default:
				Clear(chest, now);
				return false;
			case ChestLayoutVerdict.Undone:
			{
				_entries.TryGetValue(chest, out var value);
				value.Streak++;
				if (value.Streak < 3)
				{
					_entries[chest] = value;
					return false;
				}
				value.Streak = 0;
				value.Until = now + _seconds;
				_entries[chest] = value;
				Armed++;
				return true;
			}
			}
		}

		public bool Forget(ChestId chest)
		{
			return _entries.Remove(chest);
		}

		public int PruneExcept(ICollection<ChestId>? keep)
		{
			if (keep == null || _entries.Count == 0)
			{
				return 0;
			}
			List<ChestId> list = null;
			foreach (ChestId key in _entries.Keys)
			{
				if (!keep.Contains(key))
				{
					if (list == null)
					{
						list = new List<ChestId>();
					}
					list.Add(key);
				}
			}
			if (list == null)
			{
				return 0;
			}
			for (int i = 0; i < list.Count; i++)
			{
				_entries.Remove(list[i]);
			}
			return list.Count;
		}

		public void Clear()
		{
			_entries.Clear();
			Armed = 0L;
		}

		private void Clear(ChestId chest, double now)
		{
			if (_entries.TryGetValue(chest, out var value))
			{
				if (now >= value.Until)
				{
					_entries.Remove(chest);
					return;
				}
				value.Streak = 0;
				_entries[chest] = value;
			}
		}
	}
	public enum PauseEnd
	{
		Running,
		Released,
		Expired,
		NotPaused
	}
	public sealed class BlockedPauseTracker
	{
		public const int RequiredRounds = 3;

		private int _blockedRounds;

		private bool _movedThisRound;

		private bool _inUseThisRound;

		private double _pauseUntil;

		public bool IsPaused => _pauseUntil > 0.0;

		public double PauseUntil => _pauseUntil;

		public int BlockedRounds => _blockedRounds;

		public bool PlayerBlockedThisRound => _inUseThisRound;

		public long Pauses { get; private set; }

		public void NoteMove()
		{
			_movedThisRound = true;
		}

		public bool NoteBlock(BlockReason reason)
		{
			if (reason != BlockReason.InUse)
			{
				return false;
			}
			_inUseThisRound = true;
			return true;
		}

		public bool EndRound(double now, double pauseSeconds)
		{
			bool movedThisRound = _movedThisRound;
			bool inUseThisRound = _inUseThisRound;
			_movedThisRound = false;
			_inUseThisRound = false;
			if (movedThisRound || !inUseThisRound)
			{
				_blockedRounds = 0;
				return false;
			}
			_blockedRounds++;
			if (_blockedRounds < 3 || pauseSeconds <= 0.0 || IsPaused)
			{
				return false;
			}
			_blockedRounds = 0;
			_pauseUntil = now + pauseSeconds;
			Pauses++;
			return true;
		}

		public PauseEnd Check(double now, bool released)
		{
			if (!IsPaused)
			{
				return PauseEnd.NotPaused;
			}
			if (released)
			{
				Clear();
				return PauseEnd.Released;
			}
			if (now >= _pauseUntil)
			{
				Clear();
				return PauseEnd.Expired;
			}
			return PauseEnd.Running;
		}

		public double SecondsLeft(double now)
		{
			double num = _pauseUntil - now;
			if (!IsPaused || !(num > 0.0))
			{
				return 0.0;
			}
			return num;
		}

		public void Reset()
		{
			Clear();
			_blockedRounds = 0;
			_movedThisRound = false;
			_inUseThisRound = false;
		}

		private void Clear()
		{
			_pauseUntil = 0.0;
		}
	}
	public enum BlockReason
	{
		None,
		InUse,
		Cooldown,
		RevisionUnstable,
		Missing,
		Unreadable,
		ReadBackPending
	}
	public sealed class ChestLayoutAudit
	{
		private readonly List<ItemRecord> _before;

		private readonly ChestLayoutPlan _plan;

		private readonly Func<int, int>? _maxStackOf;

		private readonly int _gridWidth;

		private readonly int _gridHeight;

		private readonly int _version;

		private readonly byte[] _trailer;

		public int RecordsBefore { get; }

		public int RecordsAfter { get; private set; }

		public int UnitsBefore { get; }

		public int UnitsAfter { get; private set; }

		public bool IsComplete { get; private set; }

		public string Violation { get; private set; }

		public bool IsValid
		{
			get
			{
				if (IsComplete)
				{
					return Violation.Length == 0;
				}
				return false;
			}
		}

		private ChestLayoutAudit(List<ItemRecord> before, ChestLayoutPlan plan, Func<int, int>? maxStackOf, int gridWidth, int gridHeight, int version, byte[] trailer)
		{
			_before = before;
			_plan = plan;
			_maxStackOf = maxStackOf;
			_gridWidth = gridWidth;
			_gridHeight = gridHeight;
			_version = version;
			_trailer = trailer;
			RecordsBefore = before.Count;
			RecordsAfter = before.Count;
			UnitsBefore = Units(before);
			UnitsAfter = UnitsBefore;
			Violation = string.Empty;
		}

		public static ChestLayoutAudit Before(InventoryPackage? package, ChestLayoutPlan? plan, Func<int, int>? maxStackOf)
		{
			List<ItemRecord> list = new List<ItemRecord>();
			if (package != null)
			{
				list.AddRange(package.Items);
			}
			return new ChestLayoutAudit(list, plan ?? ChestLayoutPlan.Nothing("no plan"), maxStackOf, package?.GridWidth ?? 0, package?.GridHeight ?? 0, package?.Version ?? 0, (package == null) ? Array.Empty<byte>() : ((byte[])package.Trailer.Clone()));
		}

		public void After(InventoryPackage? package)
		{
			IsComplete = true;
			RecordsAfter = package?.Count ?? 0;
			UnitsAfter = ((package != null) ? Units(package.Items) : 0);
			if (package == null)
			{
				Violation = "the inventory is gone";
			}
			else
			{
				Violation = FirstViolation(package);
			}
		}

		public string Describe()
		{
			return "units " + UnitsBefore + "->" + UnitsAfter + ", records " + RecordsBefore + "->" + RecordsAfter;
		}

		public override string ToString()
		{
			return Describe() + ((!IsComplete) ? " (incomplete)" : ((Violation.Length == 0) ? string.Empty : (" INVALID: " + Violation)));
		}

		private string FirstViolation(InventoryPackage package)
		{
			if (UnitsAfter != UnitsBefore)
			{
				return "the chest holds " + UnitsAfter + " unit(s) instead of " + UnitsBefore;
			}
			int num = RecordsBefore - _plan.MergedStacks;
			if (RecordsAfter != num)
			{
				return "the chest holds " + RecordsAfter + " record(s) instead of " + num + " (" + RecordsBefore + " minus " + _plan.MergedStacks + " merge(s))";
			}
			string text = CheckMapping(package);
			if (text.Length > 0)
			{
				return text;
			}
			string text2 = CheckGrid(package);
			if (text2.Length > 0)
			{
				return text2;
			}
			string text3 = CheckClasses(package);
			if (text3.Length > 0)
			{
				return text3;
			}
			int num2 = CountNonPositive(_before);
			int num3 = CountNonPositive(package.Items);
			if (num3 > num2)
			{
				return "the rewrite created " + (num3 - num2) + " record(s) with a stack of zero or less";
			}
			return CheckBytes(package);
		}

		private string CheckMapping(InventoryPackage package)
		{
			IReadOnlyList<int> originOf = _plan.OriginOf;
			if (originOf.Count != package.Count)
			{
				return "the plan maps " + originOf.Count + " record(s), the chest holds " + package.Count;
			}
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < package.Count; i++)
			{
				int num = originOf[i];
				if (num < 0 || num >= _before.Count)
				{
					return "record " + i + " claims to come from record " + num + ", which the chest never held";
				}
				if (!hashSet.Add(num))
				{
					return "record " + i + " and an earlier one both claim to come from record " + num + "; the mapping is not injective";
				}
				if (!package.Items[i].RawEqualsIgnoringGridPositionAndStack(_before[num]))
				{
					return "record " + i + " is not the record " + num + " it came from (" + _before[num]?.ToString() + " -> " + package.Items[i]?.ToString() + ")";
				}
			}
			return string.Empty;
		}

		private string CheckGrid(InventoryPackage package)
		{
			if (_gridWidth <= 0 || _gridHeight <= 0)
			{
				return "the grid size of this chest is unknown";
			}
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < package.Count; i++)
			{
				ItemRecord itemRecord = package.Items[i];
				if (itemRecord.GridX >= _gridWidth || itemRecord.GridY >= _gridHeight)
				{
					string[] obj = new string[11]
					{
						"record ",
						i.ToString(),
						" sits at slot ",
						itemRecord.GridX.ToString(),
						",",
						itemRecord.GridY.ToString(),
						", outside the ",
						null,
						null,
						null,
						null
					};
					int gridWidth = _gridWidth;
					obj[7] = gridWidth.ToString();
					obj[8] = "x";
					gridWidth = _gridHeight;
					obj[9] = gridWidth.ToString();
					obj[10] = " grid";
					return string.Concat(obj);
				}
				if (!hashSet.Add(itemRecord.GridY * _gridWidth + itemRecord.GridX))
				{
					return "two records share the grid slot " + itemRecord.GridX + "," + itemRecord.GridY;
				}
			}
			return string.Empty;
		}

		private string CheckClasses(InventoryPackage package)
		{
			List<ItemRecord> list = new List<ItemRecord>();
			List<int> list2 = new List<int>();
			List<int> list3 = new List<int>();
			List<int> list4 = new List<int>();
			foreach (ItemRecord item in _before)
			{
				int num = IndexOfClass(list, item);
				if (num < 0)
				{
					list.Add(item);
					list2.Add(0);
					list3.Add(0);
					list4.Add(0);
					num = list.Count - 1;
				}
				int num2 = MaxStackOf(item.PrefabHash);
				list2[num] += item.Stack;
				if (num2 > 0 && item.Stack < num2)
				{
					list3[num]++;
				}
				if (item.Stack > list4[num])
				{
					list4[num] = item.Stack;
				}
			}
			int[] array = new int[list.Count];
			int[] array2 = new int[list.Count];
			int[] array3 = new int[list.Count];
			for (int i = 0; i < package.Count; i++)
			{
				ItemRecord itemRecord = package.Items[i];
				int num3 = IndexOfClass(list, itemRecord);
				if (num3 < 0)
				{
					return "record " + i + " belongs to no item the chest held before (" + itemRecord?.ToString() + ")";
				}
				int num4 = MaxStackOf(itemRecord.PrefabHash);
				array[num3] += itemRecord.Stack;
				if (num4 > 0 && itemRecord.Stack < num4)
				{
					array2[num3]++;
				}
				if (itemRecord.Stack > array3[num3])
				{
					array3[num3] = itemRecord.Stack;
				}
			}
			for (int j = 0; j < list.Count; j++)
			{
				if (array[j] != list2[j])
				{
					return "the item " + list[j]?.ToString() + " holds " + array[j] + " unit(s) instead of " + list2[j];
				}
				int num5 = MaxStackOf(list[j].PrefabHash);
				if (num5 > 1 && list4[j] <= num5)
				{
					if (array3[j] > num5)
					{
						return "the item " + list[j]?.ToString() + " has a stack of " + array3[j] + " above its maximum of " + num5;
					}
					if (array2[j] > 1)
					{
						return "the item " + list[j]?.ToString() + " is left with " + array2[j] + " partial stacks instead of one at most";
					}
				}
				else if (array2[j] > list3[j] || array3[j] > list4[j])
				{
					return "the item " + list[j]?.ToString() + " may only be rearranged, but its stacks changed (partial " + list3[j] + "->" + array2[j] + ", biggest " + list4[j] + "->" + array3[j] + ")";
				}
			}
			return string.Empty;
		}

		private string CheckBytes(InventoryPackage package)
		{
			InventoryPackage inventoryPackage;
			try
			{
				inventoryPackage = InventoryPackage.Parse(package.Serialize());
			}
			catch (InventoryFormatException ex)
			{
				return "the rewritten chest does not parse back: " + ex.Message;
			}
			if (inventoryPackage.Version != _version)
			{
				string text = inventoryPackage.Version.ToString();
				int version = _version;
				return "the rewritten chest carries package version " + text + " instead of " + version;
			}
			if (!ItemRecord.BytesEqual(inventoryPackage.Trailer, _trailer))
			{
				return "the trailing bytes of the package changed (" + _trailer.Length + " byte(s) -> " + inventoryPackage.Trailer.Length + ")";
			}
			if (inventoryPackage.Count != package.Count)
			{
				return "the rewritten chest parses back as " + inventoryPackage.Count + " record(s) instead of " + package.Count;
			}
			for (int i = 0; i < inventoryPackage.Count; i++)
			{
				if (!inventoryPackage.Items[i].RawEquals(package.Items[i]))
				{
					return "record " + i + " does not survive the round trip (" + package.Items[i]?.ToString() + " -> " + inventoryPackage.Items[i]?.ToString() + ")";
				}
			}
			return string.Empty;
		}

		private int MaxStackOf(int prefabHash)
		{
			if (_maxStackOf == null)
			{
				return 0;
			}
			int num = _maxStackOf(prefabHash);
			if (num >= 0)
			{
				return num;
			}
			return 0;
		}

		private static int Units(IReadOnlyList<ItemRecord> records)
		{
			int num = 0;
			for (int i = 0; i < records.Count; i++)
			{
				num += records[i].Stack;
			}
			return num;
		}

		private static int CountNonPositive(IReadOnlyList<ItemRecord> records)
		{
			int num = 0;
			for (int i = 0; i < records.Count; i++)
			{
				if (records[i].Stack <= 0)
				{
					num++;
				}
			}
			return num;
		}

		private static int IndexOfClass(List<ItemRecord> samples, ItemRecord record)
		{
			for (int i = 0; i < samples.Count; i++)
			{
				if (samples[i].RawEqualsIgnoringGridPositionAndStack(record))
				{
					return i;
				}
			}
			return -1;
		}
	}
	public sealed class ChestLayoutPlan
	{
		private static readonly ItemRecord[] NoRecords = new ItemRecord[0];

		private static readonly int[] NoOrigins = new int[0];

		public IReadOnlyList<ItemRecord> Records { get; }

		public IReadOnlyList<int> OriginOf { get; }

		public bool IsTidy { get; }

		public bool IsUnsafe { get; }

		public string Reason { get; }

		public int RecordsBefore { get; }

		public int MergedStacks { get; }

		public int MovedSlots { get; }

		public int RearrangedOnlyClasses { get; }

		public int Fingerprint { get; }

		public bool HasWork
		{
			get
			{
				if (Records.Count > 0 && !IsTidy)
				{
					return !IsUnsafe;
				}
				return false;
			}
		}

		private ChestLayoutPlan(IReadOnlyList<ItemRecord> records, IReadOnlyList<int> originOf, bool tidy, bool unsafeChest, string reason, int recordsBefore, int mergedStacks, int movedSlots, int rearrangedOnlyClasses, int fingerprint)
		{
			Records = records;
			OriginOf = originOf;
			IsTidy = tidy;
			IsUnsafe = unsafeChest;
			Reason = reason ?? string.Empty;
			RecordsBefore = recordsBefore;
			MergedStacks = mergedStacks;
			MovedSlots = movedSlots;
			RearrangedOnlyClasses = rearrangedOnlyClasses;
			Fingerprint = fingerprint;
		}

		public static ChestLayoutPlan Nothing(string reason)
		{
			return new ChestLayoutPlan(NoRecords, NoOrigins, tidy: false, unsafeChest: false, reason, 0, 0, 0, 0, 0);
		}

		public static ChestLayoutPlan Unusable(string reason, int recordsBefore)
		{
			return new ChestLayoutPlan(NoRecords, NoOrigins, tidy: false, unsafeChest: true, reason, recordsBefore, 0, 0, 0, 0);
		}

		internal static ChestLayoutPlan For(IReadOnlyList<ItemRecord> records, IReadOnlyList<int> originOf, bool tidy, string reason, int recordsBefore, int mergedStacks, int movedSlots, int rearrangedOnlyClasses)
		{
			return new ChestLayoutPlan(records, originOf, tidy, unsafeChest: false, reason, recordsBefore, mergedStacks, movedSlots, rearrangedOnlyClasses, ChestLayoutVerification.FingerprintOf(records));
		}

		public string Describe()
		{
			if (IsUnsafe)
			{
				return "skipped: " + Reason;
			}
			if (!HasWork)
			{
				return Reason;
			}
			return "records " + RecordsBefore + "->" + Records.Count + ", merged " + MergedStacks + ", moved " + MovedSlots + ((RearrangedOnlyClasses > 0) ? (", kept " + RearrangedOnlyClasses) : string.Empty);
		}

		public override string ToString()
		{
			return Describe();
		}

		public void ApplyTo(InventoryPackage package)
		{
			if (package == null)
			{
				throw new ArgumentNullException("package");
			}
			if (!HasWork)
			{
				throw new InvalidOperationException("this plan has nothing to apply: " + Describe());
			}
			package.ReplaceAll(Records);
		}
	}
	public static class ChestLayoutPlanner
	{
		private sealed class LayoutClass
		{
			private readonly List<int> _indexes = new List<int>();

			internal ItemRecord Sample { get; }

			internal byte[] Canonical { get; }

			internal string? Name { get; }

			internal int MaxStack { get; }

			internal int Tier { get; }

			internal int Bucket { get; }

			internal IReadOnlyList<int> Indexes => _indexes;

			internal int FirstIndex => _indexes[0];

			internal int Units { get; private set; }

			internal bool HasOverStack { get; private set; }

			internal bool Merges
			{
				get
				{
					if (MaxStack > 1 && MaxStack <= 65535)
					{
						return !HasOverStack;
					}
					return false;
				}
			}

			internal LayoutClass(ItemRecord sample, string? name, int maxStack)
			{
				Sample = sample;
				Canonical = sample.CanonicalWithoutPositionAndStack();
				Name = name;
				MaxStack = maxStack;
				Tier = ItemProgression.TierOf(name);
				Bucket = ((name == null) ? 2 : ((maxStack <= 1) ? 1 : 0));
			}

			internal void Add(int index, int stack)
			{
				_indexes.Add(index);
				Units += stack;
				if (MaxStack > 0 && stack > MaxStack)
				{
					HasOverStack = true;
				}
			}
		}

		private const int MaxStackField = 65535;

		private const int BucketStackable = 0;

		private const int BucketNotStackable = 1;

		private const int BucketUnknown = 2;

		public static ChestLayoutPlan Plan(InventoryPackage? package, Func<int, int>? maxStackOf, Func<int, string?>? sortKeyOf, bool objectDbReady)
		{
			if (!objectDbReady)
			{
				return ChestLayoutPlan.Nothing("skipped this tick: the item database is not up yet");
			}
			if (package == null)
			{
				return ChestLayoutPlan.Unusable("the inventory could not be read", 0);
			}
			ChestLayoutPlan chestLayoutPlan = CheckFailClosed(package);
			if (chestLayoutPlan != null)
			{
				return chestLayoutPlan;
			}
			if (package.Count == 0)
			{
				return ChestLayoutPlan.Nothing("the chest is empty");
			}
			List<LayoutClass> list = BuildClasses(package, maxStackOf, sortKeyOf);
			list.Sort(CompareClasses);
			List<ItemRecord> list2 = new List<ItemRecord>(package.Count);
			List<int> list3 = new List<int>(package.Count);
			int num = 0;
			int num2 = 0;
			foreach (LayoutClass item in list)
			{
				if (!item.Merges)
				{
					num2++;
					foreach (int index in item.Indexes)
					{
						list2.Add(package.Items[index]);
						list3.Add(index);
					}
				}
				else
				{
					num += Repack(package, item, list2, list3);
				}
			}
			return Finish(package, list2, list3, num, num2);
		}

		private static ChestLayoutPlan? CheckFailClosed(InventoryPackage package)
		{
			if (!package.HasGrid)
			{
				return ChestLayoutPlan.Unusable("the grid size of this chest is unknown", package.Count);
			}
			int num = package.GridWidth * package.GridHeight;
			if (package.Count > num)
			{
				return ChestLayoutPlan.Unusable("the chest holds " + package.Count + " record(s) but its " + package.GridWidth + "x" + package.GridHeight + " grid has only " + num + " slot(s)", package.Count);
			}
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < package.Count; i++)
			{
				ItemRecord itemRecord = package.Items[i];
				if (itemRecord.GridX >= package.GridWidth || itemRecord.GridY >= package.GridHeight)
				{
					return ChestLayoutPlan.Unusable("record " + i + " sits at slot " + itemRecord.GridX + "," + itemRecord.GridY + ", outside the " + package.GridWidth + "x" + package.GridHeight + " grid ACS knows for this chest", package.Count);
				}
				if (itemRecord.Stack <= 0)
				{
					return ChestLayoutPlan.Unusable("record " + i + " at slot " + itemRecord.GridX + "," + itemRecord.GridY + " has a stack of " + itemRecord.Stack, package.Count);
				}
				if (!hashSet.Add(itemRecord.GridY * package.GridWidth + itemRecord.GridX))
				{
					return ChestLayoutPlan.Unusable("two records share the grid slot " + itemRecord.GridX + "," + itemRecord.GridY, package.Count);
				}
			}
			return null;
		}

		private static List<LayoutClass> BuildClasses(InventoryPackage package, Func<int, int>? maxStackOf, Func<int, string?>? sortKeyOf)
		{
			List<LayoutClass> list = new List<LayoutClass>();
			Dictionary<int, string> dictionary = new Dictionary<int, string>();
			Dictionary<int, int> dictionary2 = new Dictionary<int, int>();
			for (int i = 0; i < package.Count; i++)
			{
				ItemRecord itemRecord = package.Items[i];
				LayoutClass layoutClass = null;
				for (int j = 0; j < list.Count; j++)
				{
					if (list[j].Sample.RawEqualsIgnoringGridPositionAndStack(itemRecord))
					{
						layoutClass = list[j];
						break;
					}
				}
				if (layoutClass == null)
				{
					int prefabHash = itemRecord.PrefabHash;
					if (!dictionary.TryGetValue(prefabHash, out var value))
					{
						value = (dictionary[prefabHash] = sortKeyOf?.Invoke(prefabHash));
					}
					if (!dictionary2.TryGetValue(prefabHash, out var value2))
					{
						value2 = (dictionary2[prefabHash] = maxStackOf?.Invoke(prefabHash) ?? 0);
					}
					layoutClass = new LayoutClass(itemRecord, value, value2);
					list.Add(layoutClass);
				}
				layoutClass.Add(i, itemRecord.Stack);
			}
			return list;
		}

		private static int Repack(InventoryPackage package, LayoutClass group, List<ItemRecord> records, List<int> origins)
		{
			int maxStack = group.MaxStack;
			int units = group.Units;
			int num = units / maxStack;
			int num2 = units % maxStack;
			int num3 = num + ((num2 > 0) ? 1 : 0);
			List<int> list = new List<int>(group.Indexes);
			list.Sort(delegate(int left, int right)
			{
				int num6 = package.Items[right].Stack.CompareTo(package.Items[left].Stack);
				return (num6 == 0) ? left.CompareTo(right) : num6;
			});
			for (int num4 = 0; num4 < num3; num4++)
			{
				int num5 = list[num4];
				int stack = ((num4 < num) ? maxStack : num2);
				records.Add(package.Items[num5].WithStack(stack));
				origins.Add(num5);
			}
			return list.Count - num3;
		}

		private static ChestLayoutPlan Finish(InventoryPackage package, List<ItemRecord> records, List<int> origins, int merged, int rearrangedOnly)
		{
			int num = 0;
			bool flag = records.Count == package.Count;
			for (int i = 0; i < records.Count; i++)
			{
				byte x = (byte)(i % package.GridWidth);
				byte y = (byte)(i / package.GridWidth);
				ItemRecord itemRecord = records[i].WithGridPosition(x, y);
				ItemRecord itemRecord2 = package.Items[origins[i]];
				records[i] = itemRecord;
				if (itemRecord.GridX != itemRecord2.GridX || itemRecord.GridY != itemRecord2.GridY)
				{
					num++;
				}
				if (flag && (origins[i] != i || !itemRecord.RawEquals(itemRecord2)))
				{
					flag = false;
				}
			}
			return ChestLayoutPlan.For(records, origins, flag, flag ? "the chest is already tidy" : ("records " + package.Count + "->" + records.Count), package.Count, merged, num, rearrangedOnly);
		}

		private static int CompareClasses(LayoutClass left, LayoutClass right)
		{
			int num = left.Bucket.CompareTo(right.Bucket);
			if (num != 0)
			{
				return num;
			}
			if (left.Bucket == 2)
			{
				int num2 = left.Sample.PrefabHash.CompareTo(right.Sample.PrefabHash);
				if (num2 != 0)
				{
					return num2;
				}
				return left.FirstIndex.CompareTo(right.FirstIndex);
			}
			int num3 = left.Tier.CompareTo(right.Tier);
			if (num3 != 0)
			{
				return num3;
			}
			int num4 = string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
			if (num4 != 0)
			{
				return num4;
			}
			num4 = string.CompareOrdinal(left.Name, right.Name);
			if (num4 != 0)
			{
				return num4;
			}
			if (left.Bucket == 1)
			{
				return left.FirstIndex.CompareTo(right.FirstIndex);
			}
			int num5 = right.Sample.Quality.CompareTo(left.Sample.Quality);
			if (num5 != 0)
			{
				return num5;
			}
			int num6 = left.Sample.Variant.CompareTo(right.Sample.Variant);
			if (num6 != 0)
			{
				return num6;
			}
			int num7 = right.Sample.WorldLevel.CompareTo(left.Sample.WorldLevel);
			if (num7 != 0)
			{
				return num7;
			}
			return CompareBytes(left.Canonical, right.Canonical);
		}

		private static int CompareBytes(byte[] left, byte[] right)
		{
			int num = Math.Min(left.Length, right.Length);
			for (int i = 0; i < num; i++)
			{
				if (left[i] != right[i])
				{
					if (left[i] >= right[i])
					{
						return 1;
					}
					return -1;
				}
			}
			return left.Length.CompareTo(right.Length);
		}
	}
	public enum ChestLayoutVerdict
	{
		Ok,
		Undone,
		PossibleLoss,
		Unverifiable
	}
	public readonly struct ChestLayoutVerification
	{
		public ChestLayoutVerdict Verdict { get; }

		public int MissingUnits { get; }

		public int RecordDelta { get; }

		public bool LayoutChanged { get; }

		public bool IsLoss => Verdict == ChestLayoutVerdict.PossibleLoss;

		private ChestLayoutVerification(ChestLayoutVerdict verdict, int missingUnits, int recordDelta, bool layoutChanged)
		{
			Verdict = verdict;
			MissingUnits = missingUnits;
			RecordDelta = recordDelta;
			LayoutChanged = layoutChanged;
		}

		public static int FingerprintOf(IReadOnlyList<ItemRecord>? records)
		{
			if (records == null)
			{
				return 0;
			}
			List<ItemRecord> list = new List<ItemRecord>(records.Count);
			for (int i = 0; i < records.Count; i++)
			{
				if (records[i] != null)
				{
					list.Add(records[i]);
				}
			}
			list.Sort(delegate(ItemRecord left, ItemRecord right)
			{
				int num2 = left.GridY.CompareTo(right.GridY);
				if (num2 != 0)
				{
					return num2;
				}
				int num3 = left.GridX.CompareTo(right.GridX);
				return (num3 == 0) ? left.PrefabHash.CompareTo(right.PrefabHash) : num3;
			});
			int num = -2128831035;
			num = (num ^ list.Count) * 16777619;
			foreach (ItemRecord item in list)
			{
				num = (num ^ item.GridX) * 16777619;
				num = (num ^ item.GridY) * 16777619;
				num = (num ^ item.Stack) * 16777619;
				num = (num ^ item.PrefabHash) * 16777619;
				num = (num ^ item.Quality) * 16777619;
				num = (num ^ item.Variant) * 16777619;
				num = (num ^ item.WorldLevel) * 16777619;
			}
			return num;
		}

		public static ChestLayoutVerification Check(int expectedFingerprint, IReadOnlyList<ItemRecord>? expected, IReadOnlyList<ItemRecord>? actual, bool chestHeld = false)
		{
			if (actual == null)
			{
				return new ChestLayoutVerification(ChestLayoutVerdict.Unverifiable, 0, 0, layoutChanged: true);
			}
			int num = expected?.Count ?? 0;
			int recordDelta = actual.Count - num;
			if (FingerprintOf(actual) == expectedFingerprint)
			{
				return new ChestLayoutVerification(ChestLayoutVerdict.Ok, 0, recordDelta, layoutChanged: false);
			}
			int num2 = CountMissingUnits(expected, actual);
			if (num2 > 0)
			{
				if (!chestHeld)
				{
					return new ChestLayoutVerification(ChestLayoutVerdict.PossibleLoss, num2, recordDelta, layoutChanged: true);
				}
				return new ChestLayoutVerification(ChestLayoutVerdict.Unverifiable, num2, recordDelta, layoutChanged: true);
			}
			return new ChestLayoutVerification(ChestLayoutVerdict.Undone, 0, recordDelta, layoutChanged: true);
		}

		public override string ToString()
		{
			return Verdict.ToString() + " (missingUnits=" + MissingUnits + ", recordDelta=" + RecordDelta + ", layoutChanged=" + (LayoutChanged ? "yes" : "no") + ")";
		}

		private static int CountMissingUnits(IReadOnlyList<ItemRecord>? expected, IReadOnlyList<ItemRecord> actual)
		{
			if (expected == null || expected.Count == 0)
			{
				return 0;
			}
			List<ItemRecord> list = new List<ItemRecord>();
			List<int> list2 = new List<int>();
			for (int i = 0; i < expected.Count; i++)
			{
				int num = IndexOfClass(list, expected[i]);
				if (num < 0)
				{
					list.Add(expected[i]);
					list2.Add(expected[i].Stack);
				}
				else
				{
					list2[num] += expected[i].Stack;
				}
			}
			int[] array = new int[list.Count];
			for (int j = 0; j < actual.Count; j++)
			{
				int num2 = IndexOfClass(list, actual[j]);
				if (num2 >= 0)
				{
					array[num2] += actual[j].Stack;
				}
			}
			int num3 = 0;
			for (int k = 0; k < list.Count; k++)
			{
				if (array[k] < list2[k])
				{
					num3 += list2[k] - array[k];
				}
			}
			return num3;
		}

		private static int IndexOfClass(List<ItemRecord> samples, ItemRecord? record)
		{
			if (record == null)
			{
				return -1;
			}
			for (int i = 0; i < samples.Count; i++)
			{
				if (samples[i].RawEqualsIgnoringGridPositionAndStack(record))
				{
					return i;
				}
			}
			return -1;
		}
	}
	public sealed class ChestSnapshot
	{
		public ChestId Id { get; }

		public int PrefabHash { get; }

		public AcsVector3 Position { get; }

		public string Name { get; }

		public uint DataRevision { get; }

		public BlockReason Block { get; }

		public InventoryPackage? Inventory { get; }

		public float Distance { get; }

		public bool IsUsable
		{
			get
			{
				if (Block == BlockReason.None && Inventory != null)
				{
					return Inventory.HasGrid;
				}
				return false;
			}
		}

		public bool IsInput => ChestNameNormalizer.IsInput(Name);

		public bool IsMisc => ChestNameNormalizer.IsMisc(Name);

		public bool IsGroupMember
		{
			get
			{
				if (Name.Length > 0)
				{
					return !ChestNameNormalizer.IsReserved(Name);
				}
				return false;
			}
		}

		public ChestSnapshot(ChestId id, int prefabHash, AcsVector3 position, string? name, uint dataRevision, BlockReason block, InventoryPackage? inventory, float distance)
		{
			Id = id;
			PrefabHash = prefabHash;
			Position = position;
			Name = ChestNameNormalizer.Normalize(name);
			DataRevision = dataRevision;
			Block = block;
			Inventory = inventory;
			Distance = distance;
		}

		public int CountOf(int prefabHash)
		{
			InventoryPackage inventory = Inventory;
			if (inventory == null)
			{
				return 0;
			}
			int num = 0;
			for (int i = 0; i < inventory.Count; i++)
			{
				ItemRecord itemRecord = inventory.Items[i];
				if (itemRecord.PrefabHash == prefabHash)
				{
					num += itemRecord.Stack;
				}
			}
			return num;
		}

		public override string ToString()
		{
			return Describe(null);
		}

		public string Describe(ChestCatalog? catalog)
		{
			return (catalog?.NameOf(PrefabHash) ?? PrefabHash.ToString()) + " " + Id.ToString() + " \"" + Name + "\" rev=" + DataRevision + " block=" + Block.ToString() + " items=" + ((Inventory == null) ? "?" : Inventory.Count.ToString());
		}
	}
	public sealed class ChestStabilityTracker
	{
		private sealed class Entry
		{
			internal uint DataRevision;

			internal ushort OwnerRevision;

			internal double LastChange;

			internal double CooldownUntil;

			internal double ClosedAt;

			internal bool InUse;

			internal int OpenSignals;

			internal int OwnWrites;
		}

		private readonly Dictionary<ChestId, Entry> _entries = new Dictionary<ChestId, Entry>();

		private bool _closeSeen;

		public int Count => _entries.Count;

		public bool CloseSeen => _closeSeen;

		public void Observe(ChestId chest, uint dataRevision, ushort ownerRevision, bool inUse, double now, double cooldownSeconds)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				value = new Entry
				{
					DataRevision = dataRevision,
					OwnerRevision = ownerRevision,
					LastChange = now,
					InUse = inUse
				};
				_entries[chest] = value;
				return;
			}
			if (value.DataRevision != dataRevision)
			{
				value.DataRevision = dataRevision;
				value.LastChange = now;
			}
			if (value.OwnerRevision != ownerRevision)
			{
				value.OwnerRevision = ownerRevision;
				value.LastChange = now;
				double num = now + cooldownSeconds;
				if (num > value.CooldownUntil)
				{
					value.CooldownUntil = num;
				}
			}
			if (value.InUse && !inUse)
			{
				_closeSeen = true;
				value.ClosedAt = now;
				double num2 = now + cooldownSeconds;
				if (num2 > value.CooldownUntil)
				{
					value.CooldownUntil = num2;
				}
			}
			value.InUse = inUse;
		}

		public void MarkOpened(ChestId chest, double now, double cooldownSeconds)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				value = new Entry
				{
					LastChange = now
				};
				_entries[chest] = value;
			}
			double num = now + cooldownSeconds;
			if (num > value.CooldownUntil)
			{
				value.CooldownUntil = num;
			}
			value.OpenSignals++;
		}

		public void NoteOwnWrite(ChestId chest, uint dataRevision, ushort ownerRevision)
		{
			if (_entries.TryGetValue(chest, out Entry value))
			{
				value.DataRevision = dataRevision;
				value.OwnerRevision = ownerRevision;
				value.OwnWrites++;
			}
		}

		public BlockReason Evaluate(ChestId chest, bool inUse, double now, double cooldownSeconds)
		{
			if (inUse)
			{
				return BlockReason.InUse;
			}
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				return BlockReason.RevisionUnstable;
			}
			if (now < value.CooldownUntil)
			{
				return BlockReason.Cooldown;
			}
			if (now - value.LastChange < cooldownSeconds)
			{
				return BlockReason.RevisionUnstable;
			}
			return BlockReason.None;
		}

		public bool TakeCloseSeen()
		{
			bool closeSeen = _closeSeen;
			_closeSeen = false;
			return closeSeen;
		}

		public int OpenSignalsOf(ChestId chest)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				return 0;
			}
			return value.OpenSignals;
		}

		public int OwnWritesOf(ChestId chest)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				return 0;
			}
			return value.OwnWrites;
		}

		public double SecondsSinceChange(ChestId chest, double now)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				return -1.0;
			}
			return now - value.LastChange;
		}

		public double SecondsSinceClose(ChestId chest, double now)
		{
			if (!_entries.TryGetValue(chest, out Entry value) || !(value.ClosedAt > 0.0))
			{
				return -1.0;
			}
			return now - value.ClosedAt;
		}

		public bool Forget(ChestId chest)
		{
			return _entries.Remove(chest);
		}

		public int PruneExcept(ICollection<ChestId> keep)
		{
			if (keep == null || _entries.Count == 0)
			{
				return 0;
			}
			List<ChestId> list = null;
			foreach (ChestId key in _entries.Keys)
			{
				if (!keep.Contains(key))
				{
					list = list ?? new List<ChestId>();
					list.Add(key);
				}
			}
			if (list == null)
			{
				return 0;
			}
			for (int i = 0; i < list.Count; i++)
			{
				_entries.Remove(list[i]);
			}
			return list.Count;
		}

		public void Clear()
		{
			_entries.Clear();
			_closeSeen = false;
		}
	}
	public sealed class ChestTidyMemo
	{
		private readonly Dictionary<ChestId, uint> _tidyAt = new Dictionary<ChestId, uint>();

		public int Count => _tidyAt.Count;

		public bool IsTidy(ChestId chest, uint revision)
		{
			if (_tidyAt.TryGetValue(chest, out var value))
			{
				return value == revision;
			}
			return false;
		}

		public void Note(ChestId chest, uint revision)
		{
			_tidyAt[chest] = revision;
		}

		public bool Forget(ChestId chest)
		{
			return _tidyAt.Remove(chest);
		}

		public int PruneExcept(ICollection<ChestId>? keep)
		{
			if (keep == null || _tidyAt.Count == 0)
			{
				return 0;
			}
			List<ChestId> list = null;
			foreach (ChestId key in _tidyAt.Keys)
			{
				if (!keep.Contains(key))
				{
					if (list == null)
					{
						list = new List<ChestId>();
					}
					list.Add(key);
				}
			}
			if (list == null)
			{
				return 0;
			}
			for (int i = 0; i < list.Count; i++)
			{
				_tidyAt.Remove(list[i]);
			}
			return list.Count;
		}

		public void Clear()
		{
			_tidyAt.Clear();
		}
	}
	public enum ConflictKind
	{
		Discarded,
		Waited,
		NotDiscardable,
		RevisionConflict,
		ConservationMismatch,
		LostAfterWrite,
		Duplicate
	}
	public sealed class ConflictCounters
	{
		public const double WindowSeconds = 600.0;

		public const int KindCount = 7;

		private readonly Queue<double> _window = new Queue<double>();

		private readonly long[] _totals = new long[7];

		public long Moves { get; private set; }

		public long PartialMoves { get; private set; }

		public long Unsortable { get; private set; }

		public long Ticks { get; private set; }

		public long Attempts { get; private set; }

		public long Relocated { get; private set; }

		public long Beautified { get; private set; }

		public long Distributed { get; private set; }

		public long Buffered { get; private set; }

		public long BudgetSkips { get; private set; }

		public long Pauses { get; private set; }

		public long Warnings { get; private set; }

		public int WindowCount => _window.Count;

		public bool HadActivity { get; private set; }

		public long TotalOf(ConflictKind kind)
		{
			return _totals[(int)kind];
		}

		public static bool CountsAsConflict(ConflictKind kind)
		{
			return kind != ConflictKind.Waited;
		}

		public void NoteTick()
		{
			Ticks++;
		}

		public void NoteAttempt()
		{
			Attempts++;
		}

		public void NoteBudgetSkip()
		{
			BudgetSkips++;
		}

		public void NoteMove(bool partial)
		{
			NoteMove(partial, MoveKindTag.Input);
		}

		public void NoteMove(bool partial, MoveKindTag kind)
		{
			Moves++;
			if (partial)
			{
				PartialMoves++;
			}
			switch (kind)
			{
			case MoveKindTag.Relocation:
				Relocated++;
				break;
			case MoveKindTag.Beautify:
				Beautified++;
				break;
			}
		}

		public void NoteDistribution(bool buffer)
		{
			Distributed++;
			if (buffer)
			{
				Buffered++;
			}
		}

		public void NoteUnsortable()
		{
			Unsortable++;
		}

		public void NotePause()
		{
			Pauses++;
		}

		public void Add(ConflictKind kind, double now)
		{
			_totals[(int)kind]++;
			if (CountsAsConflict(kind))
			{
				_window.Enqueue(now);
				Prune(now);
			}
		}

		public void Prune(double now)
		{
			while (_window.Count > 0 && now - _window.Peek() > 600.0)
			{
				_window.Dequeue();
			}
		}

		public bool ShouldWarn(int threshold, double now)
		{
			if (threshold <= 0)
			{
				return false;
			}
			Prune(now);
			if (_window.Count < threshold)
			{
				return false;
			}
			_window.Clear();
			Warnings++;
			return true;
		}

		public string Describe()
		{
			if (Ticks == 0L)
			{
				return string.Empty;
			}
			return "ticks=" + Ticks + " attempts=" + Attempts + " moves=" + Moves + " (partial=" + PartialMoves + ", relocated=" + Relocated + ", distributed=" + Distributed + ", buffered=" + Buffered + ", beautified=" + Beautified + ") waited=" + TotalOf(ConflictKind.Waited) + " unsortable=" + Unsortable + " discarded=" + TotalOf(ConflictKind.Discarded) + " notDiscardable=" + TotalOf(ConflictKind.NotDiscardable) + " revisionConflicts=" + TotalOf(ConflictKind.RevisionConflict) + " conservationMismatch=" + TotalOf(ConflictKind.ConservationMismatch) + " lostAfterWrite=" + TotalOf(ConflictKind.LostAfterWrite) + " duplicates=" + TotalOf(ConflictKind.Duplicate) + " budgetSkips=" + BudgetSkips + " pauses=" + Pauses + " warnings=" + Warnings + " conflictsInWindow=" + _window.Count;
		}

		public void NoteActivity()
		{
			HadActivity = true;
		}

		public void ResetActivity()
		{
			HadActivity = false;
		}

		public void Clear()
		{
			Array.Clear(_totals, 0, _totals.Length);
			_window.Clear();
			Moves = 0L;
			PartialMoves = 0L;
			Relocated = 0L;
			Distributed = 0L;
			Buffered = 0L;
			Beautified = 0L;
			Unsortable = 0L;
			Ticks = 0L;
			Attempts = 0L;
			BudgetSkips = 0L;
			Pauses = 0L;
			Warnings = 0L;
			HadActivity = false;
		}
	}
	public sealed class InPlaceMergeAudit
	{
		private readonly List<ItemRecord> _untouchedBefore;

		private readonly int _fromIndex;

		private readonly int _toIndex;

		private readonly ItemRecord? _fromBefore;

		private readonly ItemRecord? _toBefore;

		private readonly bool _amountKnown;

		private readonly string _precondition;

		public int PrefabHash { get; }

		public int UnitsBefore { get; }

		public int UnitsAfter { get; private set; }

		public int RecordsBefore { get; }

		public int RecordsAfter { get; private set; }

		public int Amount { get; }

		public int MaxStack { get; }

		public bool IsTopUp { get; }

		public bool IsFullMerge => !IsTopUp;

		public bool CanMerge => _precondition.Length == 0;

		public bool IsComplete { get; private set; }

		public string Violation { get; private set; }

		public bool IsValid
		{
			get
			{
				if (IsComplete)
				{
					return Violation.Length == 0;
				}
				return false;
			}
		}

		private InPlaceMergeAudit(int prefabHash, int fromIndex, int toIndex, int unitsBefore, int recordsBefore, List<ItemRecord> untouchedBefore, ItemRecord? fromBefore = null, ItemRecord? toBefore = null, bool amountKnown = false, int amount = 0, int maxStack = 0, bool isTopUp = false, string precondition = "")
		{
			PrefabHash = prefabHash;
			_fromIndex = fromIndex;
			_toIndex = toIndex;
			UnitsBefore = unitsBefore;
			RecordsBefore = recordsBefore;
			_untouchedBefore = untouchedBefore;
			_fromBefore = fromBefore;
			_toBefore = toBefore;
			_amountKnown = amountKnown;
			_precondition = precondition;
			Amount = amount;
			MaxStack = maxStack;
			IsTopUp = isTopUp;
			UnitsAfter = unitsBefore;
			RecordsAfter = recordsBefore;
			Violation = string.Empty;
		}

		public static InPlaceMergeAudit Before(InventoryPackage? package, int prefabHash, int fromIndex, int toIndex)
		{
			List<ItemRecord> list = new List<ItemRecord>();
			if (package == null)
			{
				return new InPlaceMergeAudit(prefabHash, fromIndex, toIndex, 0, 0, list);
			}
			CollectUntouched(package, fromIndex, toIndex, list);
			return new InPlaceMergeAudit(prefabHash, fromIndex, toIndex, MoveAudit.CountOf(package, prefabHash), package.Count, list);
		}

		public static InPlaceMergeAudit Before(InventoryPackage? package, int prefabHash, int fromIndex, int toIndex, int amount, int maxStack)
		{
			List<ItemRecord> list = new List<ItemRecord>();
			if (package == null)
			{
				return new InPlaceMergeAudit(prefabHash, fromIndex, toIndex, 0, 0, list, null, null, amountKnown: false, 0, 0, isTopUp: false, "the inventory is gone");
			}
			string text = Refuse(package, prefabHash, fromIndex, toIndex, amount, maxStack);
			ItemRecord itemRecord = null;
			ItemRecord toBefore = null;
			bool isTopUp = false;
			if (text.Length == 0)
			{
				itemRecord = package.Items[fromIndex];
				toBefore = package.Items[toIndex];
				isTopUp = amount < itemRecord.Stack;
				CollectUntouched(package, fromIndex, toIndex, list);
			}
			return new InPlaceMergeAudit(prefabHash, fromIndex, toIndex, MoveAudit.CountOf(package, prefabHash), package.Count, list, itemRecord, toBefore, amountKnown: true, amount, maxStack, isTopUp, text);
		}

		public void After(InventoryPackage? package)
		{
			IsComplete = true;
			UnitsAfter = MoveAudit.CountOf(package, PrefabHash);
			RecordsAfter = package?.Count ?? 0;
			if (_precondition.Length > 0)
			{
				Violation = _precondition;
				return;
			}
			if (package == null)
			{
				Violation = "the inventory is gone";
				return;
			}
			if (UnitsAfter != UnitsBefore)
			{
				Violation = "the units of the item type changed from " + UnitsBefore + " to " + UnitsAfter;
				return;
			}
			int num = (IsTopUp ? RecordsBefore : (RecordsBefore - 1));
			if (RecordsAfter != num)
			{
				Violation = "the chest holds " + RecordsAfter + " record(s) instead of " + num;
				return;
			}
			int num2 = ((!IsTopUp && _toIndex > _fromIndex) ? (_toIndex - 1) : _toIndex);
			if (num2 < 0 || num2 >= package.Count)
			{
				Violation = "the merged record is not in the chest any more";
				return;
			}
			if (_amountKnown)
			{
				string text = CheckSurgery(package, num2);
				if (text.Length > 0)
				{
					Violation = text;
					return;
				}
			}
			List<ItemRecord> list = new List<ItemRecord>(RecordsAfter);
			for (int i = 0; i < package.Count; i++)
			{
				if (i != num2 && (!IsTopUp || i != _fromIndex))
				{
					list.Add(package.Items[i]);
				}
			}
			if (list.Count != _untouchedBefore.Count)
			{
				Violation = "the chest holds " + list.Count + " untouched record(s) instead of " + _untouchedBefore.Count;
				return;
			}
			for (int j = 0; j < list.Count; j++)
			{
				if (!list[j].RawEquals(_untouchedBefore[j]))
				{
					Violation = "the untouched record " + j + " changed its bytes (" + _untouchedBefore[j]?.ToString() + " -> " + list[j]?.ToString() + ")";
					return;
				}
			}
			Violation = string.Empty;
		}

		public string Describe()
		{
			return "units " + UnitsBefore + "->" + UnitsAfter + ", records " + RecordsBefore + "->" + RecordsAfter + (_amountKnown ? (", " + (IsTopUp ? "top up " : "merge ") + Amount) : string.Empty);
		}

		public override string ToString()
		{
			return Describe() + ((!IsComplete) ? " (incomplete)" : ((Violation.Length == 0) ? string.Empty : (" INVALID: " + Violation)));
		}

		private static void CollectUntouched(InventoryPackage package, int fromIndex, int toIndex, List<ItemRecord> untouched)
		{
			for (int i = 0; i < package.Count; i++)
			{
				if (i != fromIndex && i != toIndex)
				{
					untouched.Add(package.Items[i]);
				}
			}
		}

		private static string Refuse(InventoryPackage package, int prefabHash, int fromIndex, int toIndex, int amount, int maxStack)
		{
			if (fromIndex < 0 || fromIndex >= package.Count || toIndex < 0 || toIndex >= package.Count)
			{
				return "slot " + fromIndex + " or " + toIndex + " is not in a chest of " + package.Count + " record(s)";
			}
			if (fromIndex == toIndex)
			{
				return "an in place merge needs two different records, both are " + fromIndex;
			}
			if (maxStack <= 0)
			{
				return "the maximum stack size of the item type is unknown";
			}
			ItemRecord itemRecord = package.Items[fromIndex];
			ItemRecord itemRecord2 = package.Items[toIndex];
			if (itemRecord.PrefabHash != prefabHash || itemRecord2.PrefabHash != prefabHash)
			{
				return "the records are not of the item type the merge is about";
			}
			if (!itemRecord2.RawEqualsIgnoringGridPositionAndStack(itemRecord))
			{
				return "the records " + fromIndex + " and " + toIndex + " are not the same merge class";
			}
			if (amount <= 0)
			{
				return "a merge of " + amount + " unit(s) is no merge";
			}
			if (amount > itemRecord.Stack)
			{
				return "the merge moves " + amount + " unit(s) but the record holds " + itemRecord.Stack;
			}
			if (itemRecord2.Stack + amount > maxStack)
			{
				return "the merge would leave " + (itemRecord2.Stack + amount) + " unit(s) in one slot, above the maximum of " + maxStack;
			}
			return string.Empty;
		}

		private string CheckSurgery(InventoryPackage package, int mergedIndex)
		{
			ItemRecord itemRecord = package.Items[mergedIndex];
			int num = _toBefore.Stack + Amount;
			if (itemRecord.Stack != num)
			{
				return "the record that took the units holds " + itemRecord.Stack + " unit(s) instead of " + num;
			}
			if (!itemRecord.RawEqualsIgnoringGridPositionAndStack(_toBefore))
			{
				return "the record that took the units changed more than its stack";
			}
			if (!IsTopUp)
			{
				return string.Empty;
			}
			if (_fromIndex < 0 || _fromIndex >= package.Count)
			{
				return "the record that gave the units is gone although this was a top up";
			}
			ItemRecord itemRecord2 = package.Items[_fromIndex];
			int num2 = _fromBefore.Stack - Amount;
			if (itemRecord2.Stack != num2)
			{
				return "the record that gave the units holds " + itemRecord2.Stack + " unit(s) instead of " + num2;
			}
			if (!itemRecord2.RawEqualsIgnoringGridPositionAndStack(_fromBefore))
			{
				return "the record that gave the units changed more than its stack";
			}
			return string.Empty;
		}
	}
	public enum InPlaceMergeVerdict
	{
		Ok,
		Undone,
		PossibleLoss,
		Unverifiable
	}
	public readonly struct InPlaceMergeVerification
	{
		public InPlaceMergeVerdict Verdict { get; }

		public int MissingUnits { get; }

		public int ExtraRecords { get; }

		public bool IsLoss => Verdict == InPlaceMergeVerdict.PossibleLoss;

		private InPlaceMergeVerification(InPlaceMergeVerdict verdict, int missingUnits, int extraRecords)
		{
			Verdict = verdict;
			MissingUnits = missingUnits;
			ExtraRecords = extraRecords;
		}

		public static InPlaceMergeVerification Check(int expectedUnits, int unitsNow, int expectedRecords, int recordsNow, bool chestHeld, bool readable = true)
		{
			int num = expectedUnits - unitsNow;
			int num2 = recordsNow - expectedRecords;
			if (num > 0)
			{
				if (!(!readable || chestHeld))
				{
					return new InPlaceMergeVerification(InPlaceMergeVerdict.PossibleLoss, num, (num2 > 0) ? num2 : 0);
				}
				return new InPlaceMergeVerification(InPlaceMergeVerdict.Unverifiable, num, (num2 > 0) ? num2 : 0);
			}
			if (!readable)
			{
				return new InPlaceMergeVerification(InPlaceMergeVerdict.Unverifiable, 0, 0);
			}
			if (num2 > 0)
			{
				return new InPlaceMergeVerification(InPlaceMergeVerdict.Undone, 0, num2);
			}
			return new InPlaceMergeVerification(InPlaceMergeVerdict.Ok, 0, 0);
		}

		public override string ToString()
		{
			return Verdict.ToString() + " (missingUnits=" + MissingUnits + ", extraRecords=" + ExtraRecords + ")";
		}
	}
	public sealed class InventoryCache
	{
		private sealed class Entry
		{
			internal object InstanceKey { get; }

			internal uint Revision { get; }

			internal InventoryPackage Package { get; }

			internal Entry(object instanceKey, uint revision, InventoryPackage package)
			{
				InstanceKey = instanceKey;
				Revision = revision;
				Package = package;
			}
		}

		private readonly Dictionary<ChestId, Entry> _entries = new Dictionary<ChestId, Entry>();

		public int Count => _entries.Count;

		public bool TryGet(ChestId chest, object? instanceKey, uint revision, out InventoryPackage? package)
		{
			package = Match(chest, instanceKey, revision);
			return package != null;
		}

		public bool TryGetCopy(ChestId chest, object? instanceKey, uint revision, out InventoryPackage? package)
		{
			package = Match(chest, instanceKey, revision)?.Clone();
			return package != null;
		}

		public void Store(ChestId chest, object instanceKey, uint revision, InventoryPackage package)
		{
			if (instanceKey == null)
			{
				throw new ArgumentNullException("instanceKey");
			}
			if (package == null)
			{
				throw new ArgumentNullException("package");
			}
			_entries[chest] = new Entry(instanceKey, revision, package.Clone());
		}

		public bool Invalidate(ChestId chest)
		{
			return _entries.Remove(chest);
		}

		public int PruneExcept(ICollection<ChestId> keep)
		{
			if (keep == null || _entries.Count == 0)
			{
				return 0;
			}
			List<ChestId> list = null;
			foreach (ChestId key in _entries.Keys)
			{
				if (!keep.Contains(key))
				{
					list = list ?? new List<ChestId>();
					list.Add(key);
				}
			}
			if (list == null)
			{
				return 0;
			}
			for (int i = 0; i < list.Count; i++)
			{
				_entries.Remove(list[i]);
			}
			return list.Count;
		}

		public void Clear()
		{
			_entries.Clear();
		}

		private InventoryPackage? Match(ChestId chest, object? instanceKey, uint revision)
		{
			if (!_entries.TryGetValue(chest, out Entry value))
			{
				return null;
			}
			if (instanceKey != null && value.InstanceKey == instanceKey && value.Revision == revision)
			{
				return value.Package;
			}
			if (instanceKey != null)
			{
				_entries.Remove(chest);
			}
			return null;
		}
	}
	public static class InventoryMove
	{
		public static MovePlan Plan(InventoryPackage? target, ItemRecord? item, int maxStack)
		{
			if (target == null || item == null || item.Stack <= 0 || !target.HasGrid)
			{
				return MovePlan.None;
			}
			int num = -1;
			int num2 = 0;
			if (maxStack > 1)
			{
				for (int i = 0; i < target.Count; i++)
				{
					ItemRecord itemRecord = target.Items[i];
					if (itemRecord.PrefabHash == item.PrefabHash && itemRecord.Quality == item.Quality && itemRecord.Variant == item.Variant && itemRecord.WorldLevel == item.WorldLevel)
					{
						int num3 = maxStack - itemRecord.Stack;
						if (num3 > num2)
						{
							num2 = num3;
							num = i;
						}