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 Keywords; public HashSet Extra; public HashSet Exclude; public HashSet 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 _enabled; private static ConfigEntry _keywords; private static ConfigEntry _extraPrefabs; private static ConfigEntry _excludePrefabs; private static ConfigEntry _excludeTokens; private static ConfigEntry _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 SuspiciousTokens = new HashSet(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("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("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("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("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("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("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 patchedMethods = val.GetPatchedMethods(); HashSet hashSet = new HashSet(); 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 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 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 list = (value ? new List(odb.m_items.Count) : null); List list2 = (value ? new List() : 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(); 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(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(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 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 Tokenize(params string[] names) { HashSet hashSet = new HashSet(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 SplitSet(string csv, bool lower) { HashSet hashSet = new HashSet(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; } }