Decompiled source of PortalMetalLock v1.5.1

PortalMetalLock.dll

Decompiled 19 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Utils;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace BakaPortalMetalLock;

[BepInPlugin("baka.PortalMetalLock", "Portal Metal Lock", "1.5.1")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
public class PortalMetalLock : BaseUnityPlugin
{
	private sealed class Sets
	{
		public HashSet<string> Keywords;

		public HashSet<string> Extra;

		public HashSet<string> Exclude;

		public HashSet<string> ExcludeTokens;
	}

	private sealed class ConfigurationManagerAttributes
	{
		public bool? IsAdminOnly;
	}

	[HarmonyPatch(typeof(ObjectDB), "Awake")]
	private static class Patch_ObjectDB_Awake
	{
		private static void Postfix(ObjectDB __instance)
		{
			ApplyTo(__instance, "ObjectDB.Awake");
		}
	}

	[HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")]
	private static class Patch_ObjectDB_CopyOtherDB
	{
		private static void Postfix(ObjectDB __instance)
		{
			ApplyTo(__instance, "ObjectDB.CopyOtherDB");
		}
	}

	[HarmonyPatch(typeof(ZNetScene), "Awake")]
	private static class Patch_ZNetScene_Awake
	{
		private static void Postfix()
		{
			if ((Object)(object)Instance != (Object)null)
			{
				((MonoBehaviour)Instance).StartCoroutine(DelayedRescans());
			}
		}
	}

	[HarmonyPatch(typeof(Player), "OnInventoryChanged")]
	private static class Patch_Player_OnInventoryChanged
	{
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance == (Object)null))
			{
				RestampInventory(((Humanoid)__instance).GetInventory());
			}
		}
	}

	[HarmonyPatch(typeof(ItemDrop), "Awake")]
	private static class Patch_ItemDrop_Awake
	{
		private static void Postfix(ItemDrop __instance)
		{
			if (!((Object)(object)__instance == (Object)null) && __instance.m_itemData != null)
			{
				RestampItem(__instance.m_itemData);
			}
		}
	}

	[HarmonyPatch(typeof(Inventory), "IsTeleportable")]
	private static class Patch_Inventory_IsTeleportable
	{
		private static void Prefix(Inventory __instance)
		{
			RestampInventory(__instance, fromTeleportCheck: true);
		}
	}

	[HarmonyPatch(typeof(Humanoid), "IsTeleportable")]
	private static class Patch_Humanoid_IsTeleportable
	{
		private static void Prefix(Humanoid __instance)
		{
			if (!((Object)(object)__instance == (Object)null))
			{
				Inventory inventory = __instance.GetInventory();
				if (inventory != null)
				{
					RestampInventory(inventory, fromTeleportCheck: true);
				}
			}
		}
	}

	public const string GUID = "baka.PortalMetalLock";

	public const string Version = "1.5.1";

	internal static ManualLogSource Log;

	internal static PortalMetalLock Instance;

	private static ConfigEntry<bool> _enabled;

	private static ConfigEntry<string> _keywords;

	private static ConfigEntry<string> _extraPrefabs;

	private static ConfigEntry<string> _excludePrefabs;

	private static ConfigEntry<string> _excludeTokens;

	private static ConfigEntry<bool> _logDecisions;

	private const string DefaultKeywords = "copper,tin,bronze,iron,silver,blackmetal,flametal,metal,ore,ores,ingot,ingots,steel,nickel,zinc,lead,brass,cupronickel,cupro,bellmetal,frosteel,darksteel,moltensteel,whitemetal,amethysteel,ifrytium,cerium,ferroboron,darkiron,thorium,aurametal,vanadium,gold,electrum,bismuth,tungsten,cobalt,pewter,mithril,adamant,adamantite,orichalcum,platinum,titanium,aluminium,aluminum,chromium";

	private const string DefaultExcludeTokens = "nail,nails,meat,necklace,food,fish,key,coin,gem,leather,hide,pelt,trophy,wood,resin,seed";

	private const ItemType BlockableType = (ItemType)1;

	private static readonly HashSet<string> SuspiciousTokens = new HashSet<string>(StringComparer.Ordinal)
	{
		"metal", "ore", "ores", "scrap", "scraps", "ingot", "ingots", "bar", "bars", "steel",
		"iron", "copper", "tin", "bronze", "silver", "blackmetal", "flametal", "nickel", "zinc", "lead",
		"brass", "cupronickel", "cupro", "bellmetal", "frosteel", "darksteel", "moltensteel", "whitemetal", "amethysteel", "ifrytium",
		"cerium", "ferroboron", "thorium", "aurametal", "vanadium", "gold", "electrum", "bismuth", "tungsten", "cobalt"
	};

	private static readonly Regex CamelBoundary = new Regex("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", RegexOptions.Compiled);

	private static readonly Regex NonAlphaNum = new Regex("[^A-Za-z0-9]+", RegexOptions.Compiled);

	private static Sets _sets;

	private static int _restampLogCooldown;

	private void Awake()
	{
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Expected O, but got Unknown
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0078: Expected O, but got Unknown
		//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b0: Expected O, but got Unknown
		//IL_00de: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e8: Expected O, but got Unknown
		//IL_0116: Unknown result type (might be due to invalid IL or missing references)
		//IL_0120: Expected O, but got Unknown
		//IL_014a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0154: Expected O, but got Unknown
		//IL_015e: 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)
		Log = ((BaseUnityPlugin)this).Logger;
		Instance = this;
		_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, new ConfigDescription("Master switch. When on, matching raw metal/ore/bar/scrap Material items are made non-teleportable so wood portals refuse them. ALL equipment goes through, and the Ashlands stone portal still carries everything (vanilla behaviour).", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		_keywords = ((BaseUnityPlugin)this).Config.Bind<string>("General", "MetalKeywords", "copper,tin,bronze,iron,silver,blackmetal,flametal,metal,ore,ores,ingot,ingots,steel,nickel,zinc,lead,brass,cupronickel,cupro,bellmetal,frosteel,darksteel,moltensteel,whitemetal,amethysteel,ifrytium,cerium,ferroboron,darkiron,thorium,aurametal,vanadium,gold,electrum,bismuth,tungsten,cobalt,pewter,mithril,adamant,adamantite,orichalcum,platinum,titanium,aluminium,aluminum,chromium", new ConfigDescription("Comma-separated, case-insensitive keywords. Names are split into tokens on camelCase and non-letters, and an item is blocked if any token EQUALS one of these keywords (whole-token match, not substring -- so 'tin' will not match 'chitin' nor 'ore' match 'core'). Only items of type Material are ever considered, so equipment that contains a metal word is never affected.", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		_extraPrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("General", "ExtraPrefabs", "", new ConfigDescription("Comma-separated EXACT prefab names to ALWAYS force non-teleportable (overrides type/keyword/exclude checks), for mod metals the keywords miss. Use VNEI in-game, or the PortalMetalLock_items.txt dump, to find exact names.", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		_excludePrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("General", "ExcludePrefabs", "BronzeNails,IronNails,LeatherScraps,SilverNecklace,BrassChain_bal", new ConfigDescription("Comma-separated EXACT prefab names to NEVER touch (false-positive guard, highest priority after ExtraPrefabs).", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		_excludeTokens = ((BaseUnityPlugin)this).Config.Bind<string>("General", "ExcludeTokens", "nail,nails,meat,necklace,food,fish,key,coin,gem,leather,hide,pelt,trophy,wood,resin,seed", new ConfigDescription("Comma-separated tokens; any Material item whose name contains one of these tokens is left teleportable even if a metal keyword matched. Keeps nails, leather scraps, food and necklaces portable.", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		_logDecisions = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "LogDecisions", true, new ConfigDescription("When true, after each ObjectDB scan the mod writes a full item dump to BepInEx/config/PortalMetalLock_items.txt (prefab | display | type | teleportable | decision) and logs which metal-ish items it SKIPPED and why. Use this to verify classification and tune ExtraPrefabs/ExcludePrefabs. Set false to quieten once tuned.", (AcceptableValueBase)null, new object[1] { AdminOnly() }));
		Harmony val = new Harmony("baka.PortalMetalLock");
		val.PatchAll();
		IEnumerable<MethodBase> patchedMethods = val.GetPatchedMethods();
		HashSet<string> hashSet = new HashSet<string>();
		foreach (MethodBase item in patchedMethods)
		{
			hashSet.Add(item.DeclaringType?.FullName + "." + item.Name);
		}
		bool num = hashSet.Contains("Inventory.IsTeleportable");
		bool flag = hashSet.Contains("Humanoid.IsTeleportable");
		Log.LogInfo((object)("PortalMetalLock v1.5.1 loaded. Harmony patches applied: " + string.Join(", ", hashSet)));
		if (!num)
		{
			Log.LogWarning((object)"CRITICAL: Inventory.IsTeleportable patch MISSING -- teleport gate may not fire!");
		}
		if (!flag)
		{
			Log.LogWarning((object)"CRITICAL: Humanoid.IsTeleportable patch MISSING -- teleport gate fallback unavailable!");
		}
	}

	private static ConfigurationManagerAttributes AdminOnly()
	{
		return new ConfigurationManagerAttributes
		{
			IsAdminOnly = true
		};
	}

	private static Sets RebuildSets()
	{
		Sets obj = new Sets
		{
			Keywords = SplitSet(_keywords.Value, lower: true),
			Extra = SplitSet(_extraPrefabs.Value, lower: false),
			Exclude = SplitSet(_excludePrefabs.Value, lower: false),
			ExcludeTokens = SplitSet(_excludeTokens.Value, lower: true)
		};
		_sets = obj;
		return obj;
	}

	private static Sets CurrentSets()
	{
		return _sets ?? RebuildSets();
	}

	private static bool ShouldBlock(string prefab, SharedData shared, Sets s)
	{
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_003c: Invalid comparison between Unknown and I4
		if (shared == null)
		{
			return false;
		}
		if (!string.IsNullOrEmpty(prefab) && s.Extra.Contains(prefab))
		{
			return true;
		}
		if (!string.IsNullOrEmpty(prefab) && s.Exclude.Contains(prefab))
		{
			return false;
		}
		if ((int)shared.m_itemType != 1)
		{
			return false;
		}
		HashSet<string> hashSet = Tokenize(prefab, shared.m_name);
		if (hashSet.Overlaps(s.ExcludeTokens))
		{
			return false;
		}
		return hashSet.Overlaps(s.Keywords);
	}

	private static IEnumerator DelayedRescans()
	{
		float[] array = new float[3] { 12f, 18f, 30f };
		for (int i = 0; i < array.Length; i++)
		{
			float delay = array[i];
			yield return (object)new WaitForSeconds(delay);
			ApplyTo(ObjectDB.instance, "delayed rescan +" + delay + "s");
		}
	}

	private static void RestampInventory(Inventory inv, bool fromTeleportCheck = false)
	{
		if (_enabled == null || !_enabled.Value || inv == null)
		{
			return;
		}
		List<ItemData> allItems = inv.GetAllItems();
		if (allItems == null)
		{
			return;
		}
		int num = 0;
		for (int i = 0; i < allItems.Count; i++)
		{
			if (RestampItem(allItems[i]))
			{
				num++;
			}
		}
		if (fromTeleportCheck && num > 0 && ++_restampLogCooldown % 60 == 1)
		{
			Log.LogInfo((object)("IsTeleportable Prefix re-stamped " + num + " item(s) that had reverted to teleportable=true."));
		}
	}

	private static bool RestampItem(ItemData item)
	{
		if (_enabled == null || !_enabled.Value || item == null)
		{
			return false;
		}
		SharedData shared = item.m_shared;
		if (shared == null || !shared.m_teleportable)
		{
			return false;
		}
		if (ShouldBlock(((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : null, shared, CurrentSets()))
		{
			shared.m_teleportable = false;
			return true;
		}
		return false;
	}

	private static int ApplyTo(ObjectDB odb, string source)
	{
		//IL_010c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0112: Invalid comparison between Unknown and I4
		if (_enabled == null || !_enabled.Value)
		{
			return 0;
		}
		if ((Object)(object)odb == (Object)null || odb.m_items == null)
		{
			return 0;
		}
		Sets sets = RebuildSets();
		bool value = _logDecisions.Value;
		int num = 0;
		List<string> list = (value ? new List<string>(odb.m_items.Count) : null);
		List<string> list2 = (value ? new List<string>() : null);
		for (int i = 0; i < odb.m_items.Count; i++)
		{
			GameObject val = odb.m_items[i];
			if ((Object)(object)val == (Object)null)
			{
				continue;
			}
			ItemDrop component = val.GetComponent<ItemDrop>();
			if ((Object)(object)component == (Object)null || component.m_itemData == null)
			{
				continue;
			}
			SharedData shared = component.m_itemData.m_shared;
			if (shared == null)
			{
				continue;
			}
			string text = ((Object)val).name ?? string.Empty;
			bool flag = ShouldBlock(text, shared, sets);
			if (flag && shared.m_teleportable)
			{
				shared.m_teleportable = false;
				num++;
			}
			if (value)
			{
				string text2 = (flag ? "BLOCK" : (sets.Exclude.Contains(text) ? "skip(ExcludePrefab)" : (((int)shared.m_itemType != 1) ? "skip(non-Material)" : "skip(no-match)")));
				list.Add(text + " | " + shared.m_name + " | " + ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString() + " | teleportable=" + shared.m_teleportable + " | " + text2);
				if (!flag && Tokenize(text, shared.m_name).Overlaps(SuspiciousTokens))
				{
					list2.Add(text + " (type=" + ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString() + ")");
				}
			}
		}
		if (num > 0 || value)
		{
			Log.LogInfo((object)("PortalMetalLock (" + source + "): marked " + num + " item(s) non-teleportable."));
		}
		if (value)
		{
			WriteDump(source, list);
			if (list2 != null && list2.Count > 0)
			{
				Log.LogInfo((object)("  SKIPPED metal-ish (left teleportable -> add to ExtraPrefabs if wrong): " + string.Join(", ", list2.Take(120)) + ((list2.Count > 120) ? (" ...(+" + (list2.Count - 120) + ")") : "")));
			}
		}
		return num;
	}

	private static void WriteDump(string source, List<string> dump)
	{
		if (dump == null)
		{
			return;
		}
		try
		{
			string path = Path.Combine(Paths.ConfigPath, "PortalMetalLock_items.txt");
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# PortalMetalLock v1.5.1 item dump");
			stringBuilder.AppendLine("# scan source: " + source + "   time: " + DateTime.Now.ToString("s"));
			stringBuilder.AppendLine("# prefab | display | itemType | teleportable | decision");
			stringBuilder.AppendLine("# (decision BLOCK = made non-teleportable; only Material items can be blocked)");
			dump.Sort(StringComparer.OrdinalIgnoreCase);
			foreach (string item in dump)
			{
				stringBuilder.AppendLine(item);
			}
			File.WriteAllText(path, stringBuilder.ToString());
		}
		catch (Exception ex)
		{
			Log.LogWarning((object)("PortalMetalLock: could not write item dump: " + ex.Message));
		}
	}

	private static HashSet<string> Tokenize(params string[] names)
	{
		HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
		foreach (string text in names)
		{
			if (string.IsNullOrEmpty(text))
			{
				continue;
			}
			string[] array = NonAlphaNum.Split(text);
			foreach (string text2 in array)
			{
				if (text2.Length == 0)
				{
					continue;
				}
				string[] array2 = CamelBoundary.Split(text2);
				foreach (string text3 in array2)
				{
					if (text3.Length != 0)
					{
						hashSet.Add(text3.ToLowerInvariant());
					}
				}
			}
		}
		return hashSet;
	}

	private static HashSet<string> SplitSet(string csv, bool lower)
	{
		HashSet<string> hashSet = new HashSet<string>(lower ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
		if (string.IsNullOrEmpty(csv))
		{
			return hashSet;
		}
		string[] array = csv.Split(',');
		for (int i = 0; i < array.Length; i++)
		{
			string text = array[i].Trim();
			if (text.Length != 0)
			{
				hashSet.Add(lower ? text.ToLowerInvariant() : text);
			}
		}
		return hashSet;
	}
}