Decompiled source of QuickSort v0.1.6

QuickSort.dll

Decompiled 11 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ChatCommandAPI;
using GameNetcodeStuff;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("0Harmony")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: IgnoresAccessChecksTo("baer1.ChatCommandAPI")]
[assembly: IgnoresAccessChecksTo("BepInEx")]
[assembly: IgnoresAccessChecksTo("BepInEx.Harmony")]
[assembly: IgnoresAccessChecksTo("Unity.InputSystem")]
[assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")]
[assembly: IgnoresAccessChecksTo("UnityEngine.IMGUIModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.UI")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("QuickSort")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyDescription("made by Asta")]
[assembly: AssemblyFileVersion("1.2.1.0")]
[assembly: AssemblyInformationalVersion("1.2.1+066683a97cb60749303cd8f64e5af2e9d2e3aaac")]
[assembly: AssemblyProduct("QuickSort")]
[assembly: AssemblyTitle("QuickSort")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.2.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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 QuickSort
{
	public static class Chat
	{
		[HarmonyPatch(typeof(HUDManager), "SubmitChat_performed")]
		[HarmonyPrefix]
		[HarmonyWrapSafe]
		public static bool OnChatSubmit(HUDManager __instance, CallbackContext context)
		{
			if (!((CallbackContext)(ref context)).performed || !Player.Local.isTypingChat)
			{
				return true;
			}
			string text = __instance.chatTextField.text;
			if (text == "/help")
			{
				string text2 = "Commands: ";
				text2 += string.Join(", ", ChatCommand.Commands);
				Log.Chat(text2);
				CloseChat(__instance);
				return false;
			}
			if (text.StartsWith("/"))
			{
				text = text.Substring(1).Trim();
				foreach (ChatCommand command in ChatCommand.Commands)
				{
					if (text.StartsWith(command.keyword))
					{
						CloseChat(__instance);
						try
						{
							command.action(command.GetArgs(text));
						}
						catch (Exception e)
						{
							Log.Exception(e);
						}
						return false;
					}
				}
			}
			return true;
		}

		public static void CloseChat(HUDManager instance)
		{
			instance.localPlayer.isTypingChat = false;
			instance.chatTextField.text = "";
			EventSystem.current.SetSelectedGameObject((GameObject)null);
			((Behaviour)instance.typingIndicator).enabled = false;
		}
	}
	public abstract class Argument
	{
		public string name;
	}
	public class Argument<T> : Argument
	{
		public T value;

		public Argument(string name, T value)
		{
			base.name = name;
			this.value = value;
		}

		public static implicit operator T(Argument<T> arg)
		{
			return arg.value;
		}
	}
	public struct ChatArgs
	{
		public Argument[] arguments;

		public string help;

		public int Length => arguments.Length;

		public bool Empty => arguments.Length == 0;

		public bool this[string name] => Get<bool>(name);

		public string this[int name] => Get<string>(name);

		public T Get<T>(string name)
		{
			Argument[] array = arguments;
			Argument[] array2 = array;
			foreach (Argument argument in array2)
			{
				if (argument.name == name)
				{
					return ((Argument<T>)argument).value;
				}
			}
			return default(T);
		}

		public T Get<T>(int name)
		{
			Argument[] array = arguments;
			Argument[] array2 = array;
			foreach (Argument argument in array2)
			{
				if (argument.name == name.ToString())
				{
					return ((Argument<T>)argument).value;
				}
			}
			return default(T);
		}
	}
	public class ChatCommand
	{
		public static List<ChatCommand> Commands = new List<ChatCommand>();

		public string keyword;

		public Action<ChatArgs> action;

		public string help;

		public ChatCommand(string keyword, string help, Action<ChatArgs> action)
		{
			this.keyword = keyword;
			this.help = help;
			this.action = action;
		}

		public static ChatCommand New(string keyword, string help, Action<ChatArgs> action)
		{
			ChatCommand chatCommand = new ChatCommand(keyword, help, action);
			Commands.Add(chatCommand);
			return chatCommand;
		}

		public ChatArgs GetArgs(string raw)
		{
			List<Argument> list = new List<Argument>();
			string[] array = raw.Split(' ');
			keyword = array[0];
			for (int i = 1; i < array.Length; i++)
			{
				if (array[i].StartsWith("-"))
				{
					string name = array[i].Substring(1);
					list.Add(new Argument<bool>(name, value: true));
				}
				else
				{
					list.Add(new Argument<string>((i - 1).ToString(), array[i]));
				}
			}
			ChatArgs result = default(ChatArgs);
			result.arguments = list.ToArray();
			result.help = help;
			return result;
		}

		public override string ToString()
		{
			return keyword;
		}
	}
	public static class Extensions
	{
		public static string NormalizeName(string s)
		{
			if (string.IsNullOrWhiteSpace(s))
			{
				return "";
			}
			return s.ToLower().Replace(" ", "_").Replace("-", "_")
				.Trim();
		}

		public static string Name(this GrabbableObject item)
		{
			return NormalizeName(item.itemProperties.itemName);
		}

		public static string Name(this Item item)
		{
			return NormalizeName(item.itemName);
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "BeginGrabObject")]
	internal static class GrabPatch
	{
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator ilGenerator)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Expected O, but got Unknown
			//IL_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_0116: Expected O, but got Unknown
			//IL_0129: Unknown result type (might be due to invalid IL or missing references)
			//IL_012f: Expected O, but got Unknown
			//IL_014a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Expected O, but got Unknown
			//IL_0158: Unknown result type (might be due to invalid IL or missing references)
			//IL_015e: Expected O, but got Unknown
			//IL_01a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Expected O, but got Unknown
			//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ba: Expected O, but got Unknown
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			CodeMatcher val = new CodeMatcher((IEnumerable<CodeInstruction>)list, ilGenerator);
			CodeInstruction val2 = null;
			MethodInfo methodInfo = AccessTools.Method(typeof(NetworkBehaviour), "get_NetworkObject", (Type[])null, (Type[])null);
			for (int i = 0; i < list.Count - 1; i++)
			{
				CodeInstruction val3 = list[i];
				if (val3.opcode == OpCodes.Callvirt && val3.operand is MethodInfo methodInfo2 && methodInfo2 == methodInfo)
				{
					CodeInstruction val4 = list[i + 1];
					if (IsStloc(val4.opcode))
					{
						val2 = val4.Clone();
						break;
					}
				}
			}
			if (val2 == null)
			{
				val2 = new CodeInstruction(OpCodes.Stloc_0, (object)null);
			}
			Label label = default(Label);
			return val.MatchForward(true, (CodeMatch[])(object)new CodeMatch[1]
			{
				new CodeMatch((OpCode?)OpCodes.Callvirt, (object)AccessTools.Method(typeof(GrabbableObject), "InteractItem", (Type[])null, (Type[])null), (string)null)
			}).MatchBack(true, (CodeMatch[])(object)new CodeMatch[1]
			{
				new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null)
			}).Insert((CodeInstruction[])(object)new CodeInstruction[4]
			{
				new CodeInstruction(OpCodes.Ldarg_0, (object)null),
				new CodeInstruction(OpCodes.Ldfld, (object)AccessTools.Field(typeof(PlayerControllerB), "currentlyGrabbingObject")),
				new CodeInstruction(OpCodes.Callvirt, (object)methodInfo),
				val2
			})
				.ThrowIfInvalid("QuickSort: BeginGrabObject pattern not found (InteractItem)")
				.CreateLabel(ref label)
				.Start()
				.Insert((CodeInstruction[])(object)new CodeInstruction[2]
				{
					new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(Player), "OverrideGrabbingObject", (Type[])null, (Type[])null)),
					new CodeInstruction(OpCodes.Brtrue, (object)label)
				})
				.InstructionEnumeration();
		}

		private static bool IsStloc(OpCode op)
		{
			return op == OpCodes.Stloc || op == OpCodes.Stloc_S || op == OpCodes.Stloc_0 || op == OpCodes.Stloc_1 || op == OpCodes.Stloc_2 || op == OpCodes.Stloc_3;
		}
	}
	public class Log
	{
		private static ManualLogSource _log;

		public static void Init(ManualLogSource log)
		{
			_log = log;
		}

		public static void NotifyPlayer(string header, string body = "", bool isWarning = false)
		{
			if ((Object)(object)HUDManager.Instance != (Object)null)
			{
				HUDManager.Instance.DisplayTip(header, body, isWarning, false, "LC_Tip1");
			}
			Debug(header);
			Debug(body);
		}

		public static void Chat(string body, string color = "FFFFFF")
		{
			if ((Object)(object)HUDManager.Instance != (Object)null)
			{
				HUDManager.Instance.AddChatMessage("<color=#" + color + ">[Pasta] " + body + "</color>", "", -1, false);
			}
			Debug(body);
		}

		public static void ConfirmSound()
		{
			if ((Object)(object)HUDManager.Instance != (Object)null && (Object)(object)GameNetworkManager.Instance != (Object)null)
			{
				HUDManager.Instance.UIAudio.PlayOneShot(GameNetworkManager.Instance.buttonTuneSFX);
			}
		}

		public static void Exception(Exception e)
		{
			string message = e.Message;
			string stackTrace = e.StackTrace;
			_log.LogError((object)message);
			_log.LogError((object)stackTrace);
		}

		public static void Error(params object[] objects)
		{
			_log.LogError((object)string.Join(" ", objects));
		}

		public static void Warning(params object[] objects)
		{
			_log.LogWarning((object)string.Join(" ", objects));
		}

		public static void Info(params object[] objects)
		{
			_log.LogInfo((object)string.Join(" ", objects));
		}

		public static void Debug(params object[] objects)
		{
			_log.LogDebug((object)string.Join(" ", objects));
		}
	}
	internal static class MoveUtils
	{
		public static bool MoveItemOnShip(GrabbableObject item, Vector3 worldPos, int floorYRot = -1)
		{
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: 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_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)item == (Object)null)
			{
				return false;
			}
			PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			GameObject val2 = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)val2 == (Object)null)
			{
				return false;
			}
			Vector3 val3 = val2.transform.InverseTransformPoint(worldPos);
			((Component)item).transform.position = worldPos;
			val.SetObjectAsNoLongerHeld(true, true, val3, item, floorYRot);
			val.ThrowObjectServerRpc(NetworkObjectReference.op_Implicit(((NetworkBehaviour)item).NetworkObject), true, true, val3, floorYRot);
			return true;
		}

		public static bool MoveItemOnShipLocal(GrabbableObject item, Vector3 shipLocalPos, int floorYRot = -1)
		{
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: 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_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: 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)
			if ((Object)(object)item == (Object)null)
			{
				return false;
			}
			PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			GameObject val2 = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)val2 == (Object)null)
			{
				return false;
			}
			((Component)item).transform.position = val2.transform.TransformPoint(shipLocalPos);
			try
			{
				val.SetObjectAsNoLongerHeld(true, true, shipLocalPos, item, floorYRot);
			}
			catch
			{
			}
			try
			{
				val.PlaceGrabbableObject(val2.transform, shipLocalPos, false, item);
				val.PlaceObjectServerRpc(NetworkObjectReference.op_Implicit(((NetworkBehaviour)item).NetworkObject), NetworkObjectReference.op_Implicit(val2), shipLocalPos, false);
			}
			catch
			{
				try
				{
					val.ThrowObjectServerRpc(NetworkObjectReference.op_Implicit(((NetworkBehaviour)item).NetworkObject), true, true, shipLocalPos, floorYRot);
				}
				catch
				{
					return false;
				}
			}
			return true;
		}

		public static bool TryTeleportToWorld(GameObject go, Vector3 worldPos, Quaternion worldRot)
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: 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)
			GrabbableObject val = (((Object)(object)go != (Object)null) ? go.GetComponent<GrabbableObject>() : null);
			if ((Object)(object)val != (Object)null)
			{
				return MoveItemOnShip(val, worldPos, val.floorYRot);
			}
			if ((Object)(object)go == (Object)null)
			{
				return false;
			}
			go.transform.SetPositionAndRotation(worldPos, worldRot);
			return true;
		}
	}
	public static class Player
	{
		public static GrabbableObject overrideObject;

		public static PlayerControllerB Local => StartOfRound.Instance?.localPlayerController;

		public static bool CanGrabObject(GrabbableObject item)
		{
			if ((Object)(object)item == (Object)null || !item.grabbable || item.deactivated || item.isHeld || item.isPocketed)
			{
				return false;
			}
			if ((Object)(object)Local == (Object)null || Local.isPlayerDead || Local.isTypingChat || Local.inTerminalMenu || Local.throwingObject || Local.IsInspectingItem || Local.isGrabbingObjectAnimation || (Object)(object)Local.inAnimationWithEnemy != (Object)null || Local.inSpecialInteractAnimation || Local.jetpackControls || Local.disablingJetpackControls || Local.activatingItem || Local.waitingToDropItem || Local.FirstEmptyItemSlot() == -1)
			{
				return false;
			}
			return true;
		}

		public static bool OverrideGrabbingObject()
		{
			if ((Object)(object)overrideObject == (Object)null)
			{
				return false;
			}
			Local.currentlyGrabbingObject = overrideObject;
			overrideObject = null;
			return true;
		}

		public static IEnumerator StartGrabbingObject(GrabbableObject grabbableObject)
		{
			if (CanGrabObject(grabbableObject))
			{
				overrideObject = grabbableObject;
				Local.BeginGrabObject();
				yield return Local.grabObjectCoroutine;
			}
		}

		public static IEnumerator StartMovingObject(GrabbableObject item, Vector3 position, NetworkObject? parent = null)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			yield return StartGrabbingObject(item);
			if ((Object)(object)Local == (Object)null)
			{
				yield break;
			}
			int frames = 0;
			while (frames < 30 && ((Object)(object)Local.currentlyHeldObjectServer == (Object)null || (Object)(object)Local.currentlyHeldObjectServer != (Object)(object)item))
			{
				frames++;
				yield return null;
			}
			if ((Object)(object)Local.currentlyHeldObjectServer == (Object)null || (Object)(object)Local.currentlyHeldObjectServer != (Object)(object)item)
			{
				Log.Warning("Failed to grab " + (item?.itemProperties?.itemName ?? "item") + "; skipping move to avoid crash.");
				yield break;
			}
			try
			{
				item.floorYRot = -1;
				Local.DiscardHeldObject(true, parent, position, false);
			}
			catch (Exception e)
			{
				Log.Exception(e);
			}
		}
	}
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInPlugin("pasta.quicksort", "QuickSort", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		public static ManualLogSource Log;

		public static ConfigFile config;

		public static ConfigEntry<string> configVersion;

		private static Harmony harmony;

		public static GameObject sorterObject;

		public static Plugin Instance { get; private set; }

		private void Awake()
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Expected O, but got Unknown
			//IL_02b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ba: Expected O, but got Unknown
			//IL_01c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ca: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			config = ((BaseUnityPlugin)this).Config;
			ConfigEntry<string> val = default(ConfigEntry<string>);
			bool flag = config.TryGetEntry<string>(new ConfigDefinition("General", "configVersion"), ref val);
			configVersion = config.Bind<string>("General", "configVersion", "0.1.5", "Config schema version (used for internal migrations).");
			bool flag2 = false;
			string text = configVersion.Value ?? "";
			if (string.IsNullOrWhiteSpace(text))
			{
				text = "0.1.5";
				configVersion.Value = text;
				flag2 = true;
			}
			if (!flag || IsVersionLessThan(text, "0.1.5"))
			{
				ConfigEntry<float> val2 = config.Bind<float>("Sorter", "sortOriginY", 0.1f, "Y coordinate of the origin position for sorting items (relative to ship)");
				if (Mathf.Abs(val2.Value - 0.5f) < 0.0001f)
				{
					val2.Value = 0.1f;
					flag2 = true;
				}
				ConfigEntry<string> val3 = config.Bind<string>("Sorter", "skippedItems", "", "SCRAP skip list (comma-separated, substring match). Only applies to scrap items.");
				string text2 = AddTokensToCommaList(val3.Value, "shotgun", "ammo");
				if (!string.Equals(text2, val3.Value, StringComparison.Ordinal))
				{
					val3.Value = text2;
					flag2 = true;
				}
				configVersion.Value = "0.1.5";
				flag2 = true;
			}
			if (flag2)
			{
				config.Save();
			}
			QuickSort.Log.Init(((BaseUnityPlugin)this).Logger);
			QuickSort.Log.Info("QuickSort - Item Sorter loading...");
			SortShortcuts.EnsureFileExists();
			SortPositions.EnsureFileExists();
			harmony = new Harmony("pasta.quicksort");
			harmony.PatchAll(typeof(Ship));
			harmony.PatchAll(typeof(Startup));
			harmony.PatchAll(typeof(GrabPatch));
			try
			{
				new SortCommand();
				new SortBindCommand();
				new SortSetCommand();
				new SortResetCommand();
				new SortPositionsCommand();
				new SortBindingsListCommand();
				new SortSkipCommand();
				new PileCommand();
				QuickSort.Log.Info("Sort command registered in Awake");
			}
			catch (Exception ex)
			{
				QuickSort.Log.Error("Failed to register sort command in Awake: " + ex.Message);
				QuickSort.Log.Error("Stack trace: " + ex.StackTrace);
			}
			try
			{
				if ((Object)(object)sorterObject == (Object)null)
				{
					sorterObject = new GameObject("PastaSorter");
					sorterObject.AddComponent<Sorter>();
					Object.DontDestroyOnLoad((Object)(object)sorterObject);
					QuickSort.Log.Info("Sorter initialized in Awake");
				}
			}
			catch (Exception ex2)
			{
				QuickSort.Log.Error("Failed to initialize Sorter in Awake: " + ex2.Message);
				QuickSort.Log.Error("Stack trace: " + ex2.StackTrace);
			}
			QuickSort.Log.Info("QuickSort - Item Sorter loaded!");
		}

		private void OnDestroy()
		{
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			if ((Object)(object)sorterObject != (Object)null)
			{
				Object.Destroy((Object)(object)sorterObject);
			}
		}

		private static bool IsVersionLessThan(string a, string b)
		{
			int[] array = Parse(a);
			int[] array2 = Parse(b);
			for (int i = 0; i < 3; i++)
			{
				if (array[i] < array2[i])
				{
					return true;
				}
				if (array[i] > array2[i])
				{
					return false;
				}
			}
			return false;
			static int[] Parse(string s)
			{
				if (string.IsNullOrWhiteSpace(s))
				{
					return new int[3];
				}
				string[] array3 = s.Trim().Split('.');
				int[] array4 = new int[3];
				for (int j = 0; j < 3; j++)
				{
					if (j < array3.Length && int.TryParse(array3[j], out var result))
					{
						array4[j] = result;
					}
					else
					{
						array4[j] = 0;
					}
				}
				return array4;
			}
		}

		private static string AddTokensToCommaList(string? list, params string[] tokensToAdd)
		{
			List<string> tokens = new List<string>();
			HashSet<string> seen = new HashSet<string>();
			if (!string.IsNullOrWhiteSpace(list))
			{
				string[] array = list.Split(',');
				foreach (string raw2 in array)
				{
					Add(raw2);
				}
			}
			if (tokensToAdd != null)
			{
				foreach (string raw3 in tokensToAdd)
				{
					Add(raw3);
				}
			}
			return string.Join(", ", tokens);
			void Add(string raw)
			{
				string text = (raw ?? "").Trim();
				if (!string.IsNullOrWhiteSpace(text))
				{
					text = Extensions.NormalizeName(text).Trim('_');
					if (!string.IsNullOrWhiteSpace(text) && seen.Add(text))
					{
						tokens.Add(text);
					}
				}
			}
		}
	}
	public static class Startup
	{
		private static bool commandRegistered;

		[HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")]
		[HarmonyPostfix]
		private static void OnLocalPlayerCreated(PlayerControllerB __instance)
		{
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Expected O, but got Unknown
			if ((Object)(object)__instance != (Object)(object)StartOfRound.Instance.localPlayerController)
			{
				return;
			}
			if (!commandRegistered)
			{
				try
				{
					new SortCommand();
					new SortBindCommand();
					new SortSetCommand();
					new SortResetCommand();
					new SortPositionsCommand();
					new SortBindingsListCommand();
					new SortSkipCommand();
					new PileCommand();
					commandRegistered = true;
					Log.Info("Sort command registered");
				}
				catch (Exception ex)
				{
					Log.Error("Failed to register sort command: " + ex.Message);
				}
			}
			if ((Object)(object)Plugin.sorterObject != (Object)null)
			{
				Object.Destroy((Object)(object)Plugin.sorterObject);
			}
			Plugin.sorterObject = new GameObject("PastaSorter");
			Plugin.sorterObject.AddComponent<Sorter>();
			Object.DontDestroyOnLoad((Object)(object)Plugin.sorterObject);
		}
	}
	public static class Ship
	{
		public static Action OnShipOrbit;

		public static Action OnShipTouchdown;

		public static Action OnShipAscent;

		public static Action OnShipDescent;

		private static readonly Dictionary<string, FieldInfo?> _startOfRoundBoolFields = new Dictionary<string, FieldInfo>();

		public static bool Stationary
		{
			get
			{
				if (GetStartOfRoundBool("shipHasLanded").GetValueOrDefault())
				{
					return true;
				}
				if (GetStartOfRoundBool("inShipPhase").GetValueOrDefault() && !GetStartOfRoundBool("shipIsLeaving").GetValueOrDefault())
				{
					return true;
				}
				return false;
			}
		}

		public static bool InOrbit => GetStartOfRoundBool("inShipPhase").GetValueOrDefault() && !GetStartOfRoundBool("shipHasLanded").GetValueOrDefault();

		private static bool? GetStartOfRoundBool(string fieldName)
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return null;
			}
			if (!_startOfRoundBoolFields.TryGetValue(fieldName, out FieldInfo value))
			{
				value = AccessTools.Field(((object)instance).GetType(), fieldName);
				_startOfRoundBoolFields[fieldName] = value;
			}
			if (value == null)
			{
				return null;
			}
			try
			{
				object value2 = value.GetValue(instance);
				if (value2 is bool)
				{
					bool value3 = (bool)value2;
					if (true)
					{
						return value3;
					}
				}
			}
			catch
			{
			}
			return null;
		}

		[HarmonyPatch(typeof(StartOfRound), "ShipLeave")]
		[HarmonyPostfix]
		[HarmonyWrapSafe]
		public static void OnShipLeave()
		{
			OnShipAscent?.Invoke();
			OnShipDescent?.Invoke();
		}

		[HarmonyPatch(typeof(StartOfRound), "OnShipLandedMiscEvents")]
		[HarmonyPostfix]
		[HarmonyWrapSafe]
		public static void OnShipLanded()
		{
			OnShipTouchdown?.Invoke();
			OnShipOrbit?.Invoke();
		}
	}
	public class Sorter : MonoBehaviour
	{
		private static readonly HashSet<string> LowerYOffsetTypes = new HashSet<string> { "toilet_paper", "chemical_jug", "cash_register", "fancy_lamp", "large_axle", "v_type_engine" };

		public ConfigEntry<float> sortOriginX;

		public ConfigEntry<float> sortOriginY;

		public ConfigEntry<float> sortOriginZ;

		public ConfigEntry<float> itemSpacing;

		public ConfigEntry<float> rowSpacing;

		public ConfigEntry<int> itemsPerRow;

		public ConfigEntry<string> skippedItems;

		public ConfigEntry<float> sortAreaWidth;

		public ConfigEntry<float> sortAreaDepth;

		public ConfigEntry<float> wallPadding;

		public ConfigEntry<bool> stackSameTypeTogether;

		public ConfigEntry<float> sameTypeStackStepY;

		private List<GrabbableObject> scrap;

		public static bool inProgress;

		private Vector3 SortOrigin => new Vector3(sortOriginX.Value, sortOriginY.Value, sortOriginZ.Value);

		private bool CanSort => Ship.InOrbit || Ship.Stationary;

		private static bool ShouldLowerYOffset(GrabbableObject item)
		{
			if ((Object)(object)item == (Object)null)
			{
				return false;
			}
			string text = item.Name();
			if (string.IsNullOrWhiteSpace(text))
			{
				return false;
			}
			return LowerYOffsetTypes.Contains(text);
		}

		private static string ApplyDefaultInputAliases(string normalizedKey)
		{
			if (string.IsNullOrWhiteSpace(normalizedKey))
			{
				return normalizedKey;
			}
			if (1 == 0)
			{
			}
			string result = ((normalizedKey == "double_barrel") ? "shotgun" : ((!(normalizedKey == "shotgun_shell")) ? normalizedKey : "ammo"));
			if (1 == 0)
			{
			}
			return result;
		}

		private void Awake()
		{
			sortOriginX = Plugin.config.Bind<float>("Sorter", "sortOriginX", -2.8f, "X coordinate of the origin position for sorting items (relative to ship)");
			sortOriginY = Plugin.config.Bind<float>("Sorter", "sortOriginY", 0.1f, "Y coordinate of the origin position for sorting items (relative to ship)");
			sortOriginZ = Plugin.config.Bind<float>("Sorter", "sortOriginZ", -4.8f, "Z coordinate of the origin position for sorting items (relative to ship)");
			itemSpacing = Plugin.config.Bind<float>("Sorter", "itemSpacing", 0.8f, "Spacing between items horizontally");
			rowSpacing = Plugin.config.Bind<float>("Sorter", "rowSpacing", 0.8f, "Spacing between rows vertically");
			itemsPerRow = Plugin.config.Bind<int>("Sorter", "itemsPerRow", 9, "Number of items per row");
			skippedItems = Plugin.config.Bind<string>("Sorter", "skippedItems", "body, clipboard, sticky_note, boombox, shovel, jetpack, flashlight, pro_flashlight, key, stun_grenade, lockpicker, mapper, extension_ladder, tzp_inhalant, walkie_talkie, zap_gun, kitchen_knife, weed_killer, radar_booster, spray_paint, belt_bag, shotgun, ammo", "SCRAP skip list (comma-separated, substring match). Only applies to scrap items.");
			skippedItems.Value = NormalizeSkipListConfig(skippedItems.Value);
			sortAreaWidth = Plugin.config.Bind<float>("Sorter", "sortAreaWidth", 9f, "Reserved: used only for future bounding. (Keeping for compatibility)");
			sortAreaDepth = Plugin.config.Bind<float>("Sorter", "sortAreaDepth", 6f, "How far forward (Z) types are allowed to expand from sortOriginZ before starting a new type-layer above.");
			wallPadding = Plugin.config.Bind<float>("Sorter", "wallPadding", 0.25f, "Padding used for depth calculation (prevents placing type rows into doors/walls).");
			stackSameTypeTogether = Plugin.config.Bind<bool>("Sorter", "stackSameTypeTogether", true, "If true, items of the same type will be stacked at the exact same X/Z position (only Y increases), instead of being spread in a small grid.");
			sameTypeStackStepY = Plugin.config.Bind<float>("Sorter", "sameTypeStackStepY", 0f, "Vertical spacing between items when stackSameTypeTogether is enabled. Set to 0 for exact overlap; increase (e.g. 0.1~0.2) if physics makes items push apart.");
		}

		private void Update()
		{
			if (inProgress && ((ButtonControl)Keyboard.current.escapeKey).wasPressedThisFrame)
			{
				inProgress = false;
				Log.Chat("Sorting cancelled", "FF0000");
			}
		}

		private void SortCommandHandler(string[] args)
		{
			if (args.Length != 0 && args[0] == "help")
			{
				Log.Chat("Usage: /sort [help]", "FFFF00");
				return;
			}
			if (!CanSort)
			{
				Log.NotifyPlayer("Sorter Error", "Must be in orbit or stationary at company", isWarning: true);
				return;
			}
			if (inProgress)
			{
				Log.NotifyPlayer("Sorter Error", "Operation in progress", isWarning: true);
				return;
			}
			CategorizeItems();
			Log.ConfirmSound();
			((MonoBehaviour)this).StartCoroutine(SortItems(force: false));
		}

		public IEnumerator SortItems(bool force, bool ignoreSkippedItems = false, bool includeSavedPositionTypesEvenIfSkipped = false)
		{
			inProgress = true;
			Log.Chat("Press [Escape] to cancel sorting", "FFFF00");
			if ((Object)(object)Player.Local == (Object)null)
			{
				Log.NotifyPlayer("Sorter Error", "Local player not ready yet", isWarning: true);
				inProgress = false;
				yield break;
			}
			GameObject ship = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)ship == (Object)null)
			{
				Log.NotifyPlayer("Sorter Error", "Ship not found", isWarning: true);
				inProgress = false;
				yield break;
			}
			Vector3 originLocal = SortOrigin;
			HashSet<string> savedTypes = null;
			string posListError;
			List<(string itemKey, Vector3 shipLocalPos)> savedPositions = SortPositions.ListAll(out posListError);
			if (posListError != null)
			{
				Log.Warning(posListError);
			}
			else
			{
				savedTypes = new HashSet<string>(savedPositions.Select<(string, Vector3), string>(((string itemKey, Vector3 shipLocalPos) p) => p.itemKey));
			}
			Dictionary<string, List<GrabbableObject>> groupedItems = new Dictionary<string, List<GrabbableObject>>();
			foreach (GrabbableObject item in scrap)
			{
				string itemName = item.Name();
				bool ignoreSkipTokensForThisItem = ignoreSkippedItems || (includeSavedPositionTypesEvenIfSkipped && savedTypes != null && savedTypes.Contains(itemName));
				if (!ShouldSkipFullSort(item, ignoreSkipTokensForThisItem))
				{
					if (!groupedItems.ContainsKey(itemName))
					{
						groupedItems[itemName] = new List<GrabbableObject>();
					}
					groupedItems[itemName].Add(item);
				}
			}
			List<KeyValuePair<string, List<GrabbableObject>>> orderedGroups = (from kvp in groupedItems
				orderby kvp.Value != null && kvp.Value.Any(IsTwoHandedItem) descending, kvp.Key
				select kvp).ToList();
			List<string> orderedTypeNames = orderedGroups.Select((KeyValuePair<string, List<GrabbableObject>> kvp) => kvp.Key).ToList();
			HashSet<string> reservedTypes = savedTypes;
			Dictionary<string, Vector3> layout = CreateLayout(orderedTypeNames, reservedTypes);
			RaycastHit hitCenter = default(RaycastHit);
			foreach (KeyValuePair<string, List<GrabbableObject>> group in orderedGroups)
			{
				string itemName2 = group.Key;
				List<GrabbableObject> items = group.Value;
				Vector3 typePos = (layout.ContainsKey(itemName2) ? layout[itemName2] : Vector3.zero);
				Vector3 customShipLocal;
				string posError;
				bool hasCustomPos = SortPositions.TryGet(itemName2, out customShipLocal, out posError);
				if (posError != null)
				{
					Log.Warning(posError);
				}
				bool ignoreSkipTokensForThisType = includeSavedPositionTypesEvenIfSkipped && hasCustomPos;
				Vector3 pileCenterLocal = (Vector3)(hasCustomPos ? new Vector3(customShipLocal.x, 0f, customShipLocal.z) : (originLocal + new Vector3(typePos.x, 0f, typePos.z)));
				float customYOffset = (hasCustomPos ? customShipLocal.y : 0f);
				float typeLayerYOffset = (hasCustomPos ? 0f : typePos.y);
				float originYOffset = (hasCustomPos ? 0f : originLocal.y);
				float groundYLocal = pileCenterLocal.y;
				Vector3 rayStartCenter = ship.transform.TransformPoint(pileCenterLocal + Vector3.up * 2f);
				if (Physics.Raycast(rayStartCenter, Vector3.down, ref hitCenter, 80f, 268437761, (QueryTriggerInteraction)1))
				{
					groundYLocal = ship.transform.InverseTransformPoint(((RaycastHit)(ref hitCenter)).point).y;
				}
				hitCenter = default(RaycastHit);
				for (int stackIndex = 0; stackIndex < items.Count; stackIndex++)
				{
					GrabbableObject item2 = items[stackIndex];
					if (ShouldBreak(item2))
					{
						Log.NotifyPlayer("Sorter Stopping", "Operation cancelled or ship is in motion", isWarning: true);
						inProgress = false;
						yield break;
					}
					float pileX = 0f;
					float pileZ = 0f;
					float pileY;
					if (stackSameTypeTogether.Value)
					{
						pileY = (float)stackIndex * Mathf.Max(0f, sameTypeStackStepY.Value);
					}
					else
					{
						int cols = Mathf.Max(1, itemsPerRow.Value);
						int rows = cols;
						int perLayer = cols * rows;
						int layer = stackIndex / perLayer;
						int inLayer = stackIndex % perLayer;
						int r = inLayer / cols;
						int c = inLayer % cols;
						pileX = ((float)c - (float)(cols - 1) / 2f) * 0.1f;
						pileZ = ((float)r - (float)(rows - 1) / 2f) * 0.1f;
						pileY = (float)layer * 0.07f;
					}
					Vector3 targetLocal = new Vector3(pileCenterLocal.x + pileX, groundYLocal + originYOffset + (item2.itemProperties.verticalOffset - 0.1f) + (typeLayerYOffset + customYOffset) + pileY - (ShouldLowerYOffset(item2) ? 0.2f : 0f), pileCenterLocal.z + pileZ);
					Vector3 worldPos = ship.transform.TransformPoint(targetLocal);
					if (!force && Vector3.Distance(worldPos, ((Component)item2).transform.position) < 0.25f)
					{
						continue;
					}
					yield return GrabbableRetry(item2);
					if (!ShouldSkipFullSort(item2, ignoreSkippedItems || ignoreSkipTokensForThisType))
					{
						item2.floorYRot = -1;
						if (!MoveUtils.MoveItemOnShipLocal(item2, targetLocal, item2.floorYRot))
						{
							Log.Warning("Failed to move " + (item2.itemProperties?.itemName ?? ((Object)item2).name));
						}
						int retry = 15;
						while (!Player.CanGrabObject(item2) && retry > 0)
						{
							yield return (object)new WaitForEndOfFrame();
							retry--;
						}
					}
				}
				customShipLocal = default(Vector3);
				posError = null;
			}
			Log.Chat("Sorting complete!", "00FF00");
			inProgress = false;
		}

		private static bool IsTwoHandedItem(GrabbableObject item)
		{
			try
			{
				if ((Object)(object)item == (Object)null)
				{
					return false;
				}
				Item itemProperties = item.itemProperties;
				if ((Object)(object)itemProperties == (Object)null)
				{
					return false;
				}
				Type type = ((object)itemProperties).GetType();
				FieldInfo fieldInfo = type.GetField("twoHanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("isTwoHanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("twoHandedItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("<twoHanded>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("<isTwoHanded>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("<twoHandedItem>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (fieldInfo != null && fieldInfo.FieldType == typeof(bool))
				{
					return (bool)fieldInfo.GetValue(itemProperties);
				}
				PropertyInfo propertyInfo = type.GetProperty("twoHanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetProperty("isTwoHanded", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetProperty("twoHandedItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (propertyInfo != null && propertyInfo.PropertyType == typeof(bool) && propertyInfo.GetIndexParameters().Length == 0)
				{
					return (bool)propertyInfo.GetValue(itemProperties, null);
				}
				FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (FieldInfo fieldInfo2 in fields)
				{
					if (!(fieldInfo2.FieldType != typeof(bool)) && fieldInfo2.Name.IndexOf("twohand", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						return (bool)fieldInfo2.GetValue(itemProperties);
					}
				}
				PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (PropertyInfo propertyInfo2 in properties)
				{
					if (!(propertyInfo2.PropertyType != typeof(bool)) && propertyInfo2.GetIndexParameters().Length == 0 && propertyInfo2.Name.IndexOf("twohand", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						return (bool)propertyInfo2.GetValue(itemProperties, null);
					}
				}
				return false;
			}
			catch
			{
				return false;
			}
		}

		public bool TryStartGatherByQuery(string query, bool force, out string? error)
		{
			//IL_0147: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0174: Unknown result type (might be due to invalid IL or missing references)
			//IL_0182: Unknown result type (might be due to invalid IL or missing references)
			error = null;
			if (!CanSort)
			{
				error = "Must be in orbit or stationary at company";
				return false;
			}
			if (inProgress)
			{
				error = "Operation in progress";
				return false;
			}
			if (string.IsNullOrWhiteSpace(query))
			{
				error = "Missing item name";
				return false;
			}
			CategorizeItems(includeSkippedItems: true);
			if (scrap == null || scrap.Count == 0)
			{
				error = "No items found in ship";
				return false;
			}
			Dictionary<string, List<GrabbableObject>> dictionary = new Dictionary<string, List<GrabbableObject>>();
			foreach (GrabbableObject item in scrap)
			{
				if (!ShouldSkipExplicitQuery(item))
				{
					string key = item.Name();
					if (!dictionary.TryGetValue(key, out var value))
					{
						value = (dictionary[key] = new List<GrabbableObject>());
					}
					value.Add(item);
				}
			}
			if (!TryResolveItemKeyFromGrouped(dictionary, query, out string resolvedKey, out error))
			{
				return false;
			}
			if (!TryGetPlayerShipLocalTarget(out var shipLocalTarget, out error))
			{
				return false;
			}
			if (!TryGetGroundYLocalAt(shipLocalTarget, out float groundYLocal, out string error2))
			{
				error = error2;
				return false;
			}
			Vector3 targetCenterShipLocal = default(Vector3);
			((Vector3)(ref targetCenterShipLocal))..ctor(shipLocalTarget.x, shipLocalTarget.y - groundYLocal, shipLocalTarget.z);
			((MonoBehaviour)this).StartCoroutine(MoveItemsOfTypeToPosition(resolvedKey, targetCenterShipLocal, force, announce: true, ignoreSkipLists: true, applyTwoHandedSortYOffset: true));
			return true;
		}

		public bool TryStartPileByQueryOrHeld(string? queryOrNull, bool force, out string? error)
		{
			//IL_01ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_020f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0216: Unknown result type (might be due to invalid IL or missing references)
			//IL_0220: Unknown result type (might be due to invalid IL or missing references)
			//IL_022f: Unknown result type (might be due to invalid IL or missing references)
			error = null;
			if (!CanSort)
			{
				error = "Must be in orbit or stationary at company";
				return false;
			}
			if (inProgress)
			{
				error = "Operation in progress";
				return false;
			}
			GrabbableObject val = (((Object)(object)Player.Local != (Object)null) ? Player.Local.currentlyHeldObjectServer : null);
			string text = ((!string.IsNullOrWhiteSpace(queryOrNull)) ? queryOrNull : (((Object)(object)val != (Object)null) ? val.Name() : ""));
			if (string.IsNullOrWhiteSpace(text))
			{
				error = "Missing item name (hold the item or provide a name).";
				return false;
			}
			CategorizeItems(includeSkippedItems: true);
			Dictionary<string, List<GrabbableObject>> dictionary = new Dictionary<string, List<GrabbableObject>>();
			if (scrap != null)
			{
				foreach (GrabbableObject item in scrap)
				{
					if (!ShouldSkipExplicitQuery(item))
					{
						string key = item.Name();
						if (!dictionary.TryGetValue(key, out var value))
						{
							value = (dictionary[key] = new List<GrabbableObject>());
						}
						value.Add(item);
					}
				}
			}
			if ((Object)(object)val != (Object)null && !ShouldSkipExplicitQuery(val))
			{
				string key2 = val.Name();
				if (!dictionary.TryGetValue(key2, out var value2))
				{
					value2 = (dictionary[key2] = new List<GrabbableObject>());
				}
				if (!value2.Contains(val))
				{
					value2.Add(val);
				}
			}
			if (dictionary.Count == 0)
			{
				error = "No items found in ship";
				return false;
			}
			if (!TryResolveItemKeyFromGrouped(dictionary, text, out string resolvedKey, out error))
			{
				return false;
			}
			if (!TryGetPlayerShipLocalTarget(out var shipLocalTarget, out error))
			{
				return false;
			}
			if (!TryGetGroundYLocalAt(shipLocalTarget, out float groundYLocal, out string error2))
			{
				error = error2;
				return false;
			}
			Vector3 targetCenterShipLocal = default(Vector3);
			((Vector3)(ref targetCenterShipLocal))..ctor(shipLocalTarget.x, shipLocalTarget.y - groundYLocal, shipLocalTarget.z);
			((MonoBehaviour)this).StartCoroutine(MoveItemsOfTypeToPosition(resolvedKey, targetCenterShipLocal, force, announce: true, ignoreSkipLists: true));
			return true;
		}

		public bool TrySetAndMoveTypeToPlayer(string? queryOrNull, bool force, out string resolvedItemKey, out string? error)
		{
			//IL_0259: Unknown result type (might be due to invalid IL or missing references)
			//IL_0278: Unknown result type (might be due to invalid IL or missing references)
			//IL_027e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0286: Unknown result type (might be due to invalid IL or missing references)
			//IL_0293: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b4: Unknown result type (might be due to invalid IL or missing references)
			resolvedItemKey = "";
			error = null;
			if (!CanSort)
			{
				error = "Must be in orbit or stationary at company";
				return false;
			}
			if (inProgress)
			{
				error = "Operation in progress";
				return false;
			}
			GrabbableObject val = (((Object)(object)Player.Local != (Object)null) ? Player.Local.currentlyHeldObjectServer : null);
			if (!string.IsNullOrWhiteSpace(queryOrNull))
			{
				CategorizeItems(includeSkippedItems: true);
				if ((scrap == null || scrap.Count == 0) && (Object)(object)val == (Object)null)
				{
					error = "No items found in ship to match that name (hold the item or omit the name).";
					return false;
				}
				Dictionary<string, List<GrabbableObject>> dictionary = new Dictionary<string, List<GrabbableObject>>();
				if (scrap != null)
				{
					foreach (GrabbableObject item in scrap)
					{
						if (!ShouldSkipExplicitQuery(item))
						{
							string key = item.Name();
							if (!dictionary.TryGetValue(key, out var value))
							{
								value = (dictionary[key] = new List<GrabbableObject>());
							}
							value.Add(item);
						}
					}
				}
				if ((Object)(object)val != (Object)null && !ShouldSkipExplicitQuery(val))
				{
					string key2 = val.Name();
					if (!dictionary.TryGetValue(key2, out var value2))
					{
						value2 = (dictionary[key2] = new List<GrabbableObject>());
					}
					if (!value2.Contains(val))
					{
						value2.Add(val);
					}
				}
				if (dictionary.Count == 0)
				{
					error = "No valid items found to match that name.";
					return false;
				}
				if (!TryResolveItemKeyFromGrouped(dictionary, queryOrNull, out resolvedItemKey, out error))
				{
					return false;
				}
			}
			else
			{
				if ((Object)(object)val == (Object)null)
				{
					error = "No item name provided and no held item.";
					return false;
				}
				resolvedItemKey = val.Name();
			}
			if (string.IsNullOrWhiteSpace(resolvedItemKey))
			{
				error = "Invalid item name";
				return false;
			}
			if (!TryGetPlayerShipLocalTarget(out var shipLocalTarget, out error))
			{
				return false;
			}
			if (!TryGetGroundYLocalAt(shipLocalTarget, out float groundYLocal, out string error2))
			{
				error = error2;
				return false;
			}
			Vector3 val2 = default(Vector3);
			((Vector3)(ref val2))..ctor(shipLocalTarget.x, shipLocalTarget.y - groundYLocal, shipLocalTarget.z);
			if (!SortPositions.Set(resolvedItemKey, val2, out error))
			{
				return false;
			}
			Log.ConfirmSound();
			((MonoBehaviour)this).StartCoroutine(MoveItemsOfTypeToPosition(resolvedItemKey, val2, force, announce: true, ignoreSkipLists: true));
			return true;
		}

		private bool TryResolveItemKeyFromGrouped(Dictionary<string, List<GrabbableObject>> grouped, string query, out string resolvedKey, out string? error)
		{
			resolvedKey = "";
			error = null;
			string q = ApplyDefaultInputAliases(Extensions.NormalizeName(query));
			if (string.IsNullOrWhiteSpace(q))
			{
				error = "Invalid item name";
				return false;
			}
			if (grouped.ContainsKey(q))
			{
				resolvedKey = q;
				return true;
			}
			List<string> source = grouped.Keys.ToList();
			string qLoose = q.Replace("_", "");
			List<string> list = source.Where(delegate(string k)
			{
				if (k.Contains(q) || q.Contains(k))
				{
					return true;
				}
				string text2 = k.Replace("_", "");
				return text2.Contains(qLoose) || qLoose.Contains(text2);
			}).Distinct().ToList();
			if (list.Count == 1)
			{
				resolvedKey = list[0];
				return true;
			}
			if (list.Count == 0)
			{
				error = "No item match for '" + query + "'.";
				return false;
			}
			string text = string.Join(", ", list.Take(8));
			if (list.Count > 8)
			{
				text += ", ...";
			}
			error = "Ambiguous item '" + query + "'. Matches: " + text;
			return false;
		}

		private bool TryGetPlayerShipLocalTarget(out Vector3 shipLocalTarget, out string? error)
		{
			//IL_0002: 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_0063: 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_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			shipLocalTarget = default(Vector3);
			error = null;
			if ((Object)(object)Player.Local == (Object)null)
			{
				error = "Local player not ready yet";
				return false;
			}
			GameObject val = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)val == (Object)null)
			{
				error = "Ship not found";
				return false;
			}
			Vector3 val2 = ((Component)Player.Local).transform.position + ((Component)Player.Local).transform.forward * 0.75f;
			shipLocalTarget = val.transform.InverseTransformPoint(val2);
			return true;
		}

		private bool TryGetGroundYLocalAt(Vector3 shipLocalXZ, out float groundYLocal, out string? error)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: 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_0066: 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_006c: 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_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			groundYLocal = 0f;
			error = null;
			GameObject val = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)val == (Object)null)
			{
				error = "Ship not found";
				return false;
			}
			Vector3 val2 = default(Vector3);
			((Vector3)(ref val2))..ctor(shipLocalXZ.x, 0f, shipLocalXZ.z);
			Vector3 val3 = val.transform.TransformPoint(val2 + Vector3.up * 2f);
			RaycastHit val4 = default(RaycastHit);
			if (Physics.Raycast(val3, Vector3.down, ref val4, 80f, 268437761, (QueryTriggerInteraction)1))
			{
				groundYLocal = val.transform.InverseTransformPoint(((RaycastHit)(ref val4)).point).y;
				return true;
			}
			groundYLocal = 0f;
			return true;
		}

		private IEnumerator MoveItemsOfTypeToPosition(string itemKey, Vector3 targetCenterShipLocal, bool force, bool announce, bool ignoreSkipLists = false, bool applyTwoHandedSortYOffset = false)
		{
			//IL_0015: 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)
			inProgress = true;
			if (announce)
			{
				Log.Chat("Moving '" + itemKey + "' to your position... Press [Escape] to cancel", "FFFF00");
			}
			if ((Object)(object)Player.Local == (Object)null)
			{
				Log.NotifyPlayer("Sorter Error", "Local player not ready yet", isWarning: true);
				inProgress = false;
				yield break;
			}
			GameObject ship = GameObject.Find("Environment/HangarShip");
			if ((Object)(object)ship == (Object)null)
			{
				Log.NotifyPlayer("Sorter Error", "Ship not found", isWarning: true);
				inProgress = false;
				yield break;
			}
			Dictionary<string, List<GrabbableObject>> groupedItems = new Dictionary<string, List<GrabbableObject>>();
			foreach (GrabbableObject item in scrap)
			{
				if (!(ignoreSkipLists ? ShouldSkipExplicitQuery(item) : ShouldSkip(item)))
				{
					string name = item.Name();
					if (!groupedItems.TryGetValue(name, out List<GrabbableObject> list))
					{
						list = (groupedItems[name] = new List<GrabbableObject>());
					}
					list.Add(item);
					list = null;
				}
			}
			if (!groupedItems.TryGetValue(itemKey, out List<GrabbableObject> items) || items.Count == 0)
			{
				items = new List<GrabbableObject>();
			}
			Vector3 pileCenterLocal = new Vector3(targetCenterShipLocal.x, 0f, targetCenterShipLocal.z);
			float extraYOffset = targetCenterShipLocal.y;
			float groundYLocal = pileCenterLocal.y;
			Vector3 rayStartCenter = ship.transform.TransformPoint(pileCenterLocal + Vector3.up * 2f);
			RaycastHit hitCenter = default(RaycastHit);
			if (Physics.Raycast(rayStartCenter, Vector3.down, ref hitCenter, 80f, 268437761, (QueryTriggerInteraction)1))
			{
				groundYLocal = ship.transform.InverseTransformPoint(((RaycastHit)(ref hitCenter)).point).y;
			}
			hitCenter = default(RaycastHit);
			GrabbableObject held = (((Object)(object)Player.Local != (Object)null) ? Player.Local.currentlyHeldObjectServer : null);
			List<GrabbableObject> itemsToMove = new List<GrabbableObject>(items);
			if ((Object)(object)held != (Object)null && held.Name() == itemKey && !itemsToMove.Contains(held))
			{
				itemsToMove.Insert(0, held);
			}
			int moved = 0;
			for (int stackIndex = 0; stackIndex < itemsToMove.Count; stackIndex++)
			{
				GrabbableObject item2 = itemsToMove[stackIndex];
				if (ShouldBreak(item2))
				{
					Log.NotifyPlayer("Sorter Stopping", "Operation cancelled or ship is in motion", isWarning: true);
					inProgress = false;
					yield break;
				}
				float pileX = 0f;
				float pileZ = 0f;
				float pileY;
				if (stackSameTypeTogether.Value)
				{
					pileY = (float)stackIndex * Mathf.Max(0f, sameTypeStackStepY.Value);
				}
				else
				{
					int cols = Mathf.Max(1, itemsPerRow.Value);
					int rows = cols;
					int perLayer = cols * rows;
					int layer = stackIndex / perLayer;
					int inLayer = stackIndex % perLayer;
					int r = inLayer / cols;
					int c = inLayer % cols;
					pileX = ((float)c - (float)(cols - 1) / 2f) * 0.1f;
					pileZ = ((float)r - (float)(rows - 1) / 2f) * 0.1f;
					pileY = (float)layer * 0.07f;
				}
				Vector3 targetLocal = new Vector3(pileCenterLocal.x + pileX, groundYLocal + (item2.itemProperties.verticalOffset - 0.05f) + extraYOffset + pileY, pileCenterLocal.z + pileZ);
				Vector3 worldPos = ship.transform.TransformPoint(targetLocal);
				if (!force && Vector3.Distance(worldPos, ((Component)item2).transform.position) < 0.25f)
				{
					continue;
				}
				if ((Object)(object)held != (Object)null && (Object)(object)item2 == (Object)(object)held)
				{
					item2.floorYRot = -1;
					MoveUtils.MoveItemOnShipLocal(item2, targetLocal, item2.floorYRot);
					moved++;
					yield return null;
					continue;
				}
				yield return GrabbableRetry(item2);
				if (!(ignoreSkipLists ? ShouldSkipExplicitQuery(item2) : ShouldSkip(item2)))
				{
					item2.floorYRot = -1;
					if (!MoveUtils.MoveItemOnShipLocal(item2, targetLocal, item2.floorYRot))
					{
						Log.Warning("Failed to move " + (item2.itemProperties?.itemName ?? ((Object)item2).name));
					}
					int retry = 15;
					while (!Player.CanGrabObject(item2) && retry > 0)
					{
						yield return (object)new WaitForEndOfFrame();
						retry--;
					}
					moved++;
				}
			}
			Log.Chat($"Moved '{itemKey}' ({moved} items)", "00FF00");
			inProgress = false;
		}

		private Dictionary<string, Vector3> CreateLayout(List<string> itemNames, HashSet<string>? reservedTypes = null)
		{
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<string, Vector3> dictionary = new Dictionary<string, Vector3>();
			int num = Mathf.Max(1, itemsPerRow.Value);
			int num2 = 0;
			for (int i = 0; i < itemNames.Count; i++)
			{
				string text = itemNames[i];
				if (reservedTypes != null && reservedTypes.Contains(text))
				{
					num2++;
					continue;
				}
				int num3 = i - num2;
				int num4 = num3 / num;
				int num5 = num3 % num;
				float num6 = (float)(num - 1) * 0.5f;
				float num7 = ((float)num5 - num6) * itemSpacing.Value;
				float num8 = 0f;
				float num9 = (float)num4 * rowSpacing.Value + 0.05f;
				dictionary[text] = new Vector3(num7, num9, num8);
			}
			return dictionary;
		}

		public void CategorizeItems(bool includeSkippedItems = false)
		{
			scrap = new List<GrabbableObject>();
			GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>();
			GrabbableObject[] array2 = array;
			foreach (GrabbableObject val in array2)
			{
				if (!(includeSkippedItems ? ShouldSkipExplicitQuery(val) : ShouldSkip(val)) && val.isInShipRoom)
				{
					scrap.Add(val);
				}
			}
			scrap = (from item in scrap
				orderby item.scrapValue, item.Name()
				select item).ToList();
		}

		private IEnumerator GrabbableRetry(GrabbableObject item)
		{
			int retry = 15;
			while (!Player.CanGrabObject(item) && retry > 0)
			{
				yield return (object)new WaitForEndOfFrame();
				retry--;
			}
		}

		private bool ShouldBreak(GrabbableObject item)
		{
			return !inProgress || !Ship.Stationary || ((Object)(object)Player.Local != (Object)null && (Player.Local.beamOutParticle.isPlaying || Player.Local.beamUpParticle.isPlaying));
		}

		private bool ShouldSkipExplicitQuery(GrabbableObject item)
		{
			if ((Object)(object)item == (Object)null)
			{
				return true;
			}
			if (!item.grabbable || item.deactivated || item.isHeld || item.isPocketed)
			{
				return true;
			}
			if (item.Name() == "body")
			{
				return true;
			}
			if (!item.isInShipRoom)
			{
				return true;
			}
			return false;
		}

		private static string NormalizeSkipListConfig(string list)
		{
			if (string.IsNullOrWhiteSpace(list))
			{
				return list ?? "";
			}
			HashSet<string> hashSet = new HashSet<string>();
			List<string> list2 = new List<string>();
			string[] array = list.Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i]?.Trim() ?? "";
				if (!string.IsNullOrWhiteSpace(text))
				{
					text = NormalizeSkipToken(text);
					if (text == "rader_booster")
					{
						text = "radar_booster";
					}
					if (!string.IsNullOrWhiteSpace(text) && hashSet.Add(text))
					{
						list2.Add(text);
					}
				}
			}
			return string.Join(", ", list2);
		}

		private static List<string> ParseSkipListTokens(string list)
		{
			if (string.IsNullOrWhiteSpace(list))
			{
				return new List<string>();
			}
			return (from t in list.Split(',')
				select NormalizeSkipToken(t) into t
				where !string.IsNullOrWhiteSpace(t)
				select (t == "rader_booster") ? "radar_booster" : t).Distinct().ToList();
		}

		private static string NormalizeSkipToken(string s)
		{
			if (string.IsNullOrWhiteSpace(s))
			{
				return "";
			}
			string text = Extensions.NormalizeName(s);
			text = text.Trim('_');
			if (text == "rader_booster")
			{
				text = "radar_booster";
			}
			return text;
		}

		private static void TrySaveConfig()
		{
			try
			{
				ConfigFile config = Plugin.config;
				if (config != null)
				{
					config.Save();
				}
			}
			catch (Exception ex)
			{
				Log.Warning("Failed to save config: " + ex.Message);
			}
		}

		public bool TrySkipAdd(string rawItemName, out string? error, out string? message)
		{
			error = null;
			message = null;
			string text = NormalizeSkipToken(rawItemName);
			if (string.IsNullOrWhiteSpace(text))
			{
				error = "Usage: /sort skip add <itemName>";
				return false;
			}
			List<string> list = ParseSkipListTokens(skippedItems.Value);
			if (list.Contains(text))
			{
				message = "Already in skippedItems: " + text;
				return true;
			}
			list.Add(text);
			skippedItems.Value = NormalizeSkipListConfig(string.Join(", ", list));
			TrySaveConfig();
			message = "Added to skippedItems: " + text;
			return true;
		}

		public bool TrySkipRemove(string rawItemName, out string? error, out string? message)
		{
			error = null;
			message = null;
			string token = NormalizeSkipToken(rawItemName);
			if (string.IsNullOrWhiteSpace(token))
			{
				error = "Usage: /sort skip remove <itemName>";
				return false;
			}
			List<string> list = ParseSkipListTokens(skippedItems.Value);
			int count = list.Count;
			list = list.Where((string t) => t != token).ToList();
			if (list.Count == count)
			{
				message = "Not in skippedItems: " + token;
				return true;
			}
			skippedItems.Value = NormalizeSkipListConfig(string.Join(", ", list));
			TrySaveConfig();
			message = "Removed from skippedItems: " + token;
			return true;
		}

		public List<string> GetSkippedTokens()
		{
			return (from t in ParseSkipListTokens(skippedItems.Value)
				orderby t
				select t).ToList();
		}

		private bool ShouldSkip(GrabbableObject item)
		{
			if ((Object)(object)item == (Object)null)
			{
				return true;
			}
			if (!item.grabbable || item.deactivated || item.isHeld || item.isPocketed)
			{
				return true;
			}
			if (item.Name() == "body")
			{
				return true;
			}
			if (!item.isInShipRoom)
			{
				return true;
			}
			string text = item.Name();
			string text2 = (((Object)(object)item.itemProperties != (Object)null && item.itemProperties.isScrap) ? skippedItems.Value : "");
			if (!string.IsNullOrWhiteSpace(text2))
			{
				string[] array = text2.Split(',');
				string[] array2 = array;
				foreach (string s in array2)
				{
					string value = NormalizeSkipToken(s);
					if (!string.IsNullOrWhiteSpace(value) && text.Contains(value))
					{
						return true;
					}
				}
			}
			return false;
		}

		private bool ShouldSkipFullSort(GrabbableObject item, bool ignoreSkipTokens = false)
		{
			if ((Object)(object)item == (Object)null)
			{
				return true;
			}
			if (!item.grabbable || item.deactivated || item.isHeld || item.isPocketed)
			{
				return true;
			}
			if (item.Name() == "body")
			{
				return true;
			}
			if (!item.isInShipRoom)
			{
				return true;
			}
			if (ignoreSkipTokens)
			{
				return false;
			}
			string text = item.Name();
			string value = skippedItems.Value;
			if (!string.IsNullOrWhiteSpace(value))
			{
				string[] array = value.Split(',');
				foreach (string s in array)
				{
					string value2 = NormalizeSkipToken(s);
					if (!string.IsNullOrWhiteSpace(value2) && text.Contains(value2))
					{
						return true;
					}
				}
			}
			return false;
		}
	}
	public class SortCommand : Command
	{
		public override string Name => "sort";

		public override string[] Commands => new string[2]
		{
			"sort",
			((Command)this).Name
		};

		public override string Description => "Sorts items on the ship.\nUsage:\n  /sort                 -> sort everything\n  /sort -a              -> sort everything, IGNORE skippedItems\n  /sort -b              -> full sort, but DO NOT skip item types that have a saved /sort set position\n                       (Note: -a and -b cannot be combined)\n  /sort skip list       -> show skippedItems tokens\n  /sort skip add <name> -> add token to skippedItems (name can include spaces)\n  /sort skip remove <name> -> remove token from skippedItems\n  /sort <itemName>      -> pull that item type to YOUR position (e.g. /sort cash_register)\n  /sort <number>        -> shortcut from JSON (pull to you) (e.g. /sort 1)\n  /sort bind <name|id>  -> bind your HELD item to an alias name OR shortcut id (then /sort <name> or /sort <id> works)\n  /sort set [itemName]  -> set this type's future sort position to YOUR position (name optional if holding)\n  /sort reset [itemName]-> delete saved sort position for this type (name optional if holding)\n  /sort bindings        -> list all binds (numbers + names)\n  /sort positions       -> list saved sort positions";

		public SortCommand()
		{
			Log.Info("SortCommand constructor called");
		}

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			//IL_0238: Unknown result type (might be due to invalid IL or missing references)
			//IL_0242: Expected O, but got Unknown
			//IL_0a71: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a80: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a8f: Unknown result type (might be due to invalid IL or missing references)
			error = null;
			Log.Info("SortCommand.Invoke called with args: [" + string.Join(", ", args) + "]");
			if (args.Length != 0 && args[0] == "help")
			{
				ChatCommandAPI.Print(((Command)this).Description);
				return true;
			}
			if (!Ship.Stationary)
			{
				error = "Must be in orbit or stationary at company";
				Log.Warning("SortCommand failed: " + error);
				return false;
			}
			if (Sorter.inProgress)
			{
				error = "Operation in progress";
				Log.Warning("SortCommand failed: " + error);
				return false;
			}
			if (args.Contains("-r") || args.Contains("-redo"))
			{
				error = "Flag '-r/-redo' was removed.";
				return false;
			}
			if (args.Contains("-ab") || args.Contains("-ba"))
			{
				error = "Flags '-a' and '-b' cannot be combined. Use only one.";
				return false;
			}
			bool flag = args.Contains("-a") || args.Contains("-all") || (kwargs != null && (kwargs.ContainsKey("a") || kwargs.ContainsKey("all")));
			bool flag2 = args.Contains("-b") || args.Contains("-bound") || (kwargs != null && (kwargs.ContainsKey("b") || kwargs.ContainsKey("bound")));
			if (flag && flag2)
			{
				error = "Flags '-a' and '-b' cannot be combined. Use only one.";
				return false;
			}
			string[] array = args.Where((string a) => a != "-a" && a != "-all" && a != "-b" && a != "-bound" && a != "-ab" && a != "-ba").ToArray();
			Sorter sorter = null;
			if ((Object)(object)Plugin.sorterObject != (Object)null)
			{
				sorter = Plugin.sorterObject.GetComponent<Sorter>();
			}
			if ((Object)(object)sorter == (Object)null)
			{
				try
				{
					if ((Object)(object)Plugin.sorterObject == (Object)null)
					{
						Plugin.sorterObject = new GameObject("PastaSorter");
						Object.DontDestroyOnLoad((Object)(object)Plugin.sorterObject);
						Log.Info("Sorter lazily initialized from SortCommand");
					}
					sorter = Plugin.sorterObject.GetComponent<Sorter>();
					if ((Object)(object)sorter == (Object)null)
					{
						sorter = Plugin.sorterObject.AddComponent<Sorter>();
						Log.Info("Sorter component added from SortCommand");
					}
				}
				catch (Exception ex)
				{
					error = "Sorter not initialized yet";
					Log.Error("SortCommand failed: " + error);
					Log.Error("Exception: " + ex.Message);
					return false;
				}
				if ((Object)(object)sorter == (Object)null)
				{
					error = "Sorter not initialized yet";
					Log.Warning("SortCommand failed: " + error);
					return false;
				}
			}
			Log.Info("SortCommand executing...");
			if (array.Length != 0)
			{
				if (array[0] == "skip")
				{
					if (array.Length < 2)
					{
						error = "Usage: /sort skip <list|add|remove> ...";
						return false;
					}
					string text = array[1];
					if (text == "list" || text == "ls")
					{
						List<string> skippedTokens = sorter.GetSkippedTokens();
						if (skippedTokens.Count == 0)
						{
							ChatCommandAPI.Print("skippedItems is empty.");
							return true;
						}
						string text2 = string.Join(", ", skippedTokens.Take(20));
						if (skippedTokens.Count > 20)
						{
							text2 += ", ...";
						}
						ChatCommandAPI.Print($"skippedItems ({skippedTokens.Count}): {text2}");
						return true;
					}
					switch (text)
					{
					case "add":
					{
						string text4 = string.Join(" ", array.Skip(2)).Trim();
						if (string.IsNullOrWhiteSpace(text4))
						{
							text4 = (Player.Local?.currentlyHeldObjectServer)?.Name() ?? "";
							if (string.IsNullOrWhiteSpace(text4))
							{
								error = "Usage: /sort skip add <itemName> (or hold an item)";
								return false;
							}
						}
						text4 = ResolveSkipTarget(text4);
						if (!sorter.TrySkipAdd(text4, out error, out string message2))
						{
							return false;
						}
						if (!string.IsNullOrWhiteSpace(message2))
						{
							ChatCommandAPI.Print(message2);
						}
						return true;
					}
					default:
						if (!(text == "del"))
						{
							error = "Usage: /sort skip <list|add|remove> ...";
							return false;
						}
						goto case "remove";
					case "remove":
					case "rm":
					{
						string text3 = string.Join(" ", array.Skip(2)).Trim();
						if (string.IsNullOrWhiteSpace(text3))
						{
							text3 = (Player.Local?.currentlyHeldObjectServer)?.Name() ?? "";
							if (string.IsNullOrWhiteSpace(text3))
							{
								error = "Usage: /sort skip remove <itemName> (or hold an item)";
								return false;
							}
						}
						text3 = ResolveSkipTarget(text3);
						if (!sorter.TrySkipRemove(text3, out error, out string message))
						{
							return false;
						}
						if (!string.IsNullOrWhiteSpace(message))
						{
							ChatCommandAPI.Print(message);
						}
						return true;
					}
					}
				}
				if (array[0] == "bind")
				{
					if (array.Length < 2)
					{
						error = "Usage: /sort bind <name> (hold the item you want to bind)";
						return false;
					}
					if (array.Length >= 3 && (array[1] == "reset" || array[1] == "rm" || array[1] == "remove" || array[1] == "del"))
					{
						string text5 = string.Join(" ", array.Skip(2)).Trim();
						if (string.IsNullOrWhiteSpace(text5))
						{
							error = "Usage: /sort bind reset <name|id>";
							return false;
						}
						if (int.TryParse(text5, out var result) && result > 0)
						{
							if (!SortShortcuts.RemoveShortcut(result, out bool removed, out string error2))
							{
								error = error2 ?? "Failed to remove shortcut.";
								return false;
							}
							ChatCommandAPI.Print(removed ? $"Unbound {result}" : $"No binding for {result}");
							return true;
						}
						if (!SortShortcuts.RemoveAlias(text5, out bool removed2, out string error3))
						{
							error = error3 ?? "Failed to remove alias.";
							return false;
						}
						string text6 = Extensions.NormalizeName(text5);
						ChatCommandAPI.Print(removed2 ? ("Unbound " + text6) : ("No binding for " + text6));
						return true;
					}
					GrabbableObject val = (((Object)(object)Player.Local != (Object)null) ? Player.Local.currentlyHeldObjectServer : null);
					if ((Object)(object)val == (Object)null)
					{
						error = "You must hold an item to bind.";
						return false;
					}
					string text7 = string.Join(" ", array.Skip(1));
					string text8 = val.Name();
					if (int.TryParse(text7.Trim(), out var result2) && result2 > 0)
					{
						if (!SortShortcuts.SetShortcut(result2, text8, out string error4))
						{
							error = error4 ?? "Failed to bind shortcut.";
							return false;
						}
						ChatCommandAPI.Print($"Bound {result2} => {text8}");
						return true;
					}
					if (!SortShortcuts.BindAlias(text7, text8, out string error5))
					{
						error = error5 ?? "Failed to bind alias.";
						return false;
					}
					ChatCommandAPI.Print("Bound " + Extensions.NormalizeName(text7) + " => " + text8);
					return true;
				}
				if (array[0] == "reset")
				{
					string text9 = ((array.Length > 1) ? Extensions.NormalizeName(string.Join(" ", array.Skip(1))) : (Player.Local?.currentlyHeldObjectServer)?.Name());
					if (string.IsNullOrWhiteSpace(text9))
					{
						error = "Missing item name (hold the item or provide a name).";
						return false;
					}
					if (!SortPositions.Remove(text9, out bool removed3, out string error6))
					{
						error = error6 ?? "Failed to remove saved position.";
						return false;
					}
					if (error6 != null)
					{
						error = error6;
						return false;
					}
					if (removed3)
					{
						ChatCommandAPI.Print("Removed saved sort position for '" + text9 + "'.");
					}
					else
					{
						ChatCommandAPI.Print("No saved sort position for '" + text9 + "'.");
					}
					return true;
				}
				if (array[0] == "set")
				{
					string text10 = ((array.Length > 1) ? string.Join(" ", array.Skip(1)) : null);
					if (!sorter.TrySetAndMoveTypeToPlayer(text10, force: false, out string resolvedItemKey, out error))
					{
						return false;
					}
					if (!string.IsNullOrWhiteSpace(text10))
					{
						string text11 = Extensions.NormalizeName(text10);
						if (!string.IsNullOrWhiteSpace(text11) && text11 != resolvedItemKey)
						{
							ChatCommandAPI.Print("Resolved '" + text10 + "' => '" + resolvedItemKey + "'.");
						}
					}
					string text12 = (string.IsNullOrWhiteSpace(resolvedItemKey) ? "held_item" : resolvedItemKey);
					if (SortPositions.TryGet(resolvedItemKey, out Vector3 shipLocalPos, out string error7))
					{
						ChatCommandAPI.Print($"Saved sort position for '{text12}' => (x={shipLocalPos.x:F2}, y={shipLocalPos.y:F2}, z={shipLocalPos.z:F2}).");
					}
					else
					{
						if (error7 != null)
						{
							Log.Warning(error7);
						}
						ChatCommandAPI.Print("Saved sort position for '" + text12 + "'.");
					}
					return true;
				}
				if (array[0] == "positions")
				{
					string error8;
					List<(string, Vector3)> list = SortPositions.ListAll(out error8);
					if (error8 != null)
					{
						error = error8;
						return false;
					}
					if (list.Count == 0)
					{
						ChatCommandAPI.Print("No saved sort positions.");
						return true;
					}
					string text13 = string.Join(", ", from p in list.Take(8)
						select $"{p.itemKey}=(x={p.shipLocalPos.x:F1},y={p.shipLocalPos.y:F1},z={p.shipLocalPos.z:F1})");
					if (list.Count > 8)
					{
						text13 += ", ...";
					}
					ChatCommandAPI.Print(text13);
					return true;
				}
				if (array[0] == "bindings" || array[0] == "binds" || array[0] == "shortcuts" || array[0] == "aliases")
				{
					string error9;
					List<(int, string)> list2 = SortShortcuts.ListShortcuts(out error9);
					if (error9 != null)
					{
						error = error9;
						return false;
					}
					string error10;
					List<(string, string)> list3 = SortShortcuts.ListAliases(out error10);
					if (error10 != null)
					{
						error = error10;
						return false;
					}
					if (list2.Count == 0 && list3.Count == 0)
					{
						ChatCommandAPI.Print("No bindings found.");
						return true;
					}
					if (list2.Count > 0)
					{
						string text14 = string.Join(", ", list2.Select<(int, string), string>(((int id, string itemKey) s) => $"{s.id}={s.itemKey}"));
						ChatCommandAPI.Print(text14);
					}
					if (list3.Count > 0)
					{
						string text15 = string.Join(", ", from a in list3.Take(12)
							select a.alias + "=" + a.itemKey);
						if (list3.Count > 12)
						{
							text15 += ", ...";
						}
						ChatCommandAPI.Print(text15);
					}
					return true;
				}
				if (array.Length == 1 && int.TryParse(array[0], out var result3))
				{
					if (!SortShortcuts.TryResolve(result3, out string itemKey, out string error11))
					{
						error = error11 ?? "Unknown shortcut error";
						return false;
					}
					Log.ConfirmSound();
					if (!sorter.TryStartGatherByQuery(itemKey, force: false, out error))
					{
						return false;
					}
					Log.Info("SortCommand executed successfully (shortcut move)");
					return true;
				}
				if (array.Length == 1 && SortShortcuts.TryResolveAlias(array[0], out string itemKey2, out string _))
				{
					Log.ConfirmSound();
					if (!sorter.TryStartGatherByQuery(itemKey2, force: false, out error))
					{
						return false;
					}
					Log.Info("SortCommand executed successfully (alias move)");
					return true;
				}
				string query = string.Join(" ", array);
				Log.ConfirmSound();
				if (!sorter.TryStartGatherByQuery(query, force: false, out error))
				{
					return false;
				}
				Log.Info("SortCommand executed successfully (item move)");
				return true;
			}
			sorter.CategorizeItems(flag || flag2);
			Log.ConfirmSound();
			((MonoBehaviour)sorter).StartCoroutine(sorter.SortItems(force: false, flag, flag2));
			Log.Info("SortCommand executed successfully (full sort)");
			return true;
			static string ResolveSkipTarget(string raw)
			{
				raw = (raw ?? "").Trim();
				if (string.IsNullOrWhiteSpace(raw))
				{
					return raw;
				}
				string error13;
				if (int.TryParse(raw, out var result4) && result4 > 0)
				{
					if (SortShortcuts.TryResolve(result4, out string itemKey3, out error13))
					{
						return itemKey3;
					}
					return raw;
				}
				if (SortShortcuts.TryResolveAlias(raw, out string itemKey4, out error13))
				{
					return itemKey4;
				}
				return raw;
			}
		}
	}
	public class SortBindCommand : Command
	{
		public override string Name => "sb";

		public override string[] Commands => new string[2]
		{
			"sb",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort bind\nUsage:\n  /sb <name|id>  -> bind your HELD item to an alias name OR shortcut id";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "bind" }.Concat(args ?? Array.Empty<string>()).ToArray();
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class SortSetCommand : Command
	{
		public override string Name => "ss";

		public override string[] Commands => new string[2]
		{
			"ss",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort set\nUsage:\n  /ss [itemName]  -> set saved sort position for this type (name optional if holding; partial match supported)";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "set" }.Concat(args ?? Array.Empty<string>()).ToArray();
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class SortResetCommand : Command
	{
		public override string Name => "sr";

		public override string[] Commands => new string[2]
		{
			"sr",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort reset\nUsage:\n  /sr [itemName]  -> delete saved sort position for this type (name optional if holding)";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "reset" }.Concat(args ?? Array.Empty<string>()).ToArray();
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class SortPositionsCommand : Command
	{
		public override string Name => "sp";

		public override string[] Commands => new string[2]
		{
			"sp",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort positions\nUsage:\n  /sp  -> list saved sort positions";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "positions" };
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class SortBindingsListCommand : Command
	{
		public override string Name => "sbl";

		public override string[] Commands => new string[2]
		{
			"sbl",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort bindings\nUsage:\n  /sbl  -> list bindings (shortcuts + aliases)";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "bindings" };
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class SortSkipCommand : Command
	{
		public override string Name => "sk";

		public override string[] Commands => new string[2]
		{
			"sk",
			((Command)this).Name
		};

		public override string Description => "Shortcut for /sort skip\nUsage:\n  /sk list            -> show skippedItems tokens\n  /sk add [itemName]  -> add token (or use held item if omitted)\n  /sk remove [itemName] -> remove token (or use held item if omitted)";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			string[] array = new string[1] { "skip" }.Concat(args ?? Array.Empty<string>()).ToArray();
			return ((Command)new SortCommand()).Invoke(array, kwargs, ref error);
		}
	}
	public class PileCommand : Command
	{
		public override string Name => "pile";

		public override string[] Commands => new string[2]
		{
			"pile",
			((Command)this).Name
		};

		public override string Description => "Piles a specific item type onto your position (like /sort <item>).\nUsage:\n  /pile <itemName>  -> pull that item type to YOUR position (partial match supported)\n  /pile             -> uses your HELD item type and also moves the held item";

		public override bool Invoke(string[] args, Dictionary<string, string> kwargs, out string? error)
		{
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Expected O, but got Unknown
			error = null;
			if (args.Length != 0 && args[0] == "help")
			{
				ChatCommandAPI.Print(((Command)this).Description);
				return true;
			}
			Sorter sorter = null;
			if ((Object)(object)Plugin.sorterObject != (Object)null)
			{
				sorter = Plugin.sorterObject.GetComponent<Sorter>();
			}
			if ((Object)(object)sorter == (Object)null)
			{
				try
				{
					if ((Object)(object)Plugin.sorterObject == (Object)null)
					{
						Plugin.sorterObject = new GameObject("PastaSorter");
						Object.DontDestroyOnLoad((Object)(object)Plugin.sorterObject);
					}
					sorter = Plugin.sorterObject.GetComponent<Sorter>();
					if ((Object)(object)sorter == (Object)null)
					{
						sorter = Plugin.sorterObject.AddComponent<Sorter>();
					}
				}
				catch (Exception ex)
				{
					error = "Sorter not initialized yet";
					Log.Error("PileCommand failed: " + ex.Message);
					return false;
				}
			}
			string queryOrNull = ((args != null && args.Length != 0) ? string.Join(" ", args) : null);
			Log.ConfirmSound();
			return sorter.TryStartPileByQueryOrHeld(queryOrNull, force: false, out error);
		}
	}
	internal static class SortPositions
	{
		[Serializable]
		private class PositionsFile
		{
			public List<PositionEntry> positions = new List<PositionEntry>();
		}

		[Serializable]
		private class PositionEntry
		{
			public string item = "";

			public float x;

			public float y;

			public float z;
		}

		public static string PositionsPath => Path.Combine(Paths.ConfigPath, "pasta.quicksort.sort.positions.json");

		public static void EnsureFileExists()
		{
			try
			{
				string directoryName = Path.GetDirectoryName(PositionsPath);
				if (!string.IsNullOrWhiteSpace(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				if (!File.Exists(PositionsPath))
				{
					PositionsFile positionsFile = new PositionsFile
					{
						positions = new List<PositionEntry>
						{
							new PositionEntry
							{
								item = "soccer_ball",
								x = 9f,
								y = -1.2f,
								z = -8.4f
							},
							new PositionEntry
							{
								item = "whoopie_cushion",
								x = 9.4f,
								y = 1.81f,
								z = -6.3f
							}
						}
					};
					string content = JsonConvert.SerializeObject((object)positionsFile, (Formatting)1);
					if (!TryWriteAllTextAtomic(PositionsPath, content, out string error))
					{
						throw new IOException(error ?? "Failed to write positions file (unknown error).");
					}
				}
			}
			catch (Exception ex)
			{
				Log.Warning("Failed to create positions file: " + ex.GetType().Name + ": " + ex.Message);
				Log.Warning("Stack trace: " + ex.StackTrace);
			}
		}

		private static PositionsFile Load(out string? error)
		{
			error = null;
			EnsureFileExists();
			try
			{
				string text = File.ReadAllText(PositionsPath, Encoding.UTF8);
				PositionsFile positionsFile = JsonConvert.DeserializeObject<PositionsFile>(text) ?? new PositionsFile();
				PositionsFile positionsFile2 = positionsFile;
				if (positionsFile2.positions == null)
				{
					positionsFile2.positions = new List<PositionEntry>();
				}
				return positionsFile;
			}
			catch (Exception ex)
			{
				error = "Failed to load positions JSON: " + ex.Message;
				return new PositionsFile();
			}
		}

		private static bool Save(PositionsFile data, out string? error)
		{
			error = null;
			try
			{
				string directoryName = Path.GetDirectoryName(PositionsPath);
				if (!string.IsNullOrWhiteSpace(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				string content = JsonConvert.SerializeObject((object)data, (Formatting)1);
				if (!TryWriteAllTextAtomic(PositionsPath, content, out error))
				{
					return false;
				}
				if (!File.Exists(PositionsPath))
				{
					error = "Positions JSON not found after write. Path='" + PositionsPath + "', CWD='" + Directory.GetCurrentDirectory() + "', ConfigPath='" + Paths.ConfigPath + "', BepInExRoot='" + Paths.BepInExRootPath + "'.";
					return false;
				}
				FileInfo fileInfo = new FileInfo(PositionsPath);
				if (fileInfo.Length <= 0)
				{
					error = $"Positions JSON is empty after write. Path='{PositionsPath}', LastWriteUtc='{fileInfo.LastWriteTimeUtc:o}'.";
					return false;
				}
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to save positions JSON: " + ex.Message;
				return false;
			}
		}

		private static bool TryWriteAllTextAtomic(string path, string content, out string? error)
		{
			error = null;
			try
			{
				string directoryName = Path.GetDirectoryName(path);
				if (!string.IsNullOrWhiteSpace(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				string text = path + ".tmp";
				File.WriteAllText(text, content, Encoding.UTF8);
				if (File.Exists(path))
				{
					string destinationBackupFileName = path + ".bak";
					try
					{
						File.Replace(text, path, destinationBackupFileName, ignoreMetadataErrors: true);
					}
					catch
					{
						File.Copy(text, path, overwrite: true);
						File.Delete(text);
					}
				}
				else
				{
					File.Move(text, path);
				}
				return true;
			}
			catch (Exception ex)
			{
				error = ex.GetType().Name + ": " + ex.Message + " (Path='" + path + "', CWD='" + Directory.GetCurrentDirectory() + "', ConfigPath='" + Paths.ConfigPath + "')";
				return false;
			}
		}

		public static bool TryGet(string itemKey, out Vector3 shipLocalPos, out string? error)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: 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)
			string itemKey2 = itemKey;
			shipLocalPos = default(Vector3);
			itemKey2 = Extensions.NormalizeName(itemKey2);
			PositionsFile positionsFile = Load(out error);
			if (error != null)
			{
				return false;
			}
			PositionEntry positionEntry = positionsFile.positions?.FirstOrDefault((PositionEntry p) => p != null && Extensions.NormalizeName(p.item) == itemKey2);
			if (positionEntry == null)
			{
				return false;
			}
			shipLocalPos = new Vector3(positionEntry.x, positionEntry.y, positionEntry.z);
			return true;
		}

		public static bool Set(string itemKey, Vector3 shipLocalPos, out string? error)
		{
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: 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_0116: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_0132: Unknown result type (might be due to invalid IL or missing references)
			string itemKey2 = itemKey;
			itemKey2 = Extensions.NormalizeName(itemKey2);
			PositionsFile positionsFile = Load(out error);
			if (error != null)
			{
				return false;
			}
			PositionsFile positionsFile2 = positionsFile;
			if (positionsFile2.positions == null)
			{
				positionsFile2.positions = new List<PositionEntry>();
			}
			PositionEntry positionEntry = positionsFile.positions.FirstOrDefault((PositionEntry p) => p != null && Extensions.NormalizeName(p.item) == itemKey2);
			if (positionEntry == null)
			{
				positionEntry = new PositionEntry
				{
					item = itemKey2
				};
				positionsFile.positions.Add(positionEntry);
			}
			positionEntry.item = itemKey2;
			positionEntry.x = shipLocalPos.x;
			positionEntry.y = shipLocalPos.y;
			positionEntry.z = shipLocalPos.z;
			if (!Save(positionsFile, out error))
			{
				if (error != null)
				{
					Log.Warning(error);
				}
				return false;
			}
			Log.Info($"Saved sort position '{itemKey2}' => (x={shipLocalPos.x:F3}, y={shipLocalPos.y:F3}, z={shipLocalPos.z:F3}) to {PositionsPath}");
			return true;
		}

		public static bool Remove(string itemKey, out bool removed, out string? error)
		{
			string itemKey2 = itemKey;
			removed = false;
			itemKey2 = Extensions.NormalizeName(itemKey2);
			PositionsFile positionsFile = Load(out error);
			if (error != null)
			{
				return false;
			}
			if (positionsFile.positions == null || positionsFile.positions.Count == 0)
			{
				removed = false;
				return true;
			}
			int count = positionsFile.positions.Count;
			positionsFile.positions = positionsFile.positions.Where((PositionEntry p) => p != null && Extensions.NormalizeName(p.item) != itemKey2).ToList();
			removed = positionsFile.positions.Count != count;
			if (!Save(positionsFile, out error))
			{
				if (error != null)
				{
					Log.Warning(error);
				}
				return false;
			}
			if (removed)
			{
				Log.Info("Removed sort position '" + itemKey2 + "' from " + PositionsPath);
			}
			return true;
		}

		public static List<(string itemKey, Vector3 shipLocalPos)> ListAll(out string? error)
		{
			PositionsFile positionsFile = Load(out error);
			if (error != null)
			{
				return new List<(string, Vector3)>();
			}
			if (positionsFile.positions == null)
			{
				return new List<(string, Vector3)>();
			}
			return (from p in positionsFile.positions.Where((PositionEntry p) => p != null && !string.IsNullOrWhiteSpace(p.item)).Select((Func<PositionEntry, (string, Vector3)>)((PositionEntry p) => (Extensions.NormalizeName(p.item), new Vector3(p.x, p.y, p.z))))
				orderby p.Item1
				select p).ToList();
		}
	}
	internal static class SortShortcuts
	{
		[Serializable]
		private class ShortcutFile
		{
			public List<Shortcut> shortcuts = new List<Shortcut>();

			public Dictionary<string, string> aliases = new Dictionary<string, string>();
		}

		[Serializable]
		private class Shortcut
		{
			public int id;

			public string item = "";
		}

		private static string OldShortcutPath => Path.Combine(Paths.ConfigPath, "pasta.quicksort.sort.shortcuts.json");

		public static string ShortcutPath => Path.Combine(Paths.ConfigPath, "pasta.quicksort.sort.bindings.json");

		public static void EnsureFileExists()
		{
			try
			{
				string directoryName = Path.GetDirectoryName(ShortcutPath);
				if (!string.IsNullOrWhiteSpace(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				if (!File.Exists(ShortcutPath) && File.Exists(OldShortcutPath))
				{
					try
					{
						File.Copy(OldShortcutPath, ShortcutPath, overwrite: false);
						Log.Info("Migrated shortcuts file '" + OldShortcutPath + "' => '" + ShortcutPath + "'");
					}
					catch (Exception ex)
					{
						Log.Warning("Failed to migrate shortcuts file: " + ex.GetType().Name + ": " + ex.Message);
					}
				}
				if (!File.Exists(ShortcutPath))
				{
					ShortcutFile shortcutFile = new ShortcutFile
					{
						shortcuts = new List<Shortcut>
						{
							new Shortcut
							{
								id = 1,
								item = "weed_killer"
							},
							new Shortcut
							{
								id = 2,
								item = "shovel"
							}
						}
					};
					string content = JsonConvert.SerializeObject((object)shortcutFile, (Formatting)1);
					if (!TryWriteAllTextAtomic(ShortcutPath, content, out string error))
					{
						throw new IOException(error ?? "Failed to write shortcuts file (unknown error).");
					}
				}
			}
			catch (Exception ex2)
			{
				Log.Warning("Failed to create shortcuts file: " + ex2.GetType().Name + ": " + ex2.Message);
				Log.Warning("Stack trace: " + ex2.StackTrace);
			}
		}

		public static bool TryResolve(int id, out string itemKey, out string? error)
		{
			itemKey = "";
			error = null;
			EnsureFileExists();
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text);
				if (shortcutFile?.shortcuts == null)
				{
					error = "Shortcuts file is empty or invalid.";
					return false;
				}
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				Shortcut shortcut = shortcutFile.shortcuts.FirstOrDefault((Shortcut s) => s != null && s.id == id);
				if (shortcut == null || string.IsNullOrWhiteSpace(shortcut.item))
				{
					error = $"No shortcut found for {id}.";
					return false;
				}
				itemKey = Extensions.NormalizeName(shortcut.item);
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to load shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static bool SetShortcut(int id, string itemKey, out string? error)
		{
			error = null;
			EnsureFileExists();
			if (id <= 0)
			{
				error = "Shortcut id must be >= 1.";
				return false;
			}
			itemKey = Extensions.NormalizeName(itemKey);
			if (string.IsNullOrWhiteSpace(itemKey))
			{
				error = "Invalid item key.";
				return false;
			}
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text) ?? new ShortcutFile();
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.shortcuts == null)
				{
					shortcutFile2.shortcuts = new List<Shortcut>();
				}
				shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				Shortcut shortcut = shortcutFile.shortcuts.FirstOrDefault((Shortcut s) => s != null && s.id == id);
				if (shortcut == null)
				{
					shortcut = new Shortcut
					{
						id = id,
						item = itemKey
					};
					shortcutFile.shortcuts.Add(shortcut);
				}
				shortcut.id = id;
				shortcut.item = itemKey;
				string content = JsonConvert.SerializeObject((object)shortcutFile, (Formatting)1);
				if (!TryWriteAllTextAtomic(ShortcutPath, content, out error))
				{
					return false;
				}
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to save shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static bool TryResolveAlias(string alias, out string itemKey, out string? error)
		{
			itemKey = "";
			error = null;
			EnsureFileExists();
			alias = Extensions.NormalizeName(alias);
			if (string.IsNullOrWhiteSpace(alias))
			{
				error = "Invalid alias.";
				return false;
			}
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text);
				if (shortcutFile == null)
				{
					error = "Shortcuts file is empty or invalid.";
					return false;
				}
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				if (!shortcutFile.aliases.TryGetValue(alias, out string value) || string.IsNullOrWhiteSpace(value))
				{
					error = "No alias found for '" + alias + "'.";
					return false;
				}
				itemKey = Extensions.NormalizeName(value);
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to load shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static bool BindAlias(string alias, string itemKey, out string? error)
		{
			error = null;
			EnsureFileExists();
			alias = Extensions.NormalizeName(alias);
			itemKey = Extensions.NormalizeName(itemKey);
			if (string.IsNullOrWhiteSpace(alias))
			{
				error = "Invalid alias.";
				return false;
			}
			if (string.IsNullOrWhiteSpace(itemKey))
			{
				error = "Invalid item key.";
				return false;
			}
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text) ?? new ShortcutFile();
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.shortcuts == null)
				{
					shortcutFile2.shortcuts = new List<Shortcut>();
				}
				shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				shortcutFile.aliases[alias] = itemKey;
				string content = JsonConvert.SerializeObject((object)shortcutFile, (Formatting)1);
				if (!TryWriteAllTextAtomic(ShortcutPath, content, out error))
				{
					return false;
				}
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to save shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static bool RemoveShortcut(int id, out bool removed, out string? error)
		{
			removed = false;
			error = null;
			EnsureFileExists();
			if (id <= 0)
			{
				error = "Shortcut id must be >= 1.";
				return false;
			}
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text) ?? new ShortcutFile();
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.shortcuts == null)
				{
					shortcutFile2.shortcuts = new List<Shortcut>();
				}
				shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				int count = shortcutFile.shortcuts.Count;
				shortcutFile.shortcuts = shortcutFile.shortcuts.Where((Shortcut s) => s != null && s.id != id).ToList();
				removed = shortcutFile.shortcuts.Count != count;
				string content = JsonConvert.SerializeObject((object)shortcutFile, (Formatting)1);
				if (!TryWriteAllTextAtomic(ShortcutPath, content, out error))
				{
					return false;
				}
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to save shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static bool RemoveAlias(string alias, out bool removed, out string? error)
		{
			removed = false;
			error = null;
			EnsureFileExists();
			alias = Extensions.NormalizeName(alias);
			if (string.IsNullOrWhiteSpace(alias))
			{
				error = "Invalid alias.";
				return false;
			}
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text) ?? new ShortcutFile();
				ShortcutFile shortcutFile2 = shortcutFile;
				if (shortcutFile2.shortcuts == null)
				{
					shortcutFile2.shortcuts = new List<Shortcut>();
				}
				shortcutFile2 = shortcutFile;
				if (shortcutFile2.aliases == null)
				{
					shortcutFile2.aliases = new Dictionary<string, string>();
				}
				removed = shortcutFile.aliases.Remove(alias);
				string content = JsonConvert.SerializeObject((object)shortcutFile, (Formatting)1);
				if (!TryWriteAllTextAtomic(ShortcutPath, content, out error))
				{
					return false;
				}
				return true;
			}
			catch (Exception ex)
			{
				error = "Failed to save shortcuts JSON: " + ex.Message;
				return false;
			}
		}

		public static List<(string alias, string itemKey)> ListAliases(out string? error)
		{
			error = null;
			EnsureFileExists();
			try
			{
				string text = File.ReadAllText(ShortcutPath, Encoding.UTF8);
				ShortcutFile shortcutFile = JsonConvert.DeserializeObject<ShortcutFile>(text);
				if (shortcutFile?.aliases == null)
				{
					return new List<(string, string)>();
				}
				return (from kv in shortcutFile.aliases
					where !string.IsNullOrWhiteSpace(kv.Key) && !string.IsNullOrWhiteSpace(kv.Value)
					select (Extensions.NormalizeName(kv.Key), Extensions.NormalizeName(kv.Value)) into x
					orderby x.Item1
					select x).ToList();
			}
			catch (Exception ex)
			{
				error = "Failed to load shortcuts JSON: " + ex.Message;
				return new List<(string, strin