Decompiled source of HTF MoreHead v0.6.3

HTFMoreHead.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using FishNet.Broadcast;
using FishNet.Connection;
using FishNet.Managing;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Transporting;
using HarmonyLib;
using HowToFish.MoreHead.Bootstrap;
using HowToFish.MoreHead.Content;
using HowToFish.MoreHead.Diagnostics;
using HowToFish.MoreHead.Networking;
using HowToFish.MoreHead.Persistence;
using HowToFish.MoreHead.Players;
using HowToFish.MoreHead.Visuals;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("HTFMoreHead")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Custom cosmetic framework for How to Fish")]
[assembly: AssemblyFileVersion("0.6.3.0")]
[assembly: AssemblyInformationalVersion("0.6.3+06517c320af77ef3e7ad1f01838d4b612fc44623")]
[assembly: AssemblyProduct("HTFMoreHead")]
[assembly: AssemblyTitle("HTFMoreHead")]
[assembly: AssemblyVersion("0.6.3.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace HowToFish.MoreHead
{
	[BepInPlugin("com.htfmodding.morehead", "HTF MoreHead", "0.6.3")]
	public sealed class MoreHeadPlugin : BaseUnityPlugin
	{
		private const int MainThreadQueueCapacity = 128;

		private const int MainThreadActionsPerFrame = 32;

		private Harmony _harmony;

		private MainThreadDispatcher _dispatcher;

		private BoundedLog _boundedLog;

		private PlayerProbeTracker _tracker;

		private CosmeticCatalog _catalog;

		private LocalLoadoutStore _localLoadoutStore;

		private LocalSkinPreviewVisuals _localSkinPreviewVisuals;

		private PlayerAppearanceService _appearanceService;

		private CosmeticNetworkService _networkService;

		private ConfigEntry<bool> _enableStage0Probe;

		private ConfigEntry<float> _probeTimeoutSeconds;

		private bool _shuttingDown;

		private int _probeDisableCleanupRequested;

		private int _probeSettingChangePending;

		private int _requestedProbeEnabled;

		internal static MoreHeadPlugin Instance { get; private set; }

		private void Awake()
		{
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Expected O, but got Unknown
			//IL_020f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0219: Expected O, but got Unknown
			Instance = this;
			_dispatcher = new MainThreadDispatcher(128);
			_boundedLog = new BoundedLog(((BaseUnityPlugin)this).Logger, 256);
			_enableStage0Probe = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "EnableStage0Probe", false, "Legacy development probe. Production 0.6.0 forces this off.");
			_enableStage0Probe.Value = false;
			_probeTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "ProbeTimeoutSeconds", 2f, new ConfigDescription("Maximum time to reconcile Player and PlayerSkin lifecycle signals.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f), Array.Empty<object>()));
			_requestedProbeEnabled = (_enableStage0Probe.Value ? 1 : 0);
			_tracker = new PlayerProbeTracker(((BaseUnityPlugin)this).Logger, _boundedLog, _probeTimeoutSeconds.Value);
			_catalog = new CosmeticCatalog(((BaseUnityPlugin)this).Logger, _boundedLog);
			_catalog.LoadAll(Paths.PluginPath);
			_localLoadoutStore = new LocalLoadoutStore(((BaseUnityPlugin)this).Logger, Path.Combine(Paths.ConfigPath, "HTFMoreHead", "loadout.json"));
			_localLoadoutStore.Load();
			_localSkinPreviewVisuals = new LocalSkinPreviewVisuals(((BaseUnityPlugin)this).Logger, _catalog, _localLoadoutStore);
			_appearanceService = new PlayerAppearanceService(((BaseUnityPlugin)this).Logger, _boundedLog, _catalog);
			_networkService = new CosmeticNetworkService(((BaseUnityPlugin)this).Logger, _boundedLog, _catalog, _localLoadoutStore, _appearanceService);
			_localSkinPreviewVisuals.SelectionCommitted += OnLocalSelectionCommitted;
			try
			{
				RuntimeFingerprint.LogCurrent(((BaseUnityPlugin)this).Logger, ((BaseUnityPlugin)this).Info.Location);
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"[HTFMoreHead][Stage0][FINGERPRINT] Runtime fingerprint failed safely: {arg}");
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[HTFMoreHead][CREDITS] Thanks to Masaicker, creator of MoreHead, for permission to reference the original project: https://github.com/Masaicker/repo-MoreHead");
			if (!ValidatePatchTargets())
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"[HTFMoreHead][Stage0][PATCH] Required lifecycle methods were not found; stage 0 probes are disabled for this game build.");
				return;
			}
			SceneManager.sceneUnloaded += OnSceneUnloaded;
			_harmony = new Harmony("com.htfmodding.morehead");
			try
			{
				_harmony.PatchAll(Assembly.GetExecutingAssembly());
			}
			catch (Exception arg2)
			{
				SceneManager.sceneUnloaded -= OnSceneUnloaded;
				try
				{
					_harmony.UnpatchSelf();
				}
				catch (Exception arg3)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("[HTFMoreHead][Stage0][PATCH] Cleanup after patch failure also failed: " + $"{arg3}"));
				}
				_harmony = null;
				((BaseUnityPlugin)this).Logger.LogError((object)$"[HTFMoreHead][Stage0][PATCH] Lifecycle probes could not be installed: {arg2}");
				return;
			}
			_enableStage0Probe.SettingChanged += OnStage0ProbeSettingChanged;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[HTFMoreHead][Stage0][READY] Lifecycle probes installed. " + $"Enabled={_enableStage0Probe.Value}, MainThreadId={_dispatcher.MainThreadId}, " + $"Timeout={_probeTimeoutSeconds.Value:F1}s."));
		}

		private void Update()
		{
			if (_shuttingDown || _dispatcher == null)
			{
				return;
			}
			try
			{
				ApplyPendingProbeSettingChange();
				_dispatcher.Drain(32, delegate(Exception ex)
				{
					ReportPatchException("MainThreadQueue", ex);
				});
				int num = _dispatcher.ConsumeDroppedCount();
				if (num > 0)
				{
					_boundedLog.WarningOnce("main-thread-queue-overflow", "[HTFMoreHead][Stage0][THREAD] Main-thread queue overflowed; " + $"DroppedActions={num}, Capacity={128}.");
				}
				if (_enableStage0Probe != null && _enableStage0Probe.Value)
				{
					_tracker?.Tick();
				}
				_networkService?.Tick();
			}
			catch (Exception exception)
			{
				ReportPatchException("Update", exception);
			}
		}

		private void OnDestroy()
		{
			if (_shuttingDown)
			{
				return;
			}
			_shuttingDown = true;
			SceneManager.sceneUnloaded -= OnSceneUnloaded;
			if (_enableStage0Probe != null)
			{
				_enableStage0Probe.SettingChanged -= OnStage0ProbeSettingChanged;
			}
			try
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)$"[HTFMoreHead][Stage0][SHUTDOWN] Harmony unpatch failed: {arg}");
			}
			try
			{
				if (_localSkinPreviewVisuals != null)
				{
					_localSkinPreviewVisuals.SelectionCommitted -= OnLocalSelectionCommitted;
				}
				_networkService?.ClearAndDetach();
				_appearanceService?.ClearAll();
				_localSkinPreviewVisuals?.Dispose();
				_tracker?.ClearAll("PluginDestroy");
			}
			catch (Exception arg2)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)$"[HTFMoreHead][Stage0][SHUTDOWN] Tracker cleanup failed: {arg2}");
			}
			finally
			{
				_dispatcher?.Clear();
				_harmony = null;
				_tracker = null;
				_networkService = null;
				_appearanceService = null;
				_localSkinPreviewVisuals = null;
				_localLoadoutStore = null;
				_catalog?.UnloadAll();
				_catalog = null;
				_dispatcher = null;
				if ((Object)(object)Instance == (Object)(object)this)
				{
					Instance = null;
				}
			}
		}

		internal void DispatchPlayerStarted(Player player)
		{
			DispatchInfrastructure("PlayerStart", delegate
			{
				_appearanceService?.NotifyPlayerReady(player);
				_networkService?.NotifyPlayerReady(player);
				if (_enableStage0Probe != null && _enableStage0Probe.Value)
				{
					_tracker?.NotifyPlayerStarted(player);
				}
			});
		}

		internal void DispatchSkinStarted(PlayerSkin skin)
		{
			Dispatch("SkinStart", delegate
			{
				_tracker?.NotifySkinStarted(skin);
			});
		}

		internal void DispatchPlayerStopping(Player player)
		{
			DispatchInfrastructure("PlayerStop", delegate
			{
				_appearanceService?.NotifyPlayerStopping(player);
				if (_enableStage0Probe != null && _enableStage0Probe.Value)
				{
					_tracker?.NotifyPlayerStopping(player);
				}
			});
		}

		internal void DispatchLocalSkinPreview(bool visible)
		{
			DispatchInfrastructure("LocalSkinPreview", delegate
			{
				if (visible)
				{
					_localSkinPreviewVisuals?.Show();
				}
				else
				{
					_localSkinPreviewVisuals?.CloseAndSave();
				}
			});
		}

		internal void DispatchLocalSkinReady()
		{
			DispatchInfrastructure("LocalSkinReady", delegate
			{
				_localSkinPreviewVisuals?.RestorePersistedSelection();
			});
		}

		internal bool HandleNativeCosmeticArrow(CosmeticSlot slot, int amount)
		{
			try
			{
				return _localSkinPreviewVisuals != null && _localSkinPreviewVisuals.HandleArrow(slot, amount);
			}
			catch (Exception exception)
			{
				ReportPatchException("NativeCosmeticArrowPrefix", exception);
				return false;
			}
		}

		internal void NotifyNativeCosmeticArrowCompleted(CosmeticSlot slot, int amount, byte beforeIndex, byte afterIndex)
		{
			try
			{
				_localSkinPreviewVisuals?.OnNativeArrowCompleted(slot, amount, beforeIndex, afterIndex);
			}
			catch (Exception exception)
			{
				ReportPatchException("NativeCosmeticArrowPostfix", exception);
			}
		}

		internal void NotifyActiveCosmeticSlot(CosmeticSlot slot)
		{
			try
			{
				_localSkinPreviewVisuals?.SetActiveSlot(slot);
			}
			catch (Exception exception)
			{
				ReportPatchException("ActiveCosmeticSlot", exception);
			}
		}

		internal void ReportPatchException(string source, Exception exception)
		{
			if (!_shuttingDown && exception != null)
			{
				_boundedLog?.ErrorOnce("exception-" + source, $"[HTFMoreHead][Stage0][ERROR] {source} probe failed safely: {exception}");
			}
		}

		private void Dispatch(string source, Action action)
		{
			if (_enableStage0Probe != null && _enableStage0Probe.Value)
			{
				DispatchInfrastructure(source, action);
			}
		}

		private void DispatchInfrastructure(string source, Action action)
		{
			if (_shuttingDown || _dispatcher == null || action == null)
			{
				return;
			}
			_dispatcher.RunOrEnqueue(delegate
			{
				try
				{
					action();
				}
				catch (Exception exception)
				{
					ReportPatchException(source, exception);
				}
			});
		}

		private void OnSceneUnloaded(Scene scene)
		{
			DispatchInfrastructure("SceneUnloaded", delegate
			{
				_tracker?.RequestPrune();
				_appearanceService?.PruneDestroyedPlayers();
			});
		}

		private void OnLocalSelectionCommitted(CosmeticLoadout loadout)
		{
			_networkService?.RequestLocalEquip(loadout);
		}

		private void OnStage0ProbeSettingChanged(object sender, EventArgs eventArgs)
		{
			bool flag = _enableStage0Probe != null && _enableStage0Probe.Value;
			Volatile.Write(ref _requestedProbeEnabled, flag ? 1 : 0);
			if (!flag)
			{
				Interlocked.Exchange(ref _probeDisableCleanupRequested, 1);
			}
			Interlocked.Exchange(ref _probeSettingChangePending, 1);
		}

		private void ApplyPendingProbeSettingChange()
		{
			if (Interlocked.Exchange(ref _probeDisableCleanupRequested, 0) != 0)
			{
				_tracker?.ClearAll("ProbeDisabled");
			}
			if (Interlocked.Exchange(ref _probeSettingChangePending, 0) != 0)
			{
				bool flag = Volatile.Read(in _requestedProbeEnabled) != 0;
				((BaseUnityPlugin)this).Logger.LogInfo((object)$"[HTFMoreHead][Stage0][CONFIG] EnableStage0Probe={flag}.");
			}
		}

		private bool ValidatePatchTargets()
		{
			MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(Player), "OnStartClient", Type.EmptyTypes, (Type[])null);
			MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(PlayerSkin), "OnStartClient", Type.EmptyTypes, (Type[])null);
			MethodInfo methodInfo3 = AccessTools.DeclaredMethod(typeof(Player), "OnStopClient", Type.EmptyTypes, (Type[])null);
			bool result = methodInfo != null && methodInfo2 != null && methodInfo3 != null;
			((BaseUnityPlugin)this).Logger.LogInfo((object)($"[HTFMoreHead][Stage0][PATCH_CHECK] PlayerStart={methodInfo != null}, " + $"SkinStart={methodInfo2 != null}, PlayerStop={methodInfo3 != null}, " + $"ThreadId={Thread.CurrentThread.ManagedThreadId}."));
			return result;
		}
	}
	internal static class RuntimeMetadata
	{
		internal const string PluginGuid = "com.htfmodding.morehead";

		internal const string PluginName = "HTF MoreHead";

		internal const string PluginVersion = "0.6.3";

		internal const byte ProtocolVersion = 3;

		internal const string ExpectedGameVersion = "1.0.11";

		internal const string ExpectedAssemblyCSharpSha256 = "349126D96D66B290341CBDA1628C746995210097C1A415652FA8961D9A20CFA6";

		internal const string ExpectedAssemblyCSharpMvid = "a79eca11-9a20-409b-ab30-d5f7ec1cd857";

		internal const string ExpectedFishNetSha256 = "71AC44A95C7AF0CD9333C851F41792EF342B373AF26E10F69795E02C9550C61A";

		internal const string ExpectedFishNetMvid = "5254e175-6a7c-489e-beb8-55179113ab9f";
	}
}
namespace HowToFish.MoreHead.Visuals
{
	internal sealed class LocalSkinPreviewVisuals
	{
		private static readonly CosmeticSlot[] AllSlots = new CosmeticSlot[3]
		{
			CosmeticSlot.Hat,
			CosmeticSlot.Accessory,
			CosmeticSlot.Outfit
		};

		private static readonly FieldInfo LocalSkinInstanceField = AccessTools.Field(typeof(LocalSkin), "_instance");

		private static readonly FieldInfo LocalHatRendererField = AccessTools.Field(typeof(LocalSkin), "_hatRenderer");

		private static readonly FieldInfo LocalAccessoryRendererField = AccessTools.Field(typeof(LocalSkin), "_accessoryRenderer");

		private static readonly FieldInfo LocalOutfitRendererField = AccessTools.Field(typeof(LocalSkin), "_outfitRenderer");

		private static readonly FieldInfo LocalHatIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalHatIndex>k__BackingField");

		private static readonly FieldInfo LocalAccessoryIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalAccessoryIndex>k__BackingField");

		private static readonly FieldInfo LocalOutfitIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalOutfitIndex>k__BackingField");

		private static readonly FieldInfo CanvasManagerInstanceField = AccessTools.Field(typeof(CanvasManager), "_instance");

		private static readonly FieldInfo VersionTextField = AccessTools.Field(typeof(CanvasManager), "_versionText");

		private readonly ManualLogSource _logger;

		private readonly CosmeticCatalog _catalog;

		private readonly LocalLoadoutStore _loadoutStore;

		private readonly GameObject[] _instances = (GameObject[])(object)new GameObject[3];

		private readonly int[] _customIndices = new int[3] { -1, -1, -1 };

		private readonly TextMeshProUGUI[] _selectionTitles = (TextMeshProUGUI[])(object)new TextMeshProUGUI[3];

		private readonly bool[] _selectorFallbackLogged = new bool[3];

		private CosmeticLoadout _selected;

		private bool _previewOpen;

		internal event Action<CosmeticLoadout> SelectionCommitted;

		internal LocalSkinPreviewVisuals(ManualLogSource logger, CosmeticCatalog catalog, LocalLoadoutStore loadoutStore)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_catalog = catalog ?? throw new ArgumentNullException("catalog");
			_loadoutStore = loadoutStore ?? throw new ArgumentNullException("loadoutStore");
			_selected = _loadoutStore.GetLoadout();
		}

		internal void Show()
		{
			_previewOpen = true;
			DestroyAllCustom();
			_selected = _loadoutStore.GetLoadout();
			ApplySavedSelections();
			RefreshSelectionTitles();
		}

		internal void RestorePersistedSelection()
		{
			_selected = _loadoutStore.GetLoadout();
			DestroyAllCustom();
			ApplySavedSelections();
			HideSelectionTitles();
			_logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Restored Hat='" + DisplayId(_selected.HatId) + "', Accessory='" + DisplayId(_selected.AccessoryId) + "', Outfit='" + DisplayId(_selected.OutfitId) + "'."));
		}

		internal void SetActiveSlot(CosmeticSlot slot)
		{
			if (_previewOpen)
			{
				RefreshSelectionTitle(slot);
			}
		}

		internal bool HandleArrow(CosmeticSlot slot, int amount)
		{
			int count = _catalog.GetCount(slot);
			if (!_previewOpen || _customIndices[(uint)slot] < 0 || count == 0 || amount == 0)
			{
				return false;
			}
			int num = ((amount > 0) ? 1 : (-1));
			int num2 = _customIndices[(uint)slot] + num;
			if (num2 >= 0 && num2 < count)
			{
				_customIndices[(uint)slot] = num2;
				ShowDefinition(slot, _catalog.GetAt(slot, num2), showTitle: true);
				ColorPicker.UpdateSelectedPicker();
				return true;
			}
			ExitCustomToNative(slot, num);
			return true;
		}

		internal void OnNativeArrowCompleted(CosmeticSlot slot, int amount, byte beforeIndex, byte afterIndex)
		{
			int count = _catalog.GetCount(slot);
			if (_previewOpen && _customIndices[(uint)slot] < 0 && count != 0 && amount != 0)
			{
				_selected.Set(slot, string.Empty);
				bool flag = amount > 0 && afterIndex <= beforeIndex;
				bool flag2 = amount < 0 && afterIndex >= beforeIndex;
				if (!flag && !flag2)
				{
					RefreshSelectionTitle(slot);
					return;
				}
				_customIndices[(uint)slot] = ((!flag) ? (count - 1) : 0);
				ShowDefinition(slot, _catalog.GetAt(slot, _customIndices[(uint)slot]), showTitle: true);
				_logger.LogInfo((object)($"[HTFMoreHead][NATIVE_LIST] Entered custom {slot}. Direction={amount}, " + $"NativeBefore={beforeIndex}, NativeAfter={afterIndex}, CustomIndex={_customIndices[(uint)slot]}."));
			}
		}

		internal void CloseAndSave()
		{
			_previewOpen = false;
			HideSelectionTitles();
			_loadoutStore.Save(_selected);
			this.SelectionCommitted?.Invoke(_selected.Clone());
		}

		internal void Dispose()
		{
			_loadoutStore.Save(_selected);
			_previewOpen = false;
			DestroyAllCustom();
			DestroySelectionTitles();
		}

		private void ApplySavedSelections()
		{
			for (int i = 0; i < AllSlots.Length; i++)
			{
				CosmeticSlot cosmeticSlot = AllSlots[i];
				int index = _catalog.GetIndex(cosmeticSlot, _selected.Get(cosmeticSlot));
				_customIndices[(uint)cosmeticSlot] = index;
				if (index >= 0)
				{
					ShowDefinition(cosmeticSlot, _catalog.GetAt(cosmeticSlot, index), showTitle: false);
				}
				else
				{
					_selected.Set(cosmeticSlot, string.Empty);
				}
			}
		}

		private void ShowDefinition(CosmeticSlot slot, CosmeticDefinition definition, bool showTitle)
		{
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			if (definition == null || definition.Slot != slot)
			{
				return;
			}
			DestroyCustom(slot);
			SetNativeBackingToNone(slot);
			Transform val = ResolvePreviewTarget(slot);
			if ((Object)(object)val == (Object)null)
			{
				_logger.LogWarning((object)$"[HTFMoreHead][PREVIEW] {slot} preview target is unavailable.");
				return;
			}
			GameObject val2 = Object.Instantiate<GameObject>(definition.Prefab, val, false);
			((Object)val2).name = "HTFMoreHead_Preview_" + definition.Id;
			val2.transform.localPosition = definition.Entry.localPosition.ToVector3();
			val2.transform.localEulerAngles = definition.Entry.localEulerAngles.ToVector3();
			val2.transform.localScale = definition.Entry.localScale.ToVector3();
			SetLayerRecursively(val2.transform, ((Component)val).gameObject.layer);
			_instances[(uint)slot] = val2;
			_selected.Set(slot, definition.Id);
			if (showTitle)
			{
				ShowSelectionTitle(slot, definition);
			}
		}

		private void RefreshSelectionTitles()
		{
			for (int i = 0; i < AllSlots.Length; i++)
			{
				RefreshSelectionTitle(AllSlots[i]);
			}
		}

		private void RefreshSelectionTitle(CosmeticSlot slot)
		{
			int index = _customIndices[(uint)slot];
			ShowSelectionTitle(slot, _catalog.GetAt(slot, index));
		}

		private void ShowSelectionTitle(CosmeticSlot slot, CosmeticDefinition definition)
		{
			if (definition == null)
			{
				HideSelectionTitle(slot);
				return;
			}
			TextMeshProUGUI val = EnsureSelectionTitle(slot);
			if (!((Object)(object)val == (Object)null))
			{
				string text = (string.IsNullOrWhiteSpace(definition.Entry.displayName) ? definition.Id : definition.Entry.displayName.Trim());
				string text2 = definition.AuthorName?.Trim();
				((TMP_Text)val).text = (string.IsNullOrEmpty(text2) ? text : (text + " By " + text2));
				((Component)val).gameObject.SetActive(true);
				((TMP_Text)val).transform.SetAsLastSibling();
			}
		}

		private TextMeshProUGUI EnsureSelectionTitle(CosmeticSlot slot)
		{
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c7: Unknown result type (might be due to invalid IL or missing references)
			int num = (int)slot;
			if ((Object)(object)_selectionTitles[num] != (Object)null)
			{
				AlignSelectionTitle(slot, _selectionTitles[num]);
				return _selectionTitles[num];
			}
			object obj = CanvasManagerInstanceField?.GetValue(null);
			TextMeshProUGUI val = (TextMeshProUGUI)((obj != null) ? /*isinst with value type is only supported in some contexts*/: null);
			Canvas val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponentInParent<Canvas>() : null);
			if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null)
			{
				_logger.LogWarning((object)"[HTFMoreHead][TITLE] Full-screen Canvas is unavailable.");
				return null;
			}
			val2 = val2.rootCanvas;
			GameObject val3 = new GameObject("HTFMoreHead_" + slot.ToString() + "SelectionTitle", new Type[3]
			{
				typeof(RectTransform),
				typeof(CanvasRenderer),
				typeof(TextMeshProUGUI)
			})
			{
				layer = ((Component)val).gameObject.layer
			};
			val3.transform.SetParent(((Component)val2).transform, false);
			TextMeshProUGUI component = val3.GetComponent<TextMeshProUGUI>();
			_selectionTitles[num] = component;
			((TMP_Text)component).font = ((TMP_Text)val).font;
			((TMP_Text)component).fontSharedMaterial = ((TMP_Text)val).fontSharedMaterial;
			((TMP_Text)component).fontSize = 30f;
			((TMP_Text)component).fontStyle = (FontStyles)1;
			((TMP_Text)component).alignment = (TextAlignmentOptions)513;
			((TMP_Text)component).textWrappingMode = (TextWrappingModes)0;
			((TMP_Text)component).overflowMode = (TextOverflowModes)1;
			((Graphic)component).raycastTarget = false;
			((TMP_Text)component).enableVertexGradient = true;
			((TMP_Text)component).colorGradient = new VertexGradient(new Color(1f, 0.25f, 0.35f), new Color(1f, 0.85f, 0.2f), new Color(0.2f, 0.9f, 1f), new Color(0.85f, 0.3f, 1f));
			AlignSelectionTitle(slot, component);
			return component;
		}

		private void AlignSelectionTitle(CosmeticSlot slot, TextMeshProUGUI title)
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: 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_0091: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			RectTransform rectTransform = ((TMP_Text)title).rectTransform;
			Canvas componentInParent = ((Component)title).GetComponentInParent<Canvas>();
			Canvas val = ((componentInParent != null) ? componentInParent.rootCanvas : null);
			if ((Object)(object)val != (Object)null && TryGetSelectorRightEdge(slot, val, out var localRightEdge))
			{
				Transform transform = ((Component)val).transform;
				RectTransform val2 = (RectTransform)(object)((transform is RectTransform) ? transform : null);
				float num;
				if (!((Object)(object)val2 != (Object)null))
				{
					num = 340f;
				}
				else
				{
					Rect rect = val2.rect;
					num = ((Rect)(ref rect)).xMax - localRightEdge.x - 18f;
				}
				float num2 = num;
				rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
				rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
				rectTransform.pivot = new Vector2(0f, 0.5f);
				rectTransform.anchoredPosition = localRightEdge + new Vector2(18f, 0f);
				rectTransform.sizeDelta = new Vector2(Mathf.Clamp(num2, 180f, 420f), 52f);
			}
			else
			{
				ApplyFallbackTitlePosition(slot, rectTransform);
				if (!_selectorFallbackLogged[(uint)slot])
				{
					_selectorFallbackLogged[(uint)slot] = true;
					_logger.LogWarning((object)$"[HTFMoreHead][TITLE] Could not resolve native {slot} arrow row; using fallback position.");
				}
			}
		}

		private static bool TryGetSelectorRightEdge(CosmeticSlot slot, Canvas canvas, out Vector2 localRightEdge)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0167: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Unknown result type (might be due to invalid IL or missing references)
			//IL_011e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_012b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			localRightEdge = default(Vector2);
			Transform transform = ((Component)canvas).transform;
			RectTransform val = (RectTransform)(object)((transform is RectTransform) ? transform : null);
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			string selectorMethodName = GetSelectorMethodName(slot);
			Camera val2 = (((int)canvas.renderMode == 0) ? null : canvas.worldCamera);
			Button[] array = Object.FindObjectsByType<Button>((FindObjectsInactive)1);
			float num = float.NegativeInfinity;
			float num2 = 0f;
			bool flag = false;
			Vector3[] array2 = (Vector3[])(object)new Vector3[4];
			foreach (Button val3 in array)
			{
				Canvas val4 = (((Object)(object)val3 != (Object)null) ? ((Component)val3).GetComponentInParent<Canvas>() : null);
				if ((Object)(object)val3 == (Object)null || !((Component)val3).gameObject.activeInHierarchy || (Object)(object)val4 == (Object)null || (Object)(object)val4.rootCanvas != (Object)(object)canvas || !InvokesMethod(val3, selectorMethodName))
				{
					continue;
				}
				Transform transform2 = ((Component)val3).transform;
				RectTransform val5 = (RectTransform)(object)((transform2 is RectTransform) ? transform2 : null);
				if ((Object)(object)val5 == (Object)null)
				{
					continue;
				}
				val5.GetWorldCorners(array2);
				Rect rect = val5.rect;
				Vector2 val6 = RectTransformUtility.WorldToScreenPoint(val2, ((Transform)val5).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center)));
				for (int j = 0; j < array2.Length; j++)
				{
					Vector2 val7 = RectTransformUtility.WorldToScreenPoint(val2, array2[j]);
					if (!(val7.x <= num))
					{
						num = val7.x;
						num2 = val6.y;
						flag = true;
					}
				}
			}
			if (flag)
			{
				return RectTransformUtility.ScreenPointToLocalPointInRectangle(val, new Vector2(num, num2), val2, ref localRightEdge);
			}
			return false;
		}

		private static bool InvokesMethod(Button button, string methodName)
		{
			ButtonClickedEvent onClick = button.onClick;
			for (int i = 0; i < ((UnityEventBase)onClick).GetPersistentEventCount(); i++)
			{
				if (string.Equals(((UnityEventBase)onClick).GetPersistentMethodName(i), methodName, StringComparison.Ordinal))
				{
					return true;
				}
			}
			return false;
		}

		private static string GetSelectorMethodName(CosmeticSlot slot)
		{
			return slot switch
			{
				CosmeticSlot.Accessory => "ChangeAccessoryIndex", 
				CosmeticSlot.Outfit => "ChangeOutfitIndex", 
				_ => "ChangeHatIndex", 
			};
		}

		private static void ApplyFallbackTitlePosition(CosmeticSlot slot, RectTransform rect)
		{
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: 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)
			float num = slot switch
			{
				CosmeticSlot.Accessory => 0.56f, 
				CosmeticSlot.Outfit => 0.39f, 
				_ => 0.67f, 
			};
			rect.anchorMin = new Vector2(0.81f, num);
			rect.anchorMax = new Vector2(0.81f, num);
			rect.pivot = new Vector2(0f, 0.5f);
			rect.anchoredPosition = Vector2.zero;
			rect.sizeDelta = new Vector2(340f, 52f);
		}

		private void HideSelectionTitle(CosmeticSlot slot)
		{
			TextMeshProUGUI val = _selectionTitles[(uint)slot];
			if ((Object)(object)val != (Object)null)
			{
				((Component)val).gameObject.SetActive(false);
			}
		}

		private void HideSelectionTitles()
		{
			for (int i = 0; i < AllSlots.Length; i++)
			{
				HideSelectionTitle(AllSlots[i]);
			}
		}

		private void DestroySelectionTitles()
		{
			for (int i = 0; i < _selectionTitles.Length; i++)
			{
				if ((Object)(object)_selectionTitles[i] != (Object)null)
				{
					Object.Destroy((Object)(object)((Component)_selectionTitles[i]).gameObject);
				}
				_selectionTitles[i] = null;
				_selectorFallbackLogged[i] = false;
			}
		}

		private Transform ResolvePreviewTarget(CosmeticSlot slot)
		{
			switch (slot)
			{
			case CosmeticSlot.Accessory:
			{
				Renderer obj2 = ResolveNativeRenderer(CosmeticSlot.Accessory);
				if (obj2 == null)
				{
					return null;
				}
				return ((Component)obj2).transform;
			}
			case CosmeticSlot.Outfit:
			{
				Renderer obj = ResolveNativeRenderer(CosmeticSlot.Outfit);
				if (obj == null)
				{
					return null;
				}
				return ((Component)obj).transform;
			}
			default:
			{
				Transform hatTarget = LocalSkin.HatTarget;
				if ((Object)(object)hatTarget == (Object)null)
				{
					return null;
				}
				Transform parent = hatTarget.parent;
				Transform val = (((Object)(object)parent != (Object)null) ? parent.Find("Armature/Body/Head") : null);
				if (!((Object)(object)val != (Object)null))
				{
					return hatTarget;
				}
				return val;
			}
			}
		}

		private void ExitCustomToNative(CosmeticSlot slot, int direction)
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			DestroyCustom(slot);
			_customIndices[(uint)slot] = -1;
			_selected.Set(slot, string.Empty);
			HideSelectionTitle(slot);
			if (direction < 0)
			{
				LocalSkin.ChangeMesh(-1, ToPlayerSkinType(slot));
			}
			ColorPicker.UpdateSelectedPicker();
			_logger.LogInfo((object)($"[HTFMoreHead][NATIVE_LIST] Returned to native {slot}. " + $"Direction={direction}, NativeIndex={GetNativeIndex(slot)}."));
		}

		private static Renderer ResolveNativeRenderer(CosmeticSlot slot)
		{
			object obj = LocalSkinInstanceField?.GetValue(null);
			if (obj == null)
			{
				return null;
			}
			return (Renderer)(slot switch
			{
				CosmeticSlot.Accessory => (object)/*isinst with value type is only supported in some contexts*/, 
				CosmeticSlot.Outfit => (object)/*isinst with value type is only supported in some contexts*/, 
				_ => (object)/*isinst with value type is only supported in some contexts*/, 
			});
		}

		private static void SetNativeBackingToNone(CosmeticSlot slot)
		{
			Renderer obj = ResolveNativeRenderer(slot);
			SkinnedMeshRenderer val = (SkinnedMeshRenderer)(object)((obj is SkinnedMeshRenderer) ? obj : null);
			switch (slot)
			{
			case CosmeticSlot.Accessory:
				LocalAccessoryIndexField?.SetValue(null, (byte)0);
				if ((Object)(object)val != (Object)null)
				{
					val.sharedMesh = SkinManager.GetAccessory((byte)0);
				}
				break;
			case CosmeticSlot.Outfit:
				LocalOutfitIndexField?.SetValue(null, (byte)0);
				if ((Object)(object)val != (Object)null)
				{
					val.sharedMesh = SkinManager.GetOutfit((byte)0);
				}
				break;
			default:
				LocalHatIndexField?.SetValue(null, (byte)0);
				if ((Object)(object)val != (Object)null)
				{
					val.sharedMesh = SkinManager.GetHat((byte)0);
				}
				break;
			}
		}

		private static PlayerSkinType ToPlayerSkinType(CosmeticSlot slot)
		{
			return (PlayerSkinType)(slot switch
			{
				CosmeticSlot.Accessory => 7, 
				CosmeticSlot.Outfit => 1, 
				_ => 4, 
			});
		}

		private static byte GetNativeIndex(CosmeticSlot slot)
		{
			return slot switch
			{
				CosmeticSlot.Accessory => LocalSkin.LocalAccessoryIndex, 
				CosmeticSlot.Outfit => LocalSkin.LocalOutfitIndex, 
				_ => LocalSkin.LocalHatIndex, 
			};
		}

		private void DestroyCustom(CosmeticSlot slot)
		{
			if ((Object)(object)_instances[(uint)slot] != (Object)null)
			{
				Object.Destroy((Object)(object)_instances[(uint)slot]);
			}
			_instances[(uint)slot] = null;
		}

		private void DestroyAllCustom()
		{
			for (int i = 0; i < AllSlots.Length; i++)
			{
				DestroyCustom(AllSlots[i]);
			}
		}

		private static void SetLayerRecursively(Transform root, int layer)
		{
			((Component)root).gameObject.layer = layer;
			for (int i = 0; i < root.childCount; i++)
			{
				SetLayerRecursively(root.GetChild(i), layer);
			}
		}

		private static string DisplayId(string value)
		{
			if (!string.IsNullOrEmpty(value))
			{
				return value;
			}
			return "<native>";
		}
	}
	internal sealed class PlayerAppearanceService
	{
		private sealed class PlayerVisual
		{
			internal int ClientId;

			internal ulong PlayerEntityId;

			internal Player Player;

			internal SlotVisual[] Slots;
		}

		private sealed class SlotVisual
		{
			internal readonly Transform Anchor;

			internal string AppliedId = string.Empty;

			internal GameObject Instance;

			internal SlotVisual(Transform anchor)
			{
				Anchor = anchor;
			}
		}

		private static readonly CosmeticSlot[] AllSlots = new CosmeticSlot[3]
		{
			CosmeticSlot.Hat,
			CosmeticSlot.Accessory,
			CosmeticSlot.Outfit
		};

		private static readonly FieldInfo RemoteHatRendererField = AccessTools.Field(typeof(PlayerSkin), "_hatRenderer");

		private static readonly FieldInfo RemoteAccessoryRendererField = AccessTools.Field(typeof(PlayerSkin), "_accessoryRenderer");

		private static readonly FieldInfo RemoteOutfitRendererField = AccessTools.Field(typeof(PlayerSkin), "_outfitRenderer");

		private readonly ManualLogSource _logger;

		private readonly BoundedLog _boundedLog;

		private readonly CosmeticCatalog _catalog;

		private readonly Dictionary<int, PlayerVisual> _visuals = new Dictionary<int, PlayerVisual>();

		private readonly Dictionary<ulong, int> _clientIdByPlayer = new Dictionary<ulong, int>();

		private readonly Dictionary<int, CosmeticLoadout> _loadoutByClientId = new Dictionary<int, CosmeticLoadout>();

		private readonly List<int> _scratchClientIds = new List<int>();

		private int _localClientId = -1;

		internal PlayerAppearanceService(ManualLogSource logger, BoundedLog boundedLog, CosmeticCatalog catalog)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog");
			_catalog = catalog ?? throw new ArgumentNullException("catalog");
		}

		internal void NotifyPlayerReady(Player player)
		{
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null)
			{
				return;
			}
			NetworkConnection owner = ((NetworkBehaviour)player).Owner;
			if (owner == (NetworkConnection)null || !owner.IsValid)
			{
				return;
			}
			if (owner.IsLocalClient)
			{
				SetLocalClientId(owner.ClientId);
				return;
			}
			PlayerBody body = player.Body;
			Transform val = (((Object)(object)body != (Object)null) ? body.Head : null);
			if ((Object)(object)val == (Object)null)
			{
				_boundedLog.WarningOnce("appearance-head-missing-" + EntityId.ToULong(((Object)player).GetEntityId()), "[HTFMoreHead][VISUAL] Remote Player.Body.Head is unavailable.");
				return;
			}
			PlayerSkin skin = player.Skin;
			GetRenderer(RemoteHatRendererField, skin);
			Renderer renderer = GetRenderer(RemoteAccessoryRendererField, skin);
			Renderer renderer2 = GetRenderer(RemoteOutfitRendererField, skin);
			int clientId = owner.ClientId;
			ulong num = EntityId.ToULong(((Object)player).GetEntityId());
			RemoveVisual(clientId);
			_visuals[clientId] = new PlayerVisual
			{
				ClientId = clientId,
				PlayerEntityId = num,
				Player = player,
				Slots = new SlotVisual[3]
				{
					new SlotVisual(val),
					new SlotVisual(((Object)(object)renderer != (Object)null) ? ((Component)renderer).transform : null),
					new SlotVisual(((Object)(object)renderer2 != (Object)null) ? ((Component)renderer2).transform : null)
				}
			};
			_clientIdByPlayer[num] = clientId;
			ApplyAll(clientId);
			_logger.LogInfo((object)($"[HTFMoreHead][PLAYER_READY] ClientId={clientId}, PlayerEntity={num}, " + "HatAnchor='" + ((Object)val).name + "', AccessoryAnchor='" + NameOf(renderer) + "', OutfitAnchor='" + NameOf(renderer2) + "'."));
		}

		internal void NotifyPlayerStopping(Player player)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)player == (Object)null))
			{
				ulong key = EntityId.ToULong(((Object)player).GetEntityId());
				if (_clientIdByPlayer.TryGetValue(key, out var value))
				{
					RemovePlayer(value, clearLoadout: true);
				}
			}
		}

		internal void SetLoadout(int clientId, CosmeticLoadout loadout)
		{
			if (clientId >= 0)
			{
				if (clientId == _localClientId)
				{
					_loadoutByClientId.Remove(clientId);
					return;
				}
				_loadoutByClientId[clientId] = (loadout ?? new CosmeticLoadout()).Clone();
				ApplyAll(clientId);
			}
		}

		internal void ReplaceSnapshot(IReadOnlyDictionary<int, CosmeticLoadout> snapshot)
		{
			_loadoutByClientId.Clear();
			if (snapshot != null)
			{
				foreach (KeyValuePair<int, CosmeticLoadout> item in snapshot)
				{
					if (item.Key >= 0 && item.Key != _localClientId)
					{
						_loadoutByClientId[item.Key] = (item.Value ?? new CosmeticLoadout()).Clone();
					}
				}
			}
			_scratchClientIds.Clear();
			foreach (int key in _visuals.Keys)
			{
				_scratchClientIds.Add(key);
			}
			for (int i = 0; i < _scratchClientIds.Count; i++)
			{
				ApplyAll(_scratchClientIds[i]);
			}
			_logger.LogInfo((object)($"[HTFMoreHead][SNAPSHOT_VISUALS] LocalClientId={_localClientId}, " + $"RemoteStates={_loadoutByClientId.Count}, RemotePlayers={_visuals.Count}."));
		}

		internal void RemovePlayer(int clientId, bool clearLoadout)
		{
			RemoveVisual(clientId);
			if (clearLoadout)
			{
				_loadoutByClientId.Remove(clientId);
			}
		}

		internal void PruneDestroyedPlayers()
		{
			_scratchClientIds.Clear();
			foreach (KeyValuePair<int, PlayerVisual> visual in _visuals)
			{
				if ((Object)(object)visual.Value.Player == (Object)null || (Object)(object)visual.Value.Slots[0].Anchor == (Object)null)
				{
					_scratchClientIds.Add(visual.Key);
				}
			}
			for (int i = 0; i < _scratchClientIds.Count; i++)
			{
				RemoveVisual(_scratchClientIds[i]);
			}
		}

		internal void ClearAll()
		{
			_scratchClientIds.Clear();
			foreach (int key in _visuals.Keys)
			{
				_scratchClientIds.Add(key);
			}
			for (int i = 0; i < _scratchClientIds.Count; i++)
			{
				RemoveVisual(_scratchClientIds[i]);
			}
			_loadoutByClientId.Clear();
			_clientIdByPlayer.Clear();
			_localClientId = -1;
		}

		private void SetLocalClientId(int clientId)
		{
			if (clientId >= 0)
			{
				if (_localClientId >= 0 && _localClientId != clientId)
				{
					_loadoutByClientId.Remove(_localClientId);
				}
				_localClientId = clientId;
				_loadoutByClientId.Remove(clientId);
				_logger.LogInfo((object)$"[HTFMoreHead][LOCAL_ID] LocalClientId={_localClientId}.");
			}
		}

		private void ApplyAll(int clientId)
		{
			for (int i = 0; i < AllSlots.Length; i++)
			{
				Apply(clientId, AllSlots[i]);
			}
		}

		private void Apply(int clientId, CosmeticSlot slot)
		{
			//IL_0129: Unknown result type (might be due to invalid IL or missing references)
			//IL_0146: Unknown result type (might be due to invalid IL or missing references)
			//IL_0163: Unknown result type (might be due to invalid IL or missing references)
			if (!_visuals.TryGetValue(clientId, out var value) || (Object)(object)value.Player == (Object)null)
			{
				return;
			}
			SlotVisual slotVisual = value.Slots[(uint)slot];
			if ((Object)(object)slotVisual.Anchor == (Object)null)
			{
				return;
			}
			CosmeticLoadout value2;
			string text = (_loadoutByClientId.TryGetValue(clientId, out value2) ? value2.Get(slot) : string.Empty);
			if (string.Equals(slotVisual.AppliedId, text, StringComparison.Ordinal) && (string.IsNullOrEmpty(text) || (Object)(object)slotVisual.Instance != (Object)null))
			{
				return;
			}
			DestroyInstance(slotVisual);
			slotVisual.AppliedId = text;
			if (!string.IsNullOrEmpty(text))
			{
				int index = _catalog.GetIndex(slot, text);
				CosmeticDefinition at = _catalog.GetAt(slot, index);
				if (at == null)
				{
					_boundedLog.WarningOnce("missing-cosmetic-" + clientId + "-" + text, $"[HTFMoreHead][MISSING_PACK] ClientId={clientId}, Cosmetic='{text}' is not installed locally.");
					return;
				}
				GameObject val = Object.Instantiate<GameObject>(at.Prefab, slotVisual.Anchor, false);
				((Object)val).name = "HTFMoreHead_Player_" + at.Id;
				val.transform.localPosition = at.Entry.localPosition.ToVector3();
				val.transform.localEulerAngles = at.Entry.localEulerAngles.ToVector3();
				val.transform.localScale = at.Entry.localScale.ToVector3();
				SetLayerRecursively(val.transform, ((Component)slotVisual.Anchor).gameObject.layer);
				slotVisual.Instance = val;
				_logger.LogInfo((object)($"[HTFMoreHead][EQUIPPED] ClientId={clientId}, Slot={slot}, " + "Cosmetic='" + at.Id + "', Anchor='" + ((Object)slotVisual.Anchor).name + "'."));
			}
		}

		private void RemoveVisual(int clientId)
		{
			if (_visuals.TryGetValue(clientId, out var value))
			{
				for (int i = 0; i < value.Slots.Length; i++)
				{
					DestroyInstance(value.Slots[i]);
				}
				_visuals.Remove(clientId);
				if (_clientIdByPlayer.TryGetValue(value.PlayerEntityId, out var value2) && value2 == clientId)
				{
					_clientIdByPlayer.Remove(value.PlayerEntityId);
				}
			}
		}

		private static Renderer GetRenderer(FieldInfo field, PlayerSkin skin)
		{
			if (!((Object)(object)skin != (Object)null))
			{
				return null;
			}
			object? obj = field?.GetValue(skin);
			return (Renderer)((obj is Renderer) ? obj : null);
		}

		private static string NameOf(Renderer renderer)
		{
			if (!((Object)(object)renderer != (Object)null))
			{
				return "<missing>";
			}
			return ((Object)((Component)renderer).transform).name;
		}

		private static void DestroyInstance(SlotVisual visual)
		{
			if ((Object)(object)visual.Instance != (Object)null)
			{
				Object.Destroy((Object)(object)visual.Instance);
			}
			visual.Instance = null;
			visual.AppliedId = string.Empty;
		}

		private static void SetLayerRecursively(Transform root, int layer)
		{
			((Component)root).gameObject.layer = layer;
			for (int i = 0; i < root.childCount; i++)
			{
				SetLayerRecursively(root.GetChild(i), layer);
			}
		}
	}
}
namespace HowToFish.MoreHead.Players
{
	internal sealed class PlayerProbeTracker
	{
		private sealed class ProbeRecord
		{
			internal Player Player;

			internal PlayerSkin Skin;

			internal Transform Head;

			internal ulong PlayerInstanceId;

			internal int ClientId = -1;

			internal bool IsLocal;

			internal bool PlayerStarted;

			internal bool SkinStarted;

			internal bool Finalized;

			internal bool TimedOut;

			internal long PlayerStartSequence;

			internal long SkinStartSequence;

			internal string SkinResolveRoute = "unknown";

			internal float Deadline;
		}

		private sealed class PendingSkin
		{
			internal PlayerSkin Skin;

			internal ulong SkinInstanceId;

			internal long SkinStartSequence;

			internal float Deadline;
		}

		private readonly struct HierarchyNode
		{
			internal Transform Transform { get; }

			internal int Depth { get; }

			internal HierarchyNode(Transform transform, int depth)
			{
				Transform = transform;
				Depth = depth;
			}
		}

		private const float RetryIntervalSeconds = 0.1f;

		private const int HierarchyPreviewMaxDepth = 5;

		private const int HierarchyPreviewMaxNodes = 64;

		private readonly ManualLogSource _logger;

		private readonly BoundedLog _boundedLog;

		private readonly float _timeoutSeconds;

		private readonly Dictionary<ulong, ProbeRecord> _records = new Dictionary<ulong, ProbeRecord>();

		private readonly Dictionary<int, ulong> _playerInstanceByClientId = new Dictionary<int, ulong>();

		private readonly Dictionary<ulong, PendingSkin> _pendingSkins = new Dictionary<ulong, PendingSkin>();

		private readonly List<ulong> _scratchKeys = new List<ulong>();

		private long _signalSequence;

		private int _sessionGeneration = 1;

		private float _nextTickAt;

		private bool _pruneRequested;

		private bool _sessionHasActivity;

		internal int LiveCount => _records.Count;

		internal event Action<Player, Transform> RemotePlayerReady;

		internal event Action<Player> PlayerStopping;

		internal PlayerProbeTracker(ManualLogSource logger, BoundedLog boundedLog, float timeoutSeconds)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog");
			_timeoutSeconds = Math.Max(0.5f, timeoutSeconds);
		}

		internal void NotifyPlayerStarted(Player player)
		{
			if ((Object)(object)player == (Object)null)
			{
				_boundedLog.WarningOnce("null-player-start", "[HTFMoreHead][Stage0][PROBE] Player.OnStartClient supplied a null Unity object.");
				return;
			}
			_sessionHasActivity = true;
			ProbeRecord orCreateRecord = GetOrCreateRecord(player);
			if (!orCreateRecord.PlayerStarted)
			{
				orCreateRecord.PlayerStarted = true;
				orCreateRecord.PlayerStartSequence = ++_signalSequence;
				orCreateRecord.Deadline = Time.realtimeSinceStartup + _timeoutSeconds;
				_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=PlayerStart, Session={_sessionGeneration}, " + $"Sequence={orCreateRecord.PlayerStartSequence}, Frame={Time.frameCount}, " + $"ClientId={orCreateRecord.ClientId}, PlayerInstance={orCreateRecord.PlayerInstanceId}, " + $"ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, ComponentIndex={((NetworkBehaviour)player).ComponentIndex}."));
			}
			TryFinalize(orCreateRecord);
		}

		internal void NotifySkinStarted(PlayerSkin skin)
		{
			if ((Object)(object)skin == (Object)null)
			{
				_boundedLog.WarningOnce("null-skin-start", "[HTFMoreHead][Stage0][PROBE] PlayerSkin.OnStartClient supplied a null Unity object.");
				return;
			}
			_sessionHasActivity = true;
			string route;
			Player val = ResolvePlayer(skin, out route);
			if ((Object)(object)val == (Object)null)
			{
				ulong entityId = GetEntityId((Object)(object)skin);
				if (!_pendingSkins.ContainsKey(entityId))
				{
					_pendingSkins.Add(entityId, new PendingSkin
					{
						Skin = skin,
						SkinInstanceId = entityId,
						SkinStartSequence = ++_signalSequence,
						Deadline = Time.realtimeSinceStartup + _timeoutSeconds
					});
					_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=SkinStart, Session={_sessionGeneration}, " + $"Sequence={_signalSequence}, Frame={Time.frameCount}, SkinInstance={entityId}, " + "PlayerRoute=Pending."));
				}
			}
			else
			{
				RegisterSkinSignal(val, skin, route, ++_signalSequence);
			}
		}

		internal void NotifyPlayerStopping(Player player)
		{
			if (!((Object)(object)player == (Object)null))
			{
				this.PlayerStopping?.Invoke(player);
				ulong entityId = GetEntityId((Object)(object)player);
				int clientId = GetClientId((NetworkBehaviour)(object)player);
				bool flag = RemoveRecord(entityId);
				_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_STOP] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={clientId}, PlayerInstance={entityId}, Removed={flag}, " + $"Remaining={_records.Count}."));
				TryAdvanceSessionWhenEmpty("PlayerStop");
			}
		}

		internal void RequestPrune()
		{
			_pruneRequested = true;
		}

		internal void Tick()
		{
			if (_pruneRequested)
			{
				_pruneRequested = false;
				PruneDestroyedReferences();
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!(realtimeSinceStartup < _nextTickAt))
			{
				_nextTickAt = realtimeSinceStartup + 0.1f;
				ResolvePendingSkins(realtimeSinceStartup);
				RetryPendingRecords(realtimeSinceStartup);
			}
		}

		internal void ClearAll(string reason)
		{
			int count = _records.Count;
			int count2 = _pendingSkins.Count;
			_records.Clear();
			_playerInstanceByClientId.Clear();
			_pendingSkins.Clear();
			_scratchKeys.Clear();
			_sessionGeneration++;
			_sessionHasActivity = false;
			_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_CLEAR] Reason={reason}, Records={count}, " + $"PendingSkins={count2}, NextSession={_sessionGeneration}."));
		}

		private ProbeRecord GetOrCreateRecord(Player player)
		{
			ulong entityId = GetEntityId((Object)(object)player);
			if (!_records.TryGetValue(entityId, out var value))
			{
				value = new ProbeRecord
				{
					Player = player,
					PlayerInstanceId = entityId,
					Deadline = Time.realtimeSinceStartup + _timeoutSeconds
				};
				_records.Add(entityId, value);
			}
			RefreshIdentity(value);
			return value;
		}

		private void RefreshIdentity(ProbeRecord record)
		{
			int num = (record.ClientId = GetClientId((NetworkBehaviour)(object)record.Player));
			record.IsLocal = IsLocal((NetworkBehaviour)(object)record.Player);
			if (num >= 0)
			{
				if (_playerInstanceByClientId.TryGetValue(num, out var value) && value != record.PlayerInstanceId)
				{
					_boundedLog.WarningOnce($"client-reuse-{num}-{value}-{record.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE] ClientId={num} moved from " + $"PlayerInstance={value} to {record.PlayerInstanceId}; stale record removed.");
					RemoveRecord(value);
				}
				_playerInstanceByClientId[num] = record.PlayerInstanceId;
			}
		}

		private void RegisterSkinSignal(Player player, PlayerSkin skin, string route, long sequence)
		{
			ProbeRecord orCreateRecord = GetOrCreateRecord(player);
			if (!orCreateRecord.SkinStarted)
			{
				orCreateRecord.Skin = skin;
				orCreateRecord.SkinStarted = true;
				orCreateRecord.SkinStartSequence = sequence;
				orCreateRecord.SkinResolveRoute = route;
				orCreateRecord.Deadline = Time.realtimeSinceStartup + _timeoutSeconds;
				_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=SkinStart, Session={_sessionGeneration}, " + $"Sequence={sequence}, Frame={Time.frameCount}, ClientId={orCreateRecord.ClientId}, " + $"PlayerInstance={orCreateRecord.PlayerInstanceId}, SkinInstance={GetEntityId((Object)(object)skin)}, " + $"ObjectId={GetObjectId((NetworkBehaviour)(object)skin)}, ComponentIndex={((NetworkBehaviour)skin).ComponentIndex}, PlayerRoute={route}."));
			}
			TryFinalize(orCreateRecord);
		}

		private bool TryFinalize(ProbeRecord record)
		{
			//IL_0268: Unknown result type (might be due to invalid IL or missing references)
			//IL_0283: Unknown result type (might be due to invalid IL or missing references)
			//IL_029b: Unknown result type (might be due to invalid IL or missing references)
			if (record == null || record.Finalized || record.TimedOut || !record.PlayerStarted || !record.SkinStarted)
			{
				return false;
			}
			Player player = record.Player;
			PlayerSkin skin = record.Skin;
			if ((Object)(object)player == (Object)null || (Object)(object)skin == (Object)null || (Object)(object)player.Skin != (Object)(object)skin)
			{
				return false;
			}
			RefreshIdentity(record);
			if (record.ClientId < 0)
			{
				return false;
			}
			if (record.IsLocal)
			{
				record.Finalized = true;
				_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_LOCAL] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={record.ClientId}, ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, " + $"PlayerInstance={record.PlayerInstanceId}, PlayerStartSeq={record.PlayerStartSequence}, " + $"SkinStartSeq={record.SkinStartSequence}, PlayerRoute={record.SkinResolveRoute}, " + "ThirdPersonHead=Skipped."));
				return true;
			}
			PlayerBody body = player.Body;
			Transform val = (((Object)(object)body != (Object)null) ? body.Head : null);
			if ((Object)(object)body == (Object)null || (Object)(object)val == (Object)null)
			{
				return false;
			}
			bool flag = (Object)(object)val == (Object)(object)((Component)player).transform || val.IsChildOf(((Component)player).transform);
			record.Head = val;
			record.Finalized = true;
			_logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_HEAD] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={record.ClientId}, ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, IsLocal=False, " + $"IsBean={player.IsBean}, PlayerInstance={record.PlayerInstanceId}, " + $"HeadInstance={GetEntityId((Object)(object)val)}, PlayerStartSeq={record.PlayerStartSequence}, " + $"SkinStartSeq={record.SkinStartSequence}, PlayerRoute={record.SkinResolveRoute}, " + $"Path=\"{BuildHierarchyPath(val, ((Component)player).transform)}\", UnderPlayer={flag}, " + $"Active={((Component)val).gameObject.activeInHierarchy}, LocalPosition={FormatVector3(val.localPosition)}, " + "LocalEulerAngles=" + FormatVector3(val.localEulerAngles) + ", LocalScale=" + FormatVector3(val.localScale) + ", PlayerSkinMatched=True."));
			if (!flag)
			{
				_boundedLog.WarningOnce($"head-outside-player-{record.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE] Body.Head for ClientId={record.ClientId} is not below " + "the Player transform. Do not enter the attachment phase until this is explained.");
			}
			this.RemotePlayerReady?.Invoke(player, val);
			return true;
		}

		private void ResolvePendingSkins(float now)
		{
			if (_pendingSkins.Count == 0)
			{
				return;
			}
			_scratchKeys.Clear();
			foreach (KeyValuePair<ulong, PendingSkin> pendingSkin in _pendingSkins)
			{
				PendingSkin value = pendingSkin.Value;
				if ((Object)(object)value.Skin == (Object)null)
				{
					_scratchKeys.Add(pendingSkin.Key);
					continue;
				}
				string route;
				Player val = ResolvePlayer(value.Skin, out route);
				if ((Object)(object)val != (Object)null)
				{
					_scratchKeys.Add(pendingSkin.Key);
					RegisterSkinSignal(val, value.Skin, route, value.SkinStartSequence);
				}
				else if (now >= value.Deadline)
				{
					_boundedLog.WarningOnce($"skin-player-timeout-{value.SkinInstanceId}", $"[HTFMoreHead][Stage0][PROBE_TIMEOUT] SkinInstance={value.SkinInstanceId} " + $"could not resolve its Player within {_timeoutSeconds:F1}s.");
					_scratchKeys.Add(pendingSkin.Key);
				}
			}
			for (int i = 0; i < _scratchKeys.Count; i++)
			{
				_pendingSkins.Remove(_scratchKeys[i]);
			}
			TryAdvanceSessionWhenEmpty("PendingSkinResolvedOrExpired");
		}

		private void RetryPendingRecords(float now)
		{
			if (_records.Count == 0)
			{
				return;
			}
			_scratchKeys.Clear();
			foreach (ulong key in _records.Keys)
			{
				_scratchKeys.Add(key);
			}
			for (int i = 0; i < _scratchKeys.Count; i++)
			{
				if (_records.TryGetValue(_scratchKeys[i], out var value) && !value.Finalized && !value.TimedOut && !TryFinalize(value) && !(now < value.Deadline))
				{
					value.TimedOut = true;
					string text = (((Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null && (Object)(object)value.Player.Body.Head == (Object)null) ? BuildHierarchyPreview(((Component)value.Player).transform) : "<not-required>");
					_boundedLog.WarningOnce($"player-finalize-timeout-{value.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE_TIMEOUT] PlayerInstance={value.PlayerInstanceId}, " + $"ClientId={value.ClientId}, PlayerStarted={value.PlayerStarted}, " + $"SkinStarted={value.SkinStarted}, PlayerValid={(Object)(object)value.Player != (Object)null}, " + $"SkinValid={(Object)(object)value.Skin != (Object)null}, BodyValid={(Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null}, " + $"HeadValid={(Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null && (Object)(object)value.Player.Body.Head != (Object)null}, " + "HierarchyPreview=\"" + text + "\".");
				}
			}
		}

		private void PruneDestroyedReferences()
		{
			_scratchKeys.Clear();
			foreach (KeyValuePair<ulong, ProbeRecord> record in _records)
			{
				if ((Object)(object)record.Value.Player == (Object)null)
				{
					_scratchKeys.Add(record.Key);
				}
			}
			for (int i = 0; i < _scratchKeys.Count; i++)
			{
				RemoveRecord(_scratchKeys[i]);
			}
			_scratchKeys.Clear();
			foreach (KeyValuePair<ulong, PendingSkin> pendingSkin in _pendingSkins)
			{
				if ((Object)(object)pendingSkin.Value.Skin == (Object)null)
				{
					_scratchKeys.Add(pendingSkin.Key);
				}
			}
			for (int j = 0; j < _scratchKeys.Count; j++)
			{
				_pendingSkins.Remove(_scratchKeys[j]);
			}
			TryAdvanceSessionWhenEmpty("ScenePrune");
		}

		private void TryAdvanceSessionWhenEmpty(string reason)
		{
			if (_sessionHasActivity && _records.Count == 0 && _pendingSkins.Count == 0)
			{
				_sessionHasActivity = false;
				_sessionGeneration++;
				_logger.LogInfo((object)("[HTFMoreHead][Stage0][PROBE_SESSION] Reason=" + reason + ", " + $"NextSession={_sessionGeneration}."));
			}
		}

		private bool RemoveRecord(ulong playerInstanceId)
		{
			if (!_records.TryGetValue(playerInstanceId, out var value))
			{
				return false;
			}
			_records.Remove(playerInstanceId);
			if (value.ClientId >= 0 && _playerInstanceByClientId.TryGetValue(value.ClientId, out var value2) && value2 == playerInstanceId)
			{
				_playerInstanceByClientId.Remove(value.ClientId);
			}
			return true;
		}

		private static Player ResolvePlayer(PlayerSkin skin, out string route)
		{
			route = "none";
			if ((Object)(object)skin == (Object)null)
			{
				return null;
			}
			Player component = ((Component)skin).GetComponent<Player>();
			if (Matches(component, skin))
			{
				route = "same-game-object";
				return component;
			}
			if ((Object)(object)((NetworkBehaviour)skin).NetworkObject != (Object)null)
			{
				component = ((Component)((NetworkBehaviour)skin).NetworkObject).GetComponent<Player>();
				if (Matches(component, skin))
				{
					route = "network-object-root";
					return component;
				}
				IList<NetworkBehaviour> networkBehaviours = ((NetworkBehaviour)skin).NetworkObject.NetworkBehaviours;
				if (networkBehaviours != null)
				{
					for (int i = 0; i < networkBehaviours.Count; i++)
					{
						NetworkBehaviour obj = networkBehaviours[i];
						component = (Player)(object)((obj is Player) ? obj : null);
						if (Matches(component, skin))
						{
							route = "network-behaviours";
							return component;
						}
					}
				}
			}
			component = ((Component)skin).GetComponentInParent<Player>(true);
			if (Matches(component, skin))
			{
				route = "parent";
				return component;
			}
			return null;
		}

		private static bool Matches(Player player, PlayerSkin skin)
		{
			if ((Object)(object)player != (Object)null)
			{
				return (Object)(object)player.Skin == (Object)(object)skin;
			}
			return false;
		}

		private static int GetClientId(NetworkBehaviour behaviour)
		{
			if ((Object)(object)behaviour == (Object)null)
			{
				return -1;
			}
			NetworkConnection owner = behaviour.Owner;
			if (!(owner != (NetworkConnection)null) || !owner.IsValid)
			{
				return -1;
			}
			return owner.ClientId;
		}

		private static bool IsLocal(NetworkBehaviour behaviour)
		{
			if ((Object)(object)behaviour == (Object)null)
			{
				return false;
			}
			NetworkConnection owner = behaviour.Owner;
			if (owner != (NetworkConnection)null && owner.IsValid)
			{
				return owner.IsLocalClient;
			}
			return false;
		}

		private static int GetObjectId(NetworkBehaviour behaviour)
		{
			if (!((Object)(object)behaviour != (Object)null) || !((Object)(object)behaviour.NetworkObject != (Object)null))
			{
				return -1;
			}
			return behaviour.NetworkObject.ObjectId;
		}

		private static ulong GetEntityId(Object value)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			if (!(value != (Object)null))
			{
				return 0uL;
			}
			return EntityId.ToULong(value.GetEntityId());
		}

		private static string BuildHierarchyPath(Transform target, Transform playerRoot)
		{
			if ((Object)(object)target == (Object)null)
			{
				return "<null>";
			}
			List<string> list = new List<string>();
			Transform val = target;
			bool flag = false;
			while ((Object)(object)val != (Object)null)
			{
				list.Add($"{((Object)val).name}[{val.GetSiblingIndex()}]");
				if ((Object)(object)val == (Object)(object)playerRoot)
				{
					flag = true;
					break;
				}
				val = val.parent;
			}
			list.Reverse();
			string text = string.Join("/", list.ToArray());
			if (!flag)
			{
				return "<outside-player>/" + text;
			}
			return text;
		}

		private static string BuildHierarchyPreview(Transform root)
		{
			if ((Object)(object)root == (Object)null)
			{
				return "<null>";
			}
			StringBuilder stringBuilder = new StringBuilder();
			Queue<HierarchyNode> queue = new Queue<HierarchyNode>();
			queue.Enqueue(new HierarchyNode(root, 0));
			int num = 0;
			while (queue.Count > 0 && num < 64)
			{
				HierarchyNode hierarchyNode = queue.Dequeue();
				if (num > 0)
				{
					stringBuilder.Append(" | ");
				}
				stringBuilder.Append(hierarchyNode.Depth);
				stringBuilder.Append(':');
				stringBuilder.Append(((Object)hierarchyNode.Transform).name);
				stringBuilder.Append('[');
				stringBuilder.Append(hierarchyNode.Transform.GetSiblingIndex());
				stringBuilder.Append(']');
				num++;
				if (hierarchyNode.Depth < 5)
				{
					for (int i = 0; i < hierarchyNode.Transform.childCount; i++)
					{
						queue.Enqueue(new HierarchyNode(hierarchyNode.Transform.GetChild(i), hierarchyNode.Depth + 1));
					}
				}
			}
			if (queue.Count > 0)
			{
				stringBuilder.Append(" | <truncated>");
			}
			return stringBuilder.ToString();
		}

		private static string FormatVector3(Vector3 value)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			return string.Format(CultureInfo.InvariantCulture, "({0:F4},{1:F4},{2:F4})", value.x, value.y, value.z);
		}
	}
}
namespace HowToFish.MoreHead.Persistence
{
	internal sealed class LocalLoadoutStore
	{
		private sealed class LocalLoadoutData
		{
			public int schemaVersion = 2;

			public string headCosmeticId = string.Empty;

			public string hatCosmeticId = string.Empty;

			public string accessoryCosmeticId = string.Empty;

			public string outfitCosmeticId = string.Empty;
		}

		private readonly ManualLogSource _logger;

		private readonly string _path;

		private CosmeticLoadout _loadout = new CosmeticLoadout();

		internal LocalLoadoutStore(ManualLogSource logger, string path)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_path = path ?? throw new ArgumentNullException("path");
		}

		internal CosmeticLoadout GetLoadout()
		{
			return _loadout.Clone();
		}

		internal string Get(CosmeticSlot slot)
		{
			return _loadout.Get(slot);
		}

		internal void Load()
		{
			_loadout = new CosmeticLoadout();
			if (!File.Exists(_path))
			{
				return;
			}
			try
			{
				LocalLoadoutData localLoadoutData = JsonConvert.DeserializeObject<LocalLoadoutData>(File.ReadAllText(_path));
				if (localLoadoutData == null || (localLoadoutData.schemaVersion != 1 && localLoadoutData.schemaVersion != 2))
				{
					_logger.LogWarning((object)("[HTFMoreHead][LOADOUT] Ignored unsupported loadout: '" + _path + "'."));
					return;
				}
				_loadout.HatId = Normalize((localLoadoutData.schemaVersion == 1) ? localLoadoutData.headCosmeticId : localLoadoutData.hatCosmeticId);
				_loadout.AccessoryId = Normalize(localLoadoutData.accessoryCosmeticId);
				_loadout.OutfitId = Normalize(localLoadoutData.outfitCosmeticId);
				_logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Loaded Hat='" + DisplayId(_loadout.HatId) + "', Accessory='" + DisplayId(_loadout.AccessoryId) + "', Outfit='" + DisplayId(_loadout.OutfitId) + "'."));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[HTFMoreHead][LOADOUT] Could not load '" + _path + "': " + ex.Message));
			}
		}

		internal void Save(CosmeticLoadout loadout)
		{
			_loadout = new CosmeticLoadout
			{
				HatId = Normalize(loadout?.HatId),
				AccessoryId = Normalize(loadout?.AccessoryId),
				OutfitId = Normalize(loadout?.OutfitId)
			};
			try
			{
				string directoryName = Path.GetDirectoryName(_path);
				if (!string.IsNullOrEmpty(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				LocalLoadoutData localLoadoutData = new LocalLoadoutData
				{
					schemaVersion = 2,
					hatCosmeticId = _loadout.HatId,
					accessoryCosmeticId = _loadout.AccessoryId,
					outfitCosmeticId = _loadout.OutfitId
				};
				File.WriteAllText(_path, JsonConvert.SerializeObject((object)localLoadoutData, (Formatting)1));
				_logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Saved Hat='" + DisplayId(_loadout.HatId) + "', Accessory='" + DisplayId(_loadout.AccessoryId) + "', Outfit='" + DisplayId(_loadout.OutfitId) + "'."));
			}
			catch (Exception ex)
			{
				_logger.LogError((object)("[HTFMoreHead][LOADOUT] Could not save '" + _path + "': " + ex.Message));
			}
		}

		private static string Normalize(string value)
		{
			value = value?.Trim();
			if (string.IsNullOrEmpty(value) || value.Length > 193)
			{
				return string.Empty;
			}
			return value;
		}

		private static string DisplayId(string value)
		{
			if (!string.IsNullOrEmpty(value))
			{
				return value;
			}
			return "<native>";
		}
	}
}
namespace HowToFish.MoreHead.Patches
{
	[HarmonyPatch(typeof(LocalSkin), "Awake")]
	internal static class LocalSkinAwakePatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchLocalSkinReady();
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinAwakePatch", exception);
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnStartClient")]
	internal static class PlayerStartClientPatch
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchPlayerStarted(__instance);
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("PlayerStartClientPatch", exception);
			}
		}
	}
	[HarmonyPatch(typeof(PlayerSkin), "OnStartClient")]
	internal static class PlayerSkinStartClientPatch
	{
		[HarmonyPostfix]
		private static void Postfix(PlayerSkin __instance)
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchSkinStarted(__instance);
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("PlayerSkinStartClientPatch", exception);
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnStopClient")]
	internal static class PlayerStopClientPatch
	{
		[HarmonyPrefix]
		private static void Prefix(Player __instance)
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchPlayerStopping(__instance);
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("PlayerStopClientPatch", exception);
			}
		}
	}
	[HarmonyPatch(typeof(LocalSkin), "EnablePreview")]
	internal static class LocalSkinEnablePreviewPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchLocalSkinPreview(visible: true);
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinEnablePreviewPatch", exception);
			}
		}
	}
	[HarmonyPatch(typeof(LocalSkin), "DisablePreview")]
	internal static class LocalSkinDisablePreviewPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			try
			{
				MoreHeadPlugin.Instance?.DispatchLocalSkinPreview(visible: false);
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinDisablePreviewPatch", exception);
			}
		}
	}
	internal struct CosmeticArrowState
	{
		internal byte BeforeIndex;

		internal bool HandledByMoreHead;
	}
	internal static class CosmeticArrowPatchLogic
	{
		internal static bool Prefix(CosmeticSlot slot, int amount, ref CosmeticArrowState state)
		{
			try
			{
				state.BeforeIndex = GetNativeIndex(slot);
				state.HandledByMoreHead = MoreHeadPlugin.Instance?.HandleNativeCosmeticArrow(slot, amount) ?? false;
				return !state.HandledByMoreHead;
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("CosmeticArrowPrefix." + slot, exception);
				return true;
			}
		}

		internal static void Postfix(CosmeticSlot slot, int amount, CosmeticArrowState state)
		{
			try
			{
				if (!state.HandledByMoreHead)
				{
					MoreHeadPlugin.Instance?.NotifyNativeCosmeticArrowCompleted(slot, amount, state.BeforeIndex, GetNativeIndex(slot));
				}
			}
			catch (Exception exception)
			{
				MoreHeadPlugin.Instance?.ReportPatchException("CosmeticArrowPostfix." + slot, exception);
			}
		}

		private static byte GetNativeIndex(CosmeticSlot slot)
		{
			return slot switch
			{
				CosmeticSlot.Accessory => LocalSkin.LocalAccessoryIndex, 
				CosmeticSlot.Outfit => LocalSkin.LocalOutfitIndex, 
				_ => LocalSkin.LocalHatIndex, 
			};
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "ChangeHatIndex")]
	internal static class NativeHatArrowPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(int amount, ref CosmeticArrowState __state)
		{
			return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Hat, amount, ref __state);
		}

		[HarmonyPostfix]
		private static void Postfix(int amount, CosmeticArrowState __state)
		{
			CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Hat, amount, __state);
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "ChangeAccessoryIndex")]
	internal static class NativeAccessoryArrowPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(int amount, ref CosmeticArrowState __state)
		{
			return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Accessory, amount, ref __state);
		}

		[HarmonyPostfix]
		private static void Postfix(int amount, CosmeticArrowState __state)
		{
			CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Accessory, amount, __state);
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "ChangeOutfitIndex")]
	internal static class NativeOutfitArrowPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(int amount, ref CosmeticArrowState __state)
		{
			return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Outfit, amount, ref __state);
		}

		[HarmonyPostfix]
		private static void Postfix(int amount, CosmeticArrowState __state)
		{
			CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Outfit, amount, __state);
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "SetHatColoringPart")]
	internal static class HatTabPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Hat);
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "SetAccessoryColoringPart")]
	internal static class AccessoryTabPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Accessory);
		}
	}
	[HarmonyPatch(typeof(ButtonManager), "SetOutfitColoringPart")]
	internal static class OutfitTabPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Outfit);
		}
	}
}
namespace HowToFish.MoreHead.Networking
{
	internal enum CosmeticCommand : byte
	{
		ClientHello = 1,
		ServerHello,
		EquipRequest,
		EquipDelta,
		SnapshotRequest,
		SnapshotBegin,
		SnapshotEntry,
		SnapshotEnd,
		ClearPlayer
	}
	internal struct CosmeticMessage : IBroadcast
	{
		public byte ProtocolVersion;

		public CosmeticCommand Command;

		public byte Accepted;

		public int SubjectClientId;

		public int Revision;

		public int EntryCount;

		public string HatCosmeticId;

		public string AccessoryCosmeticId;

		public string OutfitCosmeticId;

		public string CatalogDigest;
	}
	internal static class CosmeticMessageSerializers
	{
		internal static void Register()
		{
			GenericWriter<CosmeticMessage>.SetWrite((Action<Writer, CosmeticMessage>)Write);
			GenericReader<CosmeticMessage>.SetRead((Func<Reader, CosmeticMessage>)Read);
		}

		private static void Write(Writer writer, CosmeticMessage value)
		{
			writer.WriteUInt8Unpacked(value.ProtocolVersion);
			writer.WriteUInt8Unpacked((byte)value.Command);
			writer.WriteUInt8Unpacked(value.Accepted);
			writer.WriteInt32(value.SubjectClientId);
			writer.WriteInt32(value.Revision);
			writer.WriteInt32(value.EntryCount);
			writer.WriteString(value.HatCosmeticId ?? string.Empty);
			writer.WriteString(value.AccessoryCosmeticId ?? string.Empty);
			writer.WriteString(value.OutfitCosmeticId ?? string.Empty);
			writer.WriteString(value.CatalogDigest ?? string.Empty);
		}

		private static CosmeticMessage Read(Reader reader)
		{
			return new CosmeticMessage
			{
				ProtocolVersion = reader.ReadUInt8Unpacked(),
				Command = (CosmeticCommand)reader.ReadUInt8Unpacked(),
				Accepted = reader.ReadUInt8Unpacked(),
				SubjectClientId = reader.ReadInt32(),
				Revision = reader.ReadInt32(),
				EntryCount = reader.ReadInt32(),
				HatCosmeticId = reader.ReadStringAllocated(),
				AccessoryCosmeticId = reader.ReadStringAllocated(),
				OutfitCosmeticId = reader.ReadStringAllocated(),
				CatalogDigest = reader.ReadStringAllocated()
			};
		}
	}
	internal sealed class CosmeticNetworkService
	{
		private const int MaxSnapshotEntries = 32;

		private const float HookCheckInterval = 0.5f;

		private const float HelloRetryInterval = 2f;

		private const float EquipRequestInterval = 0.15f;

		private static readonly Regex NetworkCosmeticId = new Regex("^[a-z0-9][a-z0-9._-]{0,95}:[a-z0-9][a-z0-9._-]{0,95}$", RegexOptions.CultureInvariant);

		private readonly ManualLogSource _logger;

		private readonly BoundedLog _boundedLog;

		private readonly CosmeticCatalog _catalog;

		private readonly LocalLoadoutStore _loadoutStore;

		private readonly PlayerAppearanceService _appearance;

		private readonly Dictionary<int, CosmeticLoadout> _hostState = new Dictionary<int, CosmeticLoadout>();

		private readonly Dictionary<int, float> _nextEquipAt = new Dictionary<int, float>();

		private readonly HashSet<int> _acceptedClients = new HashSet<int>();

		private readonly Dictionary<int, CosmeticLoadout> _clientState = new Dictionary<int, CosmeticLoadout>();

		private readonly Dictionary<int, CosmeticLoadout> _pendingSnapshot = new Dictionary<int, CosmeticLoadout>();

		private NetworkManager _networkManager;

		private float _nextHookCheckAt;

		private float _nextHelloRetryAt;

		private int _revision;

		private int _pendingSnapshotRevision = -1;

		private int _pendingSnapshotExpected;

		private bool _protocolAccepted;

		internal CosmeticNetworkService(ManualLogSource logger, BoundedLog boundedLog, CosmeticCatalog catalog, LocalLoadoutStore loadoutStore, PlayerAppearanceService appearance)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog");
			_catalog = catalog ?? throw new ArgumentNullException("catalog");
			_loadoutStore = loadoutStore ?? throw new ArgumentNullException("loadoutStore");
			_appearance = appearance ?? throw new ArgumentNullException("appearance");
			CosmeticMessageSerializers.Register();
		}

		internal void Tick()
		{
			if (!(Time.unscaledTime < _nextHookCheckAt))
			{
				_nextHookCheckAt = Time.unscaledTime + 0.5f;
				EnsureHooks();
				if ((Object)(object)_networkManager != (Object)null && _networkManager.IsClientStarted && !_protocolAccepted && Time.unscaledTime >= _nextHelloRetryAt)
				{
					SendHello("HandshakeRetry");
				}
			}
		}

		internal void NotifyPlayerReady(Player player)
		{
			if (!((Object)(object)player == (Object)null) && !(((NetworkBehaviour)player).Owner == (NetworkConnection)null) && ((NetworkBehaviour)player).Owner.IsLocalClient)
			{
				EnsureHooks();
				if ((Object)(object)_networkManager != (Object)null && _networkManager.IsClientStarted)
				{
					SendHello("LocalPlayerReady");
				}
			}
		}

		internal void RequestLocalEquip(CosmeticLoadout loadout)
		{
			if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsClientStarted)
			{
				if (!_protocolAccepted)
				{
					SendHello("EquipBeforeAccepted");
				}
				loadout = loadout ?? new CosmeticLoadout();
				if (!IsValidLoadout(loadout))
				{
					_logger.LogWarning((object)"[HTFMoreHead][NETWORK] Refused invalid local cosmetic loadout.");
					return;
				}
				_networkManager.ClientManager.Broadcast<CosmeticMessage>(new CosmeticMessage
				{
					ProtocolVersion = 3,
					Command = CosmeticCommand.EquipRequest,
					HatCosmeticId = loadout.HatId,
					AccessoryCosmeticId = loadout.AccessoryId,
					OutfitCosmeticId = loadout.OutfitId
				}, (Channel)0);
			}
		}

		internal void ClearAndDetach()
		{
			DetachHooks();
			ClearSessionState();
		}

		private void EnsureHooks()
		{
			NetworkManager val = (((Object)(object)Server.Instance != (Object)null) ? ((NetworkBehaviour)Server.Instance).NetworkManager : null);
			if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)_networkManager))
			{
				bool num = (Object)(object)_networkManager != (Object)null;
				DetachHooks();
				if (num)
				{
					ClearSessionState();
				}
				_networkManager = val;
				_networkManager.ClientManager.RegisterBroadcast<CosmeticMessage>((Action<CosmeticMessage, Channel>)OnClientMessage);
				_networkManager.ServerManager.RegisterBroadcast<CosmeticMessage>((Action<NetworkConnection, CosmeticMessage, Channel>)OnServerMessage, true);
				_networkManager.ClientManager.OnClientConnectionState += OnClientConnectionState;
				_networkManager.ServerManager.OnRemoteConnectionState += OnRemoteConnectionState;
				_logger.LogInfo((object)"[HTFMoreHead][Stage3][NETWORK] FishNet Broadcast hooks registered.");
				if (_networkManager.IsClientStarted)
				{
					SendHello("HooksRegistered");
				}
			}
		}

		private void DetachHooks()
		{
			if (!((Object)(object)_networkManager == (Object)null))
			{
				try
				{
					_networkManager.ClientManager.UnregisterBroadcast<CosmeticMessage>((Action<CosmeticMessage, Channel>)OnClientMessage);
					_networkManager.ServerManager.UnregisterBroadcast<CosmeticMessage>((Action<NetworkConnection, CosmeticMessage, Channel>)OnServerMessage);
					_networkManager.ClientManager.OnClientConnectionState -= OnClientConnectionState;
					_networkManager.ServerManager.OnRemoteConnectionState -= OnRemoteConnectionState;
				}
				catch (Exception ex)
				{
					_logger.LogWarning((object)("[HTFMoreHead][Stage3][NETWORK] Hook cleanup failed safely: " + ex.Message));
				}
				_networkManager = null;
			}
		}

		private void SendHello(string reason)
		{
			if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsClientStarted)
			{
				_protocolAccepted = false;
				_nextHelloRetryAt = Time.unscaledTime + 2f;
				_networkManager.ClientManager.Broadcast<CosmeticMessage>(new CosmeticMessage
				{
					ProtocolVersion = 3,
					Command = CosmeticCommand.ClientHello,
					HatCosmeticId = _loadoutStore.Get(CosmeticSlot.Hat),
					AccessoryCosmeticId = _loadoutStore.Get(CosmeticSlot.Accessory),
					OutfitCosmeticId = _loadoutStore.Get(CosmeticSlot.Outfit),
					CatalogDigest = _catalog.CatalogDigest
				}, (Channel)0);
				_logger.LogInfo((object)($"[HTFMoreHead][HELLO] Sent Protocol={(byte)3}, " + "Catalog=" + _catalog.CatalogDigest + ", Loadout=" + FormatLoadout(_loadoutStore.GetLoadout()) + ", Reason=" + reason + "."));
			}
		}

		private void OnServerMessage(NetworkConnection connection, CosmeticMessage message, Channel channel)
		{
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				OnServerMessageCore(connection, message, channel);
			}
			catch (Exception arg)
			{
				_boundedLog.ErrorOnce("server-message-exception-" + message.Command, $"[HTFMoreHead][Stage3][ERROR] Server message {message.Command} failed safely: {arg}");
			}
		}

		private void OnServerMessageCore(NetworkConnection connection, CosmeticMessage message, Channel channel)
		{
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsServerStarted || connection == (NetworkConnection)null || !connection.IsActive || !connection.IsAuthenticated)
			{
				return;
			}
			if (message.ProtocolVersion != 3)
			{
				SendTo(connection, new CosmeticMessage
				{
					ProtocolVersion = 3,
					Command = CosmeticCommand.ServerHello,
					Accepted = 0
				});
				return;
			}
			switch (message.Command)
			{
			case CosmeticCommand.ClientHello:
				HandleClientHello(connection, message);
				break;
			case CosmeticCommand.EquipRequest:
				if (_acceptedClients.Contains(connection.ClientId))
				{
					HandleEquipRequest(connection, ReadLoadout(message));
				}
				break;
			case CosmeticCommand.SnapshotRequest:
				if (_acceptedClients.Contains(connection.ClientId))
				{
					SendSnapshot(connection);
				}
				break;
			case CosmeticCommand.ServerHello:
			case CosmeticCommand.EquipDelta:
				break;
			}
		}

		private void HandleClientHello(NetworkConnection connection, CosmeticMessage message)
		{
			int clientId = connection.ClientId;
			CosmeticLoadout cosmeticLoadout = ReadLoadout(message);
			if (cosmeticLoadout == null)
			{
				cosmeticLoadout = new CosmeticLoadout();
			}
			CosmeticLoadout value;
			bool num = !_hostState.TryGetValue(clientId, out value) || !LoadoutEquals(value, cosmeticLoadout);
			_hostState[clientId] = cosmeticLoadout.Clone();
			_acceptedClients.Add(clientId);
			if (num)
			{
				_revision++;
			}
			SendTo(connection, new CosmeticMessage
			{
				ProtocolVersion = 3,
				Command = CosmeticCommand.ServerHello,
				Accepted = 1,
				Revision = _revision,
				CatalogDigest = _catalog.CatalogDigest
			});
			if (num)
			{
				BroadcastDelta(clientId, cosmeticLoadout);
			}
			SendSnapshot(connection);
			if (!string.Equals(message.CatalogDigest, _catalog.CatalogDigest, StringComparison.Ordinal))
			{
				_boundedLog.WarningOnce("catalog-mismatch-server-" + clientId, $"[HTFMoreHead][Stage3][CATALOG] ClientId={clientId} catalog differs from Host; " + "missing content will be skipped locally.");
			}
			_logger.LogInfo((object)($"[HTFMoreHead][HELLO] Accepted ClientId={clientId}, " + $"Loadout={FormatLoadout(cosmeticLoadout)}, Revision={_revision}."));
		}

		private void HandleEquipRequest(NetworkConnection connection, CosmeticLoadout requestedLoadout)
		{
			int clientId = connection.ClientId;
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!_nextEquipAt.TryGetValue(clientId, out var value) || !(realtimeSinceStartup < value))
			{
				_nextEquipAt[clientId] = realtimeSinceStartup + 0.15f;
				if (!IsValidLoadout(requestedLoadout))
				{
					_boundedLog.WarningOnce("invalid-equip-" + clientId, $"[HTFMoreHead][NETWORK] ClientId={clientId} sent an invalid cosmetic loadout.");
					return;
				}
				_hostState[clientId] = requestedLoadout.Clone();
				_revision++;
				BroadcastDelta(clientId, requestedLoadout);
			}
		}

		private void BroadcastDelta(int subjectClientId, CosmeticLoadout loadout)
		{
			if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsServerStarted)
			{
				_networkManager.ServerManager.Broadcast<CosmeticMessage>(new CosmeticMessage
				{
					ProtocolVersion = 3,
					Command = CosmeticCommand.EquipDelta,
					SubjectClientId = subjectClientId,
					Revision = _revision,
					HatCosmeticId = (loadout?.HatId ?? string.Empty),
					AccessoryCosmeticId = (loadout?.AccessoryId ?? string.Empty),
					OutfitCosmeticId = (loadout?.OutfitId ?? string.Empty)
				}, true, (Channel)0);
			}
		}

		private void SendSnapshot(NetworkConnection connection)
		{
			if (connection == (NetworkConnection)null || !connection.IsActive || _hostState.Count > 32)
			{
				return;
			}
			SendTo(connection, new CosmeticMessage
			{
				ProtocolVersion = 3,
				Command = CosmeticCommand.SnapshotBegin,
				Revision = _revision,
				EntryCount = _hostState.Count
			});
			_logger.LogInfo((object)($"[HTFMoreHead][Stage3][SNAPSHOT_SEND] ClientId={connection.ClientId}, " + $"Revision={_revision}, Entries={_hostState.Count}."));
			foreach (KeyValuePair<int, CosmeticLoadout> item in _hostState)
			{
				SendTo(connection, new CosmeticMessage
				{
					ProtocolVersion = 3,
					Command = CosmeticCommand.SnapshotEntry,
					SubjectClientId = item.Key,
					Revision = _revision,
					HatCosmeticId = (item.Value?.HatId ?? string.Empty),
					AccessoryCosmeticId = (item.Value?.AccessoryId ?? string.Empty),
					OutfitCosmeticId = (item.Value?.OutfitId ?? string.Empty)
				});
			}
			SendTo(connection, new CosmeticMessage
			{
				ProtocolVersion = 3,
				Command = CosmeticCommand.SnapshotEnd,
				Revision = _revision,
				EntryCount = _hostState.Count
			});
		}

		private void SendTo(NetworkConnection connection, CosmeticMessage message)
		{
			if (!((Object)(object)_networkManager == (Object)null) && !(connection == (NetworkConnection)null) && connection.IsActive && connection.IsAuthenticated)
			{
				_networkManager.ServerManager.Broadcast<CosmeticMessage>(connection, message, true, (Channel)0);
			}
		}

		private void OnClientMessage(CosmeticMessage message, Channel channel)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				OnClientMessageCore(message, channel);
			}
			catch (Exception arg)
			{
				_boundedLog.ErrorOnce("client-message-exception-" + message.Command, $"[HTFMoreHead][Stage3][ERROR] Client message {message.Command} failed safely: {arg}");
			}
		}

		private void OnClientMessageCore(CosmeticMessage message, Channel channel)
		{
			if (message.ProtocolVersion != 3)
			{
				_protocolAccepted = false;
				_boundedLog.ErrorOnce("protocol-mismatch-client", $"[HTFMoreHead][Stage3][PROTOCOL] Host={message.ProtocolVersion}, " + $"Local={(byte)3}; custom synchronization disabled.");
				return;
			}
			switch (message.Command)
			{
			case CosmeticCommand.ServerHello:
				_protocolAccepted = message.Accepted != 0;
				if (!_protocolAccepted)
				{
					ClearClientState();
					break;
				}
				_logger.LogInfo((object)$"[HTFMoreHead][Stage3][HELLO_ACK] Accepted=True, Revision={message.Revision}.");
				if (!string.Equals(message.CatalogDigest, _catalog.CatalogDigest, StringComparison.Ordinal))
				{
					_boundedLog.WarningOnce("catalog-mismatch-client", "[HTFMoreHead][Stage3][CATALOG] Host catalog differs; missing content will be skipped locally.");
				}
				break;
			case CosmeticCommand.EquipDelta:
			{
				CosmeticLoadout cosmeticLoadout = ReadLoadout(message);
				if (_protocolAccepted && message.SubjectClientId >= 0 && cosmeticLoadout != null)
				{
					_clientState[message.SubjectClientId] = cosmeticLoadout.Clone();
					_appearance.SetLoadout(message.SubjectClientId, cosmeticLoadout);
					_logger.LogInfo((object)($"[HTFMoreHead][DELTA_APPLY] ClientId={message.SubjectClientId}, " + $"Loadout={FormatLoadout(cosmeticLoadout)}, Revision={message.Revision}."));
				}
				break;
			}
			case CosmeticCommand.SnapshotBegin:
				BeginSnapshot(message);
				break;
			case CosmeticCommand.SnapshotEntry:
				AddSnapshotEntry(message);
				break;
			case CosmeticCommand.SnapshotEnd:
				EndSnapshot(message);
				break;
			case CosmeticCommand.ClearPlayer:
				_clientState.Remove(message.SubjectClientId);
				_appearance.RemovePlayer(message.SubjectClientId, clearLoadout: true);
				break;
			case CosmeticCommand.EquipRequest:
			case CosmeticCommand.SnapshotRequest:
				break;
			}
		}

		private void BeginSnapshot(CosmeticMessage message)
		{
			if (!_protocolAccepted || message.EntryCount < 0 || message.EntryCount > 32)
			{
				_pendingSnapshotRevision = -1;
				_pendingSnapshot.Clear();
			}
			else
			{
				_pendingSnapshotRevision = message.Revision;
				_pendingSnapshotExpected = message.EntryCount;
				_pendingSnapshot.Clear();
			}
		}

		private void AddSnapshotEntry(CosmeticMessage message)
		{
			CosmeticLoadout cosmeticLoadout = ReadLoadout(message);
			if (_pendingSnapshotRevision == message.Revision && message.SubjectClientId >= 0 && cosmeticLoadout != null && _pendingSnapshot.Count < 32)
			{
				_pendingSnapshot[message.SubjectClientId] = cosmeticLoadout;
			}
		}

		private void EndSnapshot(CosmeticMessage message)
		{
			if (_pendingSnapshotRevision != message.Revision || message.EntryCount != _pendingSnapshotExpected || _pendingSnapshot.Count != _pendingSnapshotExpected)
			{
				_boundedLog.WarningOnce("snapshot-incomplete-" + message.Revision, $"[HTFMoreHead][Stage3][SNAPSHOT] Incomplete Revision={message.Revision}, " + $"Expected={_pendingSnapshotExpected}, Received={_pendingSnapshot.Count}.");
				_pendingSnapshotRevision = -1;
				_pendingSnapshot.Clear();
				return;
			}
			_clientState.Clear();
			foreach (KeyValuePair<int, CosmeticLoadout> item in _pendingSnapshot)
			{
				_clientState[item.Key] = item.Value.Clone();
			}
			_appearance.ReplaceSnapshot(_clientState);
			_logger.LogInfo((object)($"[HTFMoreHead][Stage3][SNAPSHOT] Applied Revision={message.Revision}, " + $"Entries={_clientState.Count}."));
			_pendingSnapshotRevision = -1;
			_pendingSnapshot.Clear();
		}

		private void OnClientConnectionState(ClientConnectionStateArgs args)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				OnClientConnectionStateCore(args);
			}
			catch (Exception arg)
			{
				_boundedLog.ErrorOnce("client-connection-exception", $"[HTFMoreHead][Stage3][ERROR] Client connection event failed safely: {arg}");
			}
		}

		private void OnClientConnectionStateCore(ClientConnectionStateArgs args)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Invalid comparison between Unknown and I4
			//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)
			//IL_001c: Invalid comparison between Unknown and I4
			if ((int)args.ConnectionState == 8)
			{
				SendHello("ConnectionStarted");
			}
			else if ((int)args.ConnectionState == 1)
			{
				_logger.LogInfo((object)"[HTFMoreHead][Stage3][NETWORK] Local connection stopped; session state cleared.");
				ClearSessionState();
			}
		}

		private void OnRemoteConnectionState(NetworkConnection connection, RemoteConnectionStateArgs args)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				OnRemoteConnectionStateCore(connection, args);
			}
			catch (Exception arg)
			{
				_boundedLog.ErrorOnce("remote-connection-exception", $"[HTFMoreHead][Stage3][ERROR] Remote connection event failed safely: {arg}");
			}
		}

		private void OnRemoteConnectionStateCore(NetworkConnection connection, RemoteConnectionStateArgs args)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsServerStarted && (int)args.ConnectionState == 0)
			{
				int connectionId = args.ConnectionId;
				if (_hostState.Remove(connectionId))
				{
					_nextEquipAt.Remove(connectionId);
					_acceptedClients.Remove(connectionId);
					_revision++;
					_networkManager.ServerManager.Broadcast<CosmeticMessage>(new CosmeticMessage
					{
						ProtocolVersion = 3,
						Command = CosmeticCommand.ClearPlayer,
						SubjectClientId = connectionId,
						Revision = _revision
					}, true, (Channel)0);
				}
			}
		}

		private void ClearSessionState()
		{
			_protocolAccepted = false;
			_hostState.Clear();
			_nextEquipAt.Clear();
			_acceptedClients.Clear();
			ClearClientState();
			_revision = 0;
		}

		private void ClearClientState()
		{
			_clientState.Clear();
			_pendingSnapshot.Clear();
			_pendingSnapshotRevision = -1;
			_pendingSnapshotExpected = 0;
			_appearance.ClearAll();
		}

		private static bool IsValidCosmeticId(string cosmeticId)
		{
			if (!string.IsNullOrEmpty(cosmeticId))
			{
				if (cosmeticId.Length <= 193)
				{
					return NetworkCosmeticId.IsMatch(cosmeticId);
				}
				return false;
			}
			return true;
		}

		private static bool IsValidLoadout(CosmeticLoadout loadout)
		{
			if (loadout != null && IsValidCosmeticId(loadout.HatId) && IsValidCosmeticId(loadout.AccessoryId))
			{
				return IsValidCosmeticId(loadout.OutfitId);
			}
			return false;
		}

		private static CosmeticLoadout ReadLoadout(CosmeticMessage message)
		{
			CosmeticLoadout cosmeticLoadout = new CosmeticLoadout
			{
				HatId = (message.HatCosmeticId ?? string.Empty),
				AccessoryId = (message.AccessoryCosmeticId ?? string.Empty),
				OutfitId = (message.OutfitCosmeticId ?? string.Empty)
			};
			if (!IsValidLoadout(cosmeticLoadout))
			{
				return null;
			}
			return cosmeticLoadout;
		}

		private static bool LoadoutEquals(CosmeticLoadout left, CosmeticLoadout right)
		{
			if (left != null && right != null && string.Equals(left.HatId, right.HatId, StringComparison.Ordinal) && string.Equals(left.AccessoryId, right.AccessoryId, StringComparison.Ordinal))
			{
				return string.Equals(left.OutfitId, right.OutfitId, StringComparison.Ordinal);
			}
			return false;
		}

		private static string FormatLoadout(CosmeticLoadout loadout)
		{
			loadout = loadout ?? new CosmeticLoadout();
			return "[Hat=" + DisplayId(loadout.HatId) + ", Accessory=" + DisplayId(loadout.AccessoryId) + ", Outfit=" + DisplayId(loadout.OutfitId) + "]";
		}

		private static string DisplayId(string value)
		{
			if (!string.IsNullOrEmpty(value))
			{
				return value;
			}
			return "<native>";
		}
	}
}
namespace HowToFish.MoreHead.Diagnostics
{
	internal sealed class BoundedLog
	{
		private readonly ManualLogSource _logger;

		private readonly int _maximumKeys;

		private readonly object _gate = new object();

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

		private bool _capacityWarningWritten;

		internal BoundedLog(ManualLogSource logger, int maximumKeys)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			if (maximumKeys <= 0)
			{
				throw new ArgumentOutOfRangeException("maximumKeys");
			}
			_maximumKeys = maximumKeys;
		}

		internal void WarningOnce(string key, string message)
		{
			if (TryAddKey(key))
			{
				_logger.LogWarning((object)message);
			}
		}

		internal void ErrorOnce(string key, string message)
		{
			if (TryAddKey(key))
			{
				_logger.LogError((object)message);
			}
		}

		private bool TryAddKey(string key)
		{
			lock (_gate)
			{
				if (_onceKeys.Contains(key))
				{
					return false;
				}
				if (_onceKeys.Count >= _maximumKeys)
				{
					if (!_capacityWarningWritten)
					{
						_capacityWarningWritten = true;
						_logger.LogWarning((object)"[HTFMoreHead][Stage0][LOG] One-shot diagnostic key capacity reached; additional repeated diagnostics will be suppressed.");
					}
					return false;
				}
				return _onceKeys.Add(key);
			}
		}
	}
}
namespace HowToFish.MoreHead.Content
{
	internal sealed class CosmeticCatalog
	{
		private const string ManifestFileName = "manifest.json";

		private const string EmbeddedManifestFileName = "htfmorehead.json";

		private readonly ManualLogSource _logger;

		private readonly BoundedLog _boundedLog;

		private readonly Dictionary<string, CosmeticDefinition> _byId = new Dictionary<string, CosmeticDefinition>(StringComparer.Ordinal);

		private readonly List<CosmeticDefinition> _heads = new List<CosmeticDefinition>();

		private readonly List<CosmeticDefinition> _accessories = new List<CosmeticDefinition>();

		private readonly List<CosmeticDefinition> _outfits = new List<CosmeticDefinition>();

		private readonly List<AssetBundle> _loadedBundles = new List<AssetBundle>();

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

		internal int HeadCount => _heads.Count;

		internal int AccessoryCount => _accessories.Count;

		internal int OutfitCount => _outfits.Count;

		internal string CatalogDigest { get; private set; } = "empty";

		internal CosmeticDefinition FirstHead
		{
			get
			{
				if (_heads.Count <= 0)
				{
					return null;
				}
				return _heads[0];
			}
		}

		internal CosmeticCatalog(ManualLogSource logger, BoundedLog boundedLog)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			_boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog");
		}

		internal CosmeticDefinition GetHeadAt(int index)
		{
			if (index < 0 || index >= _heads.Count)
			{
				return null;
			}
			return _heads[index];
		}

		internal int GetHeadIndex(string id)
		{
			return GetIndex(CosmeticSlot.Hat, id);
		}

		internal int GetCount(CosmeticSlot slot)
		{
			return GetList(slot).Count;
		}

		internal CosmeticDefinition GetAt(CosmeticSlot slot, int index)
		{
			List<CosmeticDefinition> list = GetList(slot);
			if (index < 0 || index >= list.Count)
			{
				return null;
			}
			return list[index];
		}

		internal int GetIndex(CosmeticSlot slot, string id)
		{
			if (string.IsNullOrEmpty(id))
			{
				return -1;
			}
			List<CosmeticDefinition> list = GetList(slot);
			for (int i = 0; i < list.Count; i++)
			{
				if (string.Equals(list[i].Id, id, StringComparison.Ordinal))
				{
					return i;
				}
			}
			return -1;
		}

		private List<CosmeticDefinition> GetList(CosmeticSlot slot)
		{
			return slot switch
			{
				CosmeticSlot.Accessory => _accessories, 
				CosmeticSlot.Outfit => _outfits, 
				_ => _heads, 
			};
		}

		internal void LoadAll(string pluginRoot)
		{
			UnloadAll();
			if (string.IsNullOrWhiteSpace(pluginRoot) || !Directory.Exists(pluginRoot))
			{
				_logger.LogInfo((object)("[HTFMoreHead][Stage2][CONTENT] Plugin directory not found: \"" + pluginRoot + "\"."));
				return;
			}
			string[] files = Directory.GetFiles(pluginRoot, "*.htfhhh", SearchOption.AllDirectories);
			Array.Sort(files, (IComparer<string>?)StringComparer.OrdinalIgnoreCase);
			for (int i = 0; i < files.Length; i++)
			{
				LoadEmbeddedPack(files[i]);
			}
			string text = Path.Combine(pluginRoot, "HTFMoreHead", "Content");
			string[] array = (Directory.Exists(text) ? Directory.GetFiles(text, "manifest.json", SearchOption.AllDirectories) : Array.Empty<string>())