Decompiled source of CatosChestViewer v0.1.3

CatosChestViewer.dll

Decompiled 5 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("Catosaur")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Shows the contents of the chest under the Valheim player's crosshair.")]
[assembly: AssemblyFileVersion("0.1.3.0")]
[assembly: AssemblyInformationalVersion("0.1.3+e69f2f9f900fe5a5ff041e0e3be3486d604cdc2f")]
[assembly: AssemblyProduct("CatosChestViewer")]
[assembly: AssemblyTitle("CatosChestViewer")]
[assembly: AssemblyVersion("0.1.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 CatosChestViewer
{
	internal sealed class ChestContentsSnapshot
	{
		internal IReadOnlyList<ChestItemEntry> Items { get; }

		internal int OccupiedSlots { get; }

		internal int TotalSlots { get; }

		internal string Fingerprint { get; }

		internal ChestContentsSnapshot(List<ChestItemEntry> items, int occupiedSlots, int totalSlots, string fingerprint)
		{
			Items = items.AsReadOnly();
			OccupiedSlots = occupiedSlots;
			TotalSlots = totalSlots;
			Fingerprint = fingerprint;
		}
	}
	internal sealed class ChestItemEntry
	{
		internal string Name { get; }

		internal int Stack { get; }

		internal int SourceStackCount { get; }

		internal bool MatchesPlayerInventory { get; }

		internal ChestItemEntry(string name, int stack, int sourceStackCount, bool matchesPlayerInventory)
		{
			Name = name;
			Stack = stack;
			SourceStackCount = sourceStackCount;
			MatchesPlayerInventory = matchesPlayerInventory;
		}
	}
	internal static class ChestInventoryReader
	{
		private sealed class AggregateEntry
		{
			internal int Stack;

			internal int SourceStackCount;
		}

		private static Type _localizationType;

		private static PropertyInfo _localizationInstance;

		private static MethodInfo _localizeMethod;

		private static float _nextReadWarningTime;

		internal static bool TryRead(Container container, Player player, out ChestContentsSnapshot snapshot)
		{
			snapshot = null;
			if (!Object.op_Implicit((Object)(object)container))
			{
				return false;
			}
			try
			{
				Inventory inventory = container.GetInventory();
				if (inventory == null)
				{
					return false;
				}
				List<ItemData> allItemsInGridOrder = inventory.GetAllItemsInGridOrder();
				List<ChestItemEntry> list = new List<ChestItemEntry>();
				HashSet<string> playerItemKeys = GetPlayerItemKeys(player);
				string fingerprint = BuildFingerprint(allItemsInGridOrder, list, playerItemKeys);
				list.Sort(CompareEntriesByName);
				int occupiedSlots = Math.Max(0, inventory.NrOfItems());
				int totalSlots = GetTotalSlots(inventory, occupiedSlots);
				snapshot = new ChestContentsSnapshot(list, occupiedSlots, totalSlots, fingerprint);
				return true;
			}
			catch (Exception ex)
			{
				if (Time.unscaledTime >= _nextReadWarningTime)
				{
					_nextReadWarningTime = Time.unscaledTime + 2f;
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogWarning((object)("Chest inventory read failed: " + ex.Message));
					}
				}
				return false;
			}
		}

		private static string BuildFingerprint(List<ItemData> items, List<ChestItemEntry> entries, HashSet<string> playerItemKeys)
		{
			string text = (ModConfig.StashSenseEnabled.Value ? "stash-sense-on" : "stash-sense-off");
			if (items == null || items.Count == 0)
			{
				return text + "|empty";
			}
			Dictionary<string, AggregateEntry> dictionary = new Dictionary<string, AggregateEntry>(StringComparer.Ordinal);
			foreach (ItemData item in items)
			{
				if (item != null && item.m_shared != null)
				{
					string key = item.m_shared.m_name ?? string.Empty;
					int num = Math.Max(0, item.m_stack);
					if (!dictionary.TryGetValue(key, out var value))
					{
						value = new AggregateEntry();
						dictionary.Add(key, value);
					}
					value.Stack = (int)Math.Min(2147483647L, (long)value.Stack + (long)num);
					value.SourceStackCount++;
				}
			}
			StringBuilder stringBuilder = new StringBuilder();
			List<string> list = new List<string>(dictionary.Keys);
			list.Sort(StringComparer.Ordinal);
			foreach (string item2 in list)
			{
				AggregateEntry aggregateEntry = dictionary[item2];
				bool flag = ModConfig.StashSenseEnabled.Value && playerItemKeys.Contains(item2);
				entries.Add(new ChestItemEntry(Localize(item2), aggregateEntry.Stack, aggregateEntry.SourceStackCount, flag));
				stringBuilder.Append(item2).Append('\u001f').Append(aggregateEntry.Stack)
					.Append('\u001f')
					.Append(aggregateEntry.SourceStackCount)
					.Append('\u001f')
					.Append(flag ? '1' : '0')
					.Append('\u001e');
			}
			if (stringBuilder.Length != 0)
			{
				return text + "|" + stringBuilder;
			}
			return text + "|empty";
		}

		private static HashSet<string> GetPlayerItemKeys(Player player)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			if (!ModConfig.StashSenseEnabled.Value || !Object.op_Implicit((Object)(object)player))
			{
				return hashSet;
			}
			Inventory inventory = ((Humanoid)player).GetInventory();
			if (inventory == null)
			{
				return hashSet;
			}
			List<ItemData> allItemsInGridOrder = inventory.GetAllItemsInGridOrder();
			if (allItemsInGridOrder == null)
			{
				return hashSet;
			}
			foreach (ItemData item in allItemsInGridOrder)
			{
				if (item?.m_shared != null && !string.IsNullOrWhiteSpace(item.m_shared.m_name))
				{
					hashSet.Add(item.m_shared.m_name);
				}
			}
			return hashSet;
		}

		private static int CompareEntriesByName(ChestItemEntry left, ChestItemEntry right)
		{
			int num = StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name);
			if (num == 0)
			{
				return StringComparer.Ordinal.Compare(left.Name, right.Name);
			}
			return num;
		}

		private static int GetTotalSlots(Inventory inventory, int occupiedSlots)
		{
			int num = Math.Max(0, inventory.GetWidth());
			int num2 = Math.Max(0, inventory.GetHeight());
			long val = (long)num * (long)num2;
			return (int)Math.Min(2147483647L, Math.Max(val, occupiedSlots));
		}

		internal static string Localize(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return "Unknown item";
			}
			try
			{
				EnsureLocalizationApi();
				object obj = _localizationInstance?.GetValue(null, null);
				if (obj != null && _localizeMethod != null)
				{
					string text = _localizeMethod.Invoke(obj, new object[1] { value }) as string;
					if (!string.IsNullOrWhiteSpace(text) && text != value)
					{
						return text;
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("Item localization failed: " + ex.Message));
				}
			}
			return HumanizeKey(value);
		}

		private static void EnsureLocalizationApi()
		{
			if (_localizationType != null)
			{
				return;
			}
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			for (int i = 0; i < assemblies.Length; i++)
			{
				Type type = assemblies[i].GetType("Localization", throwOnError: false);
				if (!(type == null))
				{
					_localizationType = type;
					_localizationInstance = type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					_localizeMethod = type.GetMethod("Localize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null);
					break;
				}
			}
		}

		private static string HumanizeKey(string value)
		{
			string text = value.Trim();
			if (text.StartsWith("$", StringComparison.Ordinal))
			{
				text = text.Substring(1);
			}
			if (text.StartsWith("item_", StringComparison.OrdinalIgnoreCase))
			{
				text = text.Substring(5);
			}
			if (text.StartsWith("piece_", StringComparison.OrdinalIgnoreCase))
			{
				text = text.Substring(6);
			}
			text = text.Replace('_', ' ').Trim();
			if (text.Length == 0)
			{
				return "Unknown item";
			}
			return char.ToUpperInvariant(text[0]) + text.Substring(1);
		}
	}
	internal static class ChestOverlay
	{
		private static Container _target;

		private static string _renderedText;

		private static string _fingerprint;

		private static float _nextReadTime;

		internal static void Apply(Hud hud, Player player)
		{
			if (!Object.op_Implicit((Object)(object)hud) || !Object.op_Implicit((Object)(object)player) || !ModConfig.Enabled.Value)
			{
				Clear();
				return;
			}
			if (!ChestTargetController.TryGetAccessibleChest(player, out var container))
			{
				Clear();
				return;
			}
			float unscaledTime = Time.unscaledTime;
			bool num = _target != container;
			if (num)
			{
				_target = container;
				_fingerprint = null;
				_renderedText = null;
				_nextReadTime = 0f;
			}
			if (num || unscaledTime >= _nextReadTime)
			{
				_nextReadTime = unscaledTime + Math.Max(0.025f, (float)ModConfig.UpdateIntervalMs.Value / 1000f);
				if (!ChestInventoryReader.TryRead(container, player, out var snapshot))
				{
					Clear();
					return;
				}
				if (!string.Equals(_fingerprint, snapshot.Fingerprint, StringComparison.Ordinal))
				{
					_fingerprint = snapshot.Fingerprint;
					try
					{
						_renderedText = ChestTextFormatter.Format(container, snapshot);
					}
					catch (Exception ex)
					{
						ManualLogSource log = Plugin.Log;
						if (log != null)
						{
							log.LogWarning((object)("Chest text formatting failed; hiding contents: " + ex.Message));
						}
						Clear();
						return;
					}
				}
			}
			if ((Object)(object)hud.m_hoverName != (Object)null && !string.IsNullOrEmpty(_renderedText))
			{
				((TMP_Text)hud.m_hoverName).text = _renderedText;
			}
		}

		internal static void Clear()
		{
			_target = null;
			_fingerprint = null;
			_renderedText = null;
			_nextReadTime = 0f;
		}
	}
	internal static class ChestTargetController
	{
		private static readonly MethodInfo CheckAccessMethod = AccessTools.Method(typeof(Container), "CheckAccess", new Type[1] { typeof(long) }, (Type[])null);

		private static bool _accessMethodWarningLogged;

		private static float _nextAccessWarningTime;

		internal static bool TryGetAccessibleChest(Player player, out Container container)
		{
			container = null;
			if (!Object.op_Implicit((Object)(object)player) || !ModConfig.Enabled.Value)
			{
				return false;
			}
			GameObject hoverObject = ((Humanoid)player).GetHoverObject();
			if (!Object.op_Implicit((Object)(object)hoverObject))
			{
				return false;
			}
			Container val = hoverObject.GetComponentInParent<Container>() ?? hoverObject.GetComponentInChildren<Container>(true);
			if (!Object.op_Implicit((Object)(object)val) || !IsAccessible(val, player.GetPlayerID()))
			{
				return false;
			}
			container = val;
			return true;
		}

		private static bool IsAccessible(Container container, long playerId)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (container.m_checkGuardStone && !PrivateArea.CheckAccess(((Component)container).transform.position, 0f, false, false))
				{
					return false;
				}
				if (CheckAccessMethod == null)
				{
					if (!_accessMethodWarningLogged)
					{
						_accessMethodWarningLogged = true;
						ManualLogSource log = Plugin.Log;
						if (log != null)
						{
							log.LogError((object)"Container.CheckAccess(long) was not found; hiding chest contents.");
						}
					}
					return false;
				}
				object obj = CheckAccessMethod.Invoke(container, new object[1] { playerId });
				bool flag = default(bool);
				int num;
				if (obj is bool)
				{
					flag = (bool)obj;
					num = 1;
				}
				else
				{
					num = 0;
				}
				return (byte)((uint)num & (flag ? 1u : 0u)) != 0;
			}
			catch (Exception ex)
			{
				if (Time.unscaledTime >= _nextAccessWarningTime)
				{
					_nextAccessWarningTime = Time.unscaledTime + 2f;
					ManualLogSource log2 = Plugin.Log;
					if (log2 != null)
					{
						log2.LogWarning((object)("Chest access check failed; hiding contents: " + ex.Message));
					}
				}
				return false;
			}
		}
	}
	internal static class ChestTextFormatter
	{
		private static readonly Regex RichTextTag = new Regex("<[^>]*>", RegexOptions.Compiled);

		private const string HeaderColor = "#FFD166FF";

		private const string NameColor = "#F2F5FFFF";

		private const string CountColor = "#7CFFB2FF";

		private const string StashSenseColor = "#FFF06AFF";

		internal static string Format(Container container, ChestContentsSnapshot snapshot)
		{
			if (snapshot == null)
			{
				return string.Empty;
			}
			int num = Math.Max(1, ModConfig.MaxLines.Value);
			int maxCharacters = Math.Max(100, ModConfig.MaxTextCharacters.Value);
			StringBuilder stringBuilder = new StringBuilder();
			if (ModConfig.ShowHeader.Value)
			{
				string value = Clean(ChestInventoryReader.Localize(container.GetHoverName()));
				AppendLine(stringBuilder, string.Format("<color={0}><b>{1}</b></color> <color={2}><b>{3}/{4}</b></color>", "#FFD166FF", EscapeRichText(value), "#7CFFB2FF", snapshot.OccupiedSlots, snapshot.TotalSlots), maxCharacters);
			}
			else
			{
				AppendLine(stringBuilder, string.Format("<color={0}><b>{1}/{2}</b></color>", "#7CFFB2FF", snapshot.OccupiedSlots, snapshot.TotalSlots), maxCharacters);
			}
			int num2 = 0;
			foreach (ChestItemEntry item in snapshot.Items)
			{
				if (num2 >= num)
				{
					break;
				}
				string text = EscapeRichText(Clean(item.Name));
				string text2 = ((ModConfig.ShowStackCount.Value && item.SourceStackCount > 1) ? $" ({item.SourceStackCount} stacks)" : string.Empty);
				string text3 = ((ModConfig.StashSenseEnabled.Value && item.MatchesPlayerInventory) ? "#FFF06AFF" : "#F2F5FFFF");
				string value2 = string.Format("<color={0}><b>{1}</b></color> <color={2}><b>x{3}</b></color>{4}", text3, text, "#7CFFB2FF", Math.Max(0, item.Stack), text2);
				if (!AppendLine(stringBuilder, value2, maxCharacters))
				{
					break;
				}
				num2++;
			}
			if (snapshot.Items.Count == 0 && ModConfig.ShowEmptyMessage.Value)
			{
				AppendLine(stringBuilder, "<color=#7CFFB2FF>" + EscapeRichText(Clean(ModConfig.EmptyMessage.Value)) + "</color>", maxCharacters);
			}
			return stringBuilder.ToString().TrimEnd('\r', '\n');
		}

		private static bool AppendLine(StringBuilder text, string value, int maxCharacters)
		{
			value = (string.IsNullOrWhiteSpace(value) ? "Unknown" : value.Trim());
			int num = value.Length + ((text.Length != 0) ? Environment.NewLine.Length : 0);
			if (text.Length + num > maxCharacters)
			{
				return false;
			}
			if (text.Length > 0)
			{
				text.AppendLine();
			}
			text.Append(value);
			return true;
		}

		private static string Clean(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return string.Empty;
			}
			return RichTextTag.Replace(value, string.Empty).Replace('\r', ' ').Replace('\n', ' ')
				.Trim();
		}

		private static string EscapeRichText(string value)
		{
			return value.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
		}
	}
	[HarmonyPatch(typeof(Hud), "UpdateCrosshair")]
	internal static class ChestViewerPatches
	{
		private static void Postfix(Hud __instance, Player player, float bowDrawPercentage)
		{
			try
			{
				ChestOverlay.Apply(__instance, player);
			}
			catch (Exception arg)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogError((object)$"Chest hover update failed: {arg}");
				}
				ChestOverlay.Clear();
			}
		}
	}
	internal static class ModConfig
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<int> UpdateIntervalMs;

		internal static ConfigEntry<int> MaxLines;

		internal static ConfigEntry<int> MaxTextCharacters;

		internal static ConfigEntry<bool> ShowHeader;

		internal static ConfigEntry<bool> ShowStackCount;

		internal static ConfigEntry<bool> StashSenseEnabled;

		internal static ConfigEntry<bool> ShowEmptyMessage;

		internal static ConfigEntry<string> EmptyMessage;

		internal static void Bind(ConfigFile config)
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Expected O, but got Unknown
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Expected O, but got Unknown
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Expected O, but got Unknown
			Enabled = config.Bind<bool>("General", "Enabled", true, "Enable the client-only chest contents display.");
			UpdateIntervalMs = config.Bind<int>("General", "UpdateIntervalMs", 100, new ConfigDescription("Minimum milliseconds between content refresh checks.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(25, 500), Array.Empty<object>()));
			MaxLines = config.Bind<int>("Display", "MaxLines", 30, new ConfigDescription("Maximum number of item lines to display.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>()));
			MaxTextCharacters = config.Bind<int>("Display", "MaxTextCharacters", 1200, new ConfigDescription("Maximum formatted hover-text length.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(100, 4096), Array.Empty<object>()));
			ShowHeader = config.Bind<bool>("Display", "ShowHeader", true, "Show the chest name above the contents.");
			ShowStackCount = config.Bind<bool>("Display", "ShowStackCount", true, "Show how many source stacks contributed to aggregated item rows.");
			StashSenseEnabled = config.Bind<bool>("Stash Sense", "Enabled", true, "Highlight chest items that are also present in your local inventory.");
			ShowEmptyMessage = config.Bind<bool>("Display", "ShowEmptyMessage", true, "Show Empty when the targeted chest has no items.");
			EmptyMessage = config.Bind<string>("Display", "EmptyMessage", "Empty", "Text shown for an empty chest when ShowEmptyMessage is enabled.");
		}
	}
	[BepInPlugin("com.catosaur.catoschestviewer", "Catos Chest Viewer", "0.1.2")]
	[BepInProcess("valheim.exe")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.catosaur.catoschestviewer";

		public const string Name = "Catos Chest Viewer";

		public const string Version = "0.1.2";

		private Harmony _harmony;

		internal static ManualLogSource Log { get; private set; }

		private void Awake()
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			ModConfig.Bind(((BaseUnityPlugin)this).Config);
			_harmony = new Harmony("com.catosaur.catoschestviewer");
			_harmony.PatchAll(typeof(ChestViewerPatches));
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Catos Chest Viewer 0.1.2 client-only loaded.");
		}

		private void LateUpdate()
		{
			ChestOverlay.Apply(Hud.instance, Player.m_localPlayer);
		}

		private void OnDestroy()
		{
			ChestOverlay.Clear();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchAll("com.catosaur.catoschestviewer");
			}
		}
	}
}