Decompiled source of BossGatedPortals v0.1.4

plugins/BossGatedPortals.dll

Decompiled 19 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Jotunn;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("BossGatedPortals")]
[assembly: AssemblyProduct("BossGatedPortals")]
[assembly: ComVisible(false)]
[assembly: AssemblyFileVersion("0.1.4")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.4.0")]
[module: UnverifiableCode]
namespace BossGatedPortals;

[HarmonyPatch(typeof(ZoneSystem), "RPC_SetGlobalKey")]
internal static class Announcements
{
	private static void Prefix(ZoneSystem __instance, string name, out bool __state)
	{
		try
		{
			__state = WasSet(__instance, name);
		}
		catch (Exception e)
		{
			Failsafe.Report("Tier unlock announcement", e);
			__state = true;
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static bool WasSet(ZoneSystem zoneSystem, string name)
	{
		return zoneSystem.GetGlobalKey(name);
	}

	private static void Postfix(ZoneSystem __instance, string name, bool __state)
	{
		try
		{
			Announce(__instance, name, __state);
		}
		catch (Exception e)
		{
			Failsafe.Report("Tier unlock announcement", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static void Announce(ZoneSystem zoneSystem, string name, bool wasSet)
	{
		if (wasSet || !Settings.Enabled.Value || !Settings.AnnounceTierUnlock.Value || !zoneSystem.GetGlobalKey(name))
		{
			return;
		}
		string text = name.Trim().ToLowerInvariant();
		foreach (Tier tier in Settings.Tiers)
		{
			if (!(tier.GlobalKey != text) && !string.IsNullOrEmpty(tier.UnlockMessage))
			{
				Logger.LogInfo((object)$"AnnounceTierUnlock: '{text}' set; announcing T{tier.Index} {tier.Name}.");
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, tier.UnlockMessage });
			}
		}
	}
}
[BepInPlugin("com.jtmill01.bossgatedportals", "BossGatedPortals", "0.1.4")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
internal class BossGatedPortals : BaseUnityPlugin
{
	public const string PluginGUID = "com.jtmill01.bossgatedportals";

	public const string PluginName = "BossGatedPortals";

	public const string PluginVersion = "0.1.4";

	public const string XPortalGUID = "yay.spikehimself.xportal";

	private readonly Harmony harmony = new Harmony("com.jtmill01.bossgatedportals.harmony");

	private void Awake()
	{
		Settings.Bind(((BaseUnityPlugin)this).Config);
		Settings.LogSummary();
		((BaseUnityPlugin)this).Config.SettingChanged += delegate
		{
			Settings.Parse();
		};
		SynchronizationManager.OnConfigurationSynchronized += delegate
		{
			ReportSettings();
		};
		SynchronizationManager.OnConfigurationWindowClosed += ReportSettings;
		ItemManager.OnItemsRegistered += Settings.Validate;
		ItemManager.OnItemsRegistered += Hints.LogUnmappedItems;
		PatchEachHook();
		CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new StatusCommand());
		bool flag = Chainloader.PluginInfos.ContainsKey("yay.spikehimself.xportal");
		Logger.LogInfo((object)string.Format("{0} {1} loaded. XPortal detected: {2}", "BossGatedPortals", "0.1.4", flag));
	}

	private void Start()
	{
		Compatibility.LogInstalledMods();
	}

	private void PatchEachHook()
	{
		Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(BossGatedPortals).Assembly);
		foreach (Type type in typesFromAssembly)
		{
			try
			{
				harmony.CreateClassProcessor(type).Patch();
			}
			catch (Exception arg)
			{
				Logger.LogError((object)("Couldn't hook the game for " + type.Name + " (game update?). " + $"That feature stays vanilla; the rest of the mod still works. {arg}"));
			}
		}
	}

	private static void ReportSettings()
	{
		Settings.Parse();
		Settings.LogSummary();
		Settings.Validate();
		Hints.LogUnmappedItems();
	}
}
internal static class Compatibility
{
	private static readonly Dictionary<string, string> Conflicts = new Dictionary<string, string>
	{
		{ "unrestrictedportals", "changes which items portals allow" },
		{ "teleporteverything", "turn its item transport OFF (keep its creature/cart transport if you want it)" },
		{ "advancedportals", "has its own item tiers for portals" },
		{ "progressionportals", "has its own progression item gate" },
		{ "worldadvancementprogression", "hooks the SAME item check with high priority and can switch a block back to allowed; never run it alongside this mod" },
		{ "serversideqol", "its PortalProgression feature gates portal items too; turn that feature off" }
	};

	public static void LogInstalledMods()
	{
		List<PluginInfo> list = Chainloader.PluginInfos.Values.Where((PluginInfo p) => p.Metadata.GUID != "com.jtmill01.bossgatedportals").ToList();
		if (Settings.WarnOnConflictingMods.Value)
		{
			foreach (PluginInfo item in list)
			{
				string id = Normalize(item.Metadata.GUID) + " " + Normalize(item.Metadata.Name);
				foreach (KeyValuePair<string, string> item2 in Conflicts.Where((KeyValuePair<string, string> c) => id.Contains(c.Key)))
				{
					Logger.LogWarning((object)("WarnOnConflictingMods: " + Describe(item) + " also changes item teleport rules (" + item2.Value + "). Two item-gate mods together give unpredictable results."));
				}
			}
		}
		if (Settings.LogPortalModDetection.Value)
		{
			PluginInfo val = ((IEnumerable<PluginInfo>)list).FirstOrDefault((Func<PluginInfo, bool>)((PluginInfo p) => p.Metadata.GUID == "yay.spikehimself.xportal"));
			PluginInfo val2 = ((IEnumerable<PluginInfo>)list).FirstOrDefault((Func<PluginInfo, bool>)((PluginInfo p) => Matches(p, "xportalnetworks")));
			PluginInfo val3 = ((IEnumerable<PluginInfo>)list).FirstOrDefault((Func<PluginInfo, bool>)((PluginInfo p) => p.Metadata.GUID == "com.sweetgiorni.anyportal" || Matches(p, "anyportal")));
			Logger.LogInfo((object)("LogPortalModDetection: XPortal " + ((val != null) ? Describe(val) : "not installed") + ", XPortalNetworks " + ((val2 != null) ? Describe(val2) : "not installed") + "."));
			if (val3 != null)
			{
				Logger.LogWarning((object)("LogPortalModDetection: " + Describe(val3) + " is installed. XPortal refuses to load alongside it; remove AnyPortal and use XPortal instead."));
			}
		}
	}

	private static bool Matches(PluginInfo p, string fragment)
	{
		if (!Normalize(p.Metadata.GUID).Contains(fragment))
		{
			return Normalize(p.Metadata.Name).Contains(fragment);
		}
		return true;
	}

	private static string Normalize(string s)
	{
		return new string((from c in (s ?? "").ToLowerInvariant()
			where c != ' ' && c != '-' && c != '_'
			select c).ToArray());
	}

	private static string Describe(PluginInfo p)
	{
		return $"{p.Metadata.Name} {p.Metadata.Version} ({p.Metadata.GUID})";
	}
}
internal static class Failsafe
{
	private static readonly HashSet<string> reported = new HashSet<string>();

	public static void Report(string where, Exception e)
	{
		if (reported.Add(where))
		{
			Logger.LogError((object)(where + " failed, using vanilla behaviour there instead " + $"(game update?): {e}"));
		}
	}

	public static bool TeleportAllSet()
	{
		return ZoneSystem.instance.GetGlobalKey("teleportall");
	}
}
internal static class Gate
{
	public static bool CanTeleport(Player player, ItemData item, bool portalAllowsAll = false)
	{
		bool teleportable = item.m_shared.m_teleportable;
		bool flag = item.m_shared.m_toolTier >= 1000;
		bool flag2 = portalAllowsAll || Failsafe.TeleportAllSet();
		if (!Settings.Enabled.Value)
		{
			if (!flag)
			{
				return flag2 || teleportable;
			}
			return false;
		}
		string text = PrefabName(item);
		if (text != null && Settings.NeverTeleportItems.Contains(text))
		{
			return false;
		}
		if (flag)
		{
			if (Settings.FinalTierIncludesCheatTier.Value)
			{
				return FinalTierUnlocksEverything(player);
			}
			return false;
		}
		if (flag2 && Settings.RespectVanillaAllowAll.Value)
		{
			return true;
		}
		Tier tier = ((text != null) ? TierFor(text) : null);
		if (teleportable && (tier == null || !Settings.GateListedVanillaItems.Value))
		{
			return true;
		}
		if (tier == null || !IsTierUnlocked(player, tier))
		{
			return FinalTierUnlocksEverything(player);
		}
		return true;
	}

	public static bool IsTierUnlocked(Player player, Tier tier)
	{
		if (IsBypassing(player))
		{
			return true;
		}
		List<Tier> tiers = Settings.Tiers;
		int num = (Settings.CumulativeTiers.Value ? (tiers.Count - 1) : tier.Index);
		for (int i = tier.Index; i <= num; i++)
		{
			if (IsTierEarned(player, tiers[i]))
			{
				return true;
			}
		}
		return false;
	}

	public static bool IsTierEarned(Player player, Tier tier)
	{
		if (Settings.EffectiveRequireWorldKey && (string.IsNullOrEmpty(tier.GlobalKey) || !ZoneSystem.instance.GetGlobalKey(tier.GlobalKey)))
		{
			return false;
		}
		if (Settings.RequirePlayerBossItem.Value && (string.IsNullOrEmpty(tier.BossItemToken) || !player.IsMaterialKnown(tier.BossItemToken)))
		{
			return false;
		}
		return true;
	}

	public static Tier TierFor(string prefab)
	{
		foreach (Tier tier in Settings.Tiers)
		{
			if (tier.Items.Contains(prefab))
			{
				return tier;
			}
		}
		return null;
	}

	private static bool FinalTierUnlocksEverything(Player player)
	{
		List<Tier> tiers = Settings.Tiers;
		if (tiers.Count == 0)
		{
			return false;
		}
		Tier tier = tiers[tiers.Count - 1];
		if (tier.AllowEverything)
		{
			return IsTierUnlocked(player, tier);
		}
		return false;
	}

	private static bool IsBypassing(Player player)
	{
		if (Settings.AdminBypass.Value && (Object)(object)player == (Object)(object)Player.m_localPlayer)
		{
			return SynchronizationManager.Instance.PlayerIsAdmin;
		}
		return false;
	}

	public static string PrefabName(ItemData item)
	{
		if (Object.op_Implicit((Object)(object)item.m_dropPrefab))
		{
			return ((Object)item.m_dropPrefab).name;
		}
		GameObject val = default(GameObject);
		if (Object.op_Implicit((Object)(object)ObjectDB.instance) && ObjectDB.instance.TryGetItemPrefab(item.m_shared, ref val) && Object.op_Implicit((Object)(object)val))
		{
			return ((Object)val).name;
		}
		return null;
	}
}
[HarmonyPatch(typeof(Inventory), "IsTeleportable")]
internal static class InventoryIsTeleportablePatch
{
	private static readonly FieldInfo VagonInstances = AccessTools.Field(typeof(Vagon), "m_instances");

	private static void Postfix(Inventory __instance, bool allowAllItems, ref bool __result)
	{
		try
		{
			Gatekeep(__instance, allowAllItems, ref __result);
		}
		catch (Exception e)
		{
			Failsafe.Report("Portal item check", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static void Gatekeep(Inventory inventory, bool allowAllItems, ref bool result)
	{
		Player localPlayer = Player.m_localPlayer;
		if (!Settings.Enabled.Value || (Object)(object)localPlayer == (Object)null)
		{
			return;
		}
		Inventory val = AttachedCartCargo(localPlayer);
		bool flag = ((Humanoid)localPlayer).GetInventory() == inventory;
		if (flag || inventory == val)
		{
			Hints.LastBlocked.Clear();
			AddBlocked(localPlayer, inventory, allowAllItems);
			if (flag && val != null)
			{
				AddBlocked(localPlayer, val, allowAllItems);
			}
			result = Hints.LastBlocked.Count == 0;
		}
	}

	private static void AddBlocked(Player player, Inventory inventory, bool allowAllItems)
	{
		foreach (ItemData allItem in inventory.GetAllItems())
		{
			if (!Gate.CanTeleport(player, allItem, allowAllItems))
			{
				Hints.LastBlocked.Add(allItem);
			}
		}
	}

	private static Inventory AttachedCartCargo(Player player)
	{
		if (!Settings.GateAttachedCartCargo.Value)
		{
			return null;
		}
		if (!(VagonInstances?.GetValue(null) is List<Vagon> list))
		{
			return null;
		}
		foreach (Vagon item in list)
		{
			if (Object.op_Implicit((Object)(object)item) && Object.op_Implicit((Object)(object)item.m_container) && item.IsAttached((Character)(object)player))
			{
				return item.m_container.GetInventory();
			}
		}
		return null;
	}
}
internal static class Hints
{
	[HarmonyPatch(typeof(Player), "Message")]
	private static class PlayerMessagePatch
	{
		private static bool Prefix(Player __instance, ref MessageType type, ref string msg)
		{
			try
			{
				return SwapForHint(__instance, ref type, ref msg);
			}
			catch (Exception e)
			{
				Failsafe.Report("Unlock hint", e);
				return true;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static bool SwapForHint(Player player, ref MessageType type, ref string msg)
		{
			if (msg != "$msg_noteleport" || (Object)(object)player != (Object)(object)Player.m_localPlayer || !Settings.Enabled.Value || !Settings.ShowUnlockHint.Value)
			{
				return true;
			}
			string text = BuildMessage();
			if (text == null)
			{
				return true;
			}
			if (Time.time - lastHintTime < Settings.HintCooldownSeconds.Value)
			{
				return false;
			}
			lastHintTime = Time.time;
			msg = text;
			type = (MessageType)((Settings.HintPositionSetting.Value == HintPosition.TopLeft) ? 1 : 2);
			return true;
		}
	}

	public static readonly List<ItemData> LastBlocked = new List<ItemData>();

	private static float lastHintTime = float.NegativeInfinity;

	public static string BuildMessage()
	{
		List<Tier> tiers = Settings.Tiers;
		if (tiers.Count == 0)
		{
			return null;
		}
		Tier tier = tiers[tiers.Count - 1];
		Tier tier2 = null;
		ItemData val = null;
		foreach (ItemData item in LastBlocked)
		{
			string text = Gate.PrefabName(item);
			if (text != null && Settings.NeverTeleportItems.Contains(text))
			{
				continue;
			}
			Tier tier3;
			if (item.m_shared.m_toolTier >= 1000)
			{
				if (!Settings.FinalTierIncludesCheatTier.Value || !tier.AllowEverything)
				{
					continue;
				}
				tier3 = tier;
			}
			else
			{
				tier3 = ((text != null) ? Gate.TierFor(text) : null) ?? tier;
			}
			if (tier2 == null || tier3.Index < tier2.Index)
			{
				tier2 = tier3;
				val = item;
			}
		}
		if (tier2 == null || string.IsNullOrEmpty(tier2.Hint))
		{
			return null;
		}
		return Settings.HintFormat.Value.Replace("{hint}", tier2.Hint).Replace("{item}", val.m_shared.m_name);
	}

	public static void LogUnmappedItems()
	{
		if ((Object)(object)ObjectDB.instance == (Object)null || !Settings.LogUnmappedItems.Value)
		{
			return;
		}
		List<string> list = new List<string>();
		foreach (GameObject item in ObjectDB.instance.m_items)
		{
			ItemDrop val = (Object.op_Implicit((Object)(object)item) ? item.GetComponent<ItemDrop>() : null);
			if (!((Object)(object)val == (Object)null) && !val.m_itemData.m_shared.m_teleportable && !Settings.NeverTeleportItems.Contains(((Object)item).name) && Gate.TierFor(((Object)item).name) == null)
			{
				list.Add(((Object)item).name);
			}
		}
		if (list.Count == 0)
		{
			Logger.LogInfo((object)"LogUnmappedItems: every teleport-blocked item is listed in a tier.");
			return;
		}
		Logger.LogInfo((object)("LogUnmappedItems: teleport-blocked items not in any tier (locked until the final tier): " + string.Join(", ", list.OrderBy((string n) => n))));
	}
}
public enum HintPosition
{
	Centered,
	TopLeft
}
internal static class Settings
{
	private class TierEntries
	{
		public string Name;

		public ConfigEntry<string> GlobalKey;

		public ConfigEntry<string> BossItem;

		public ConfigEntry<string> Items;

		public ConfigEntry<string> Hint;

		public ConfigEntry<string> UnlockMessage;

		public ConfigEntry<bool> AllowEverything;
	}

	public static ConfigEntry<bool> Enabled;

	public static ConfigEntry<bool> RequireWorldKey;

	public static ConfigEntry<bool> RequirePlayerBossItem;

	public static ConfigEntry<bool> CumulativeTiers;

	public static ConfigEntry<string> NeverTeleport;

	public static ConfigEntry<bool> HildirChestsByTier;

	public static ConfigEntry<bool> GateListedVanillaItems;

	public static ConfigEntry<bool> FinalTierIncludesCheatTier;

	public static ConfigEntry<bool> LogUnmappedItems;

	public static ConfigEntry<bool> ValidateItemIds;

	public static ConfigEntry<bool> ShowUnlockHint;

	public static ConfigEntry<string> HintFormat;

	public static ConfigEntry<HintPosition> HintPositionSetting;

	public static ConfigEntry<float> HintCooldownSeconds;

	public static ConfigEntry<bool> AccurateTooltips;

	public static ConfigEntry<bool> AnnounceTierUnlock;

	public static ConfigEntry<bool> AdminBypass;

	public static ConfigEntry<bool> EnableStatusCommand;

	public static ConfigEntry<bool> RespectVanillaAllowAll;

	public static ConfigEntry<bool> GateAttachedCartCargo;

	public static ConfigEntry<bool> WarnOnConflictingMods;

	public static ConfigEntry<bool> LogPortalModDetection;

	public static List<Tier> Tiers = new List<Tier>();

	public static HashSet<string> NeverTeleportItems = new HashSet<string>();

	private static readonly List<TierEntries> tierEntries = new List<TierEntries>();

	private static readonly string[][] TierDefaults = new string[8][]
	{
		new string[6] { "Meadows", "defeated_eikthyr", "HardAntler", "", "", "" },
		new string[6] { "Black Forest", "defeated_gdking", "CryptKey", "CopperOre, Copper, CopperScrap, TinOre, Tin, Bronze, BronzeScrap", "Something old still stirs beneath the Black Forest canopy.", "The portals hum. Metals of the forest may now pass." },
		new string[6] { "Swamp", "defeated_bonemass", "Wishbone", "IronOre, IronScrap, Iron, Ironpit", "The Swamp's guardian has not yet been put to rest.", "The portals hum. Iron from the Swamp may now pass." },
		new string[6] { "Mountains", "defeated_dragon", "DragonTear", "SilverOre, Silver, DragonEgg", "The Mountain peaks answer to a ruler you have not faced.", "The portals hum. Treasures of the Mountains may now pass." },
		new string[6] { "Plains", "defeated_goblinking", "YagluthDrop", "BlackMetalScrap, BlackMetal", "The Plains still bow to their fallen king.", "The portals hum. Black metal may now pass." },
		new string[6] { "Mistlands", "defeated_queen", "QueenDrop", "MechanicalSpring, DvergrNeedle", "Something waits deep within the Mistlands.", "The portals hum. Relics of the Mistlands may now pass." },
		new string[6] { "Ashlands", "defeated_fader", "FaderDrop", "FlametalOreNew, FlametalNew, FlametalOre, Flametal, CharredCogwheel", "The Ashlands' guardian still smolders.", "The portals hum. Flametal may now pass." },
		new string[6] { "Deep North", "defeated_frozenking_p3", "FrozenKingDrop", "GoldOre, Gold", "The portal will not carry this until the far North is conquered.", "The portals roar. Nothing is barred from passage now." }
	};

	private static readonly Dictionary<string, int> HildirChestTiers = new Dictionary<string, int>
	{
		{ "chest_hildir1", 1 },
		{ "chest_hildir2", 3 },
		{ "chest_hildir3", 4 }
	};

	private static ConfigFile config;

	private static int order;

	public static bool EffectiveRequireWorldKey
	{
		get
		{
			if (!RequireWorldKey.Value)
			{
				return !RequirePlayerBossItem.Value;
			}
			return true;
		}
	}

	public static void Bind(ConfigFile cfg)
	{
		config = cfg;
		bool saveOnConfigSet = cfg.SaveOnConfigSet;
		cfg.SaveOnConfigSet = false;
		try
		{
			BindEntries();
		}
		finally
		{
			cfg.Save();
			cfg.SaveOnConfigSet = saveOnConfigSet;
		}
		Parse();
	}

	private static void BindEntries()
	{
		Enabled = Admin("0.1 - General", "Enabled", value: true, "Master switch for the mod.");
		RequireWorldKey = Admin("0.1 - General", "RequireWorldKey", value: true, "The boss must have been killed ON THIS WORLD (world-wide global key, saved on the server).");
		RequirePlayerBossItem = Admin("0.1 - General", "RequirePlayerBossItem", value: true, "The individual player must also have picked up that boss's drop at least once. Makes EVERY player follow progression.\nSafety rule: if this and RequireWorldKey are BOTH false, the world key is still required and a warning is logged.");
		CumulativeTiers = Admin("0.1 - General", "CumulativeTiers", value: false, "If true, unlocking a later tier also unlocks every earlier tier for that player. false = each tier must be earned on its own.");
		NeverTeleport = Admin("0.2 - Item Rules", "NeverTeleport", "", "Items that can NEVER be teleported, even after the final tier. Prefab names, comma-separated. Example: DragonEgg");
		HildirChestsByTier = Admin("0.2 - Item Rules", "HildirChestsByTier", value: false, "If true, Hildir's chests unlock with the tier of the biome they're found in (Brass = Black Forest, Silver = Mountains, Bronze = Plains). false = they never teleport, as in vanilla.");
		GateListedVanillaItems = Admin("0.2 - Item Rules", "GateListedVanillaItems", value: false, "If true, items listed in a tier are gated even if vanilla normally lets them through portals. false = tiers only UNLOCK items vanilla already blocks.");
		FinalTierIncludesCheatTier = Admin("0.2 - Item Rules", "FinalTierIncludesCheatTier", value: false, "Vanilla blocks tool-tier 1000+ items at every portal. The only such item is FrozenKingDrop. If true, the final tier also unlocks those.");
		LogUnmappedItems = Admin("0.2 - Item Rules", "LogUnmappedItems", value: true, "At startup, log every teleport-blocked item not listed in any tier. Unlisted blocked items stay locked until the final tier.");
		ValidateItemIds = Admin("0.2 - Item Rules", "ValidateItemIds", value: true, "At startup, warn about any item or boss-drop ID in this file that doesn't exist in the game.");
		ShowUnlockHint = Admin("0.3 - Messages", "ShowUnlockHint", value: true, "Show a hint when a blocked item stops teleporting. Never names a boss.");
		HintFormat = Admin("0.3 - Messages", "HintFormat", "An item blocks the portal. {hint}", "Message text. {hint} = the tier's Hint. Optional {item} names the blocked item. If several tiers are locked, only the earliest locked tier's hint is shown.");
		HintPositionSetting = Client("0.3 - Messages", "HintPosition", HintPosition.Centered, "Where hints appear. Client setting: each player can choose.");
		HintCooldownSeconds = Client("0.3 - Messages", "HintCooldownSeconds", 5f, "Minimum seconds between repeat hints while standing in a portal. Client setting.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f));
		AccurateTooltips = Admin("0.3 - Messages", "AccurateTooltips", value: true, "Item tooltips show whether the item can be teleported RIGHT NOW for this player, using the same check the portal uses.");
		AnnounceTierUnlock = Admin("0.3 - Messages", "AnnounceTierUnlock", value: true, "When a boss's world key is first set, announce to everyone that portals accept more goods. Uses the tier's UnlockMessage; never names a boss.");
		AdminBypass = Admin("0.4 - Admin", "AdminBypass", value: false, "Server admins ignore the gate (useful for building/testing).");
		EnableStatusCommand = Admin("0.4 - Admin", "EnableStatusCommand", value: true, "Admin console command 'portalgate status': lists each tier as locked/unlocked for the world and for the calling player. Admin-only; shows boss names.");
		RespectVanillaAllowAll = Admin("0.5 - Compatibility", "RespectVanillaAllowAll", value: true, "Never block a portal that already lets everything through in vanilla (the Stone Portal), and respect the 'Portals' world modifier when it allows all items.");
		GateAttachedCartCargo = Admin("0.5 - Compatibility", "GateAttachedCartCargo", value: true, "If a cart-teleport mod carries an attached cart through, gate its cargo with the same check.");
		WarnOnConflictingMods = Admin("0.5 - Compatibility", "WarnOnConflictingMods", value: true, "Log a warning at startup if another mod that changes item teleport rules is installed.");
		LogPortalModDetection = Admin("0.6 - XPortal Compatibility", "LogPortalModDetection", value: true, "Log the detected XPortal / XPortalNetworks version at startup, and warn if AnyPortal is present.");
		for (int i = 0; i < TierDefaults.Length; i++)
		{
			string[] array = TierDefaults[i];
			string section = $"T{i} - {array[0]}";
			bool flag = i == TierDefaults.Length - 1;
			order = 0;
			tierEntries.Add(new TierEntries
			{
				Name = array[0],
				GlobalKey = Admin(section, "GlobalKey", array[1], "World kill flag for this tier's boss."),
				BossItem = Admin(section, "BossItem", array[2], "Boss drop (prefab name) the player must have held."),
				AllowEverything = (flag ? Admin(section, "AllowEverything", value: true, "Final tier: unlocks every teleport-blocked item, including unlisted ones (except NeverTeleport, and cheat-tier unless FinalTierIncludesCheatTier).") : null),
				Items = Admin(section, "Items", array[3], "Item prefab names this tier unlocks, comma-separated."),
				Hint = Admin(section, "Hint", array[4], flag ? "Hint while locked. Also used for any unlisted blocked item. Never name a boss." : "Hint while locked. Never name a boss."),
				UnlockMessage = Admin(section, "UnlockMessage", array[5], "Server-wide message when this tier's world key is first set. Never name a boss.")
			});
		}
	}

	public static void Parse()
	{
		List<Tier> list = new List<Tier>();
		for (int i = 0; i < Settings.tierEntries.Count; i++)
		{
			TierEntries tierEntries = Settings.tierEntries[i];
			list.Add(new Tier
			{
				Index = i,
				Name = tierEntries.Name,
				GlobalKey = tierEntries.GlobalKey.Value.Trim().ToLowerInvariant(),
				BossItem = tierEntries.BossItem.Value.Trim(),
				Items = SplitList(tierEntries.Items.Value),
				Hint = tierEntries.Hint.Value.Trim(),
				UnlockMessage = tierEntries.UnlockMessage.Value.Trim(),
				AllowEverything = (tierEntries.AllowEverything != null && tierEntries.AllowEverything.Value)
			});
		}
		NeverTeleportItems = SplitList(NeverTeleport.Value);
		foreach (KeyValuePair<string, int> hildirChestTier in HildirChestTiers)
		{
			if (HildirChestsByTier.Value)
			{
				list[hildirChestTier.Value].Items.Add(hildirChestTier.Key);
			}
			else
			{
				NeverTeleportItems.Add(hildirChestTier.Key);
			}
		}
		Tiers = list;
		if (!((Object)(object)ObjectDB.instance != (Object)null))
		{
			return;
		}
		foreach (Tier item in list)
		{
			item.BossItemToken = GetItemToken(item.BossItem);
		}
	}

	public static void LogSummary()
	{
		if (!RequireWorldKey.Value && !RequirePlayerBossItem.Value)
		{
			Logger.LogWarning((object)"RequireWorldKey and RequirePlayerBossItem are both false. Ignoring that: the boss's world key is still required for each tier.");
		}
		List<string> list = new List<string>
		{
			$"Settings: Enabled={Enabled.Value}, WorldKey={EffectiveRequireWorldKey}, " + $"PlayerBossItem={RequirePlayerBossItem.Value}, Cumulative={CumulativeTiers.Value}, " + $"GateListedVanillaItems={GateListedVanillaItems.Value}, AdminBypass={AdminBypass.Value}",
			"NeverTeleport: " + Describe(NeverTeleportItems),
			"Tiers:"
		};
		foreach (Tier tier in Tiers)
		{
			string text = ((tier.BossItemToken != null) ? (" (" + tier.BossItemToken + ")") : "");
			string text2 = (tier.AllowEverything ? ("EVERYTHING" + ((tier.Items.Count > 0) ? (" + " + Describe(tier.Items)) : "")) : Describe(tier.Items));
			list.Add($"  T{tier.Index} {tier.Name,-12} key={tier.GlobalKey}  bossItem={tier.BossItem}{text}  items={text2}");
		}
		Logger.LogInfo((object)string.Join("\n", list));
	}

	public static void Validate()
	{
		if ((Object)(object)ObjectDB.instance == (Object)null)
		{
			return;
		}
		foreach (Tier tier in Tiers)
		{
			tier.BossItemToken = GetItemToken(tier.BossItem);
		}
		if (!ValidateItemIds.Value)
		{
			return;
		}
		int problems = 0;
		foreach (string neverTeleportItem in NeverTeleportItems)
		{
			Check(neverTeleportItem, "NeverTeleport");
		}
		Dictionary<string, int> dictionary = new Dictionary<string, int>();
		foreach (Tier tier2 in Tiers)
		{
			string text = $"[T{tier2.Index} - {tier2.Name}]";
			if (string.IsNullOrEmpty(tier2.GlobalKey))
			{
				problems++;
				Logger.LogWarning((object)("ValidateItemIds: " + text + " has no GlobalKey."));
			}
			if (string.IsNullOrEmpty(tier2.BossItem))
			{
				if (RequirePlayerBossItem.Value)
				{
					problems++;
					Logger.LogWarning((object)("ValidateItemIds: " + text + " has no BossItem."));
				}
			}
			else
			{
				Check(tier2.BossItem, text + " BossItem");
			}
			foreach (string item in tier2.Items)
			{
				Check(item, text + " Items");
				if (dictionary.TryGetValue(item, out var value))
				{
					problems++;
					Logger.LogWarning((object)$"ValidateItemIds: '{item}' is listed in both T{value} and T{tier2.Index}.");
				}
				else
				{
					dictionary[item] = tier2.Index;
				}
			}
		}
		if (problems == 0)
		{
			Logger.LogInfo((object)"ValidateItemIds: all item and boss-drop IDs exist.");
		}
		void Check(string id, string where)
		{
			if (!((Object)(object)ObjectDB.instance.GetItemPrefab(id) != (Object)null))
			{
				problems++;
				Logger.LogWarning((object)("ValidateItemIds: '" + id + "' in " + where + " is not an item in this version of Valheim."));
			}
		}
	}

	private static string GetItemToken(string prefab)
	{
		if (string.IsNullOrEmpty(prefab))
		{
			return null;
		}
		GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(prefab);
		if (!Object.op_Implicit((Object)(object)itemPrefab))
		{
			return null;
		}
		ItemDrop component = itemPrefab.GetComponent<ItemDrop>();
		if (!Object.op_Implicit((Object)(object)component))
		{
			return null;
		}
		return component.m_itemData.m_shared.m_name;
	}

	private static HashSet<string> SplitList(string value)
	{
		return new HashSet<string>(from s in value.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries)
			select s.Trim() into s
			where s.Length > 0
			select s);
	}

	private static string Describe(HashSet<string> set)
	{
		if (set.Count != 0)
		{
			return string.Join(", ", set);
		}
		return "(none)";
	}

	private static ConfigEntry<T> Admin<T>(string section, string key, T value, string description, AcceptableValueBase range = null)
	{
		return Bind(section, key, value, description, range, adminOnly: true);
	}

	private static ConfigEntry<T> Client<T>(string section, string key, T value, string description, AcceptableValueBase range = null)
	{
		return Bind(section, key, value, description, range, adminOnly: false);
	}

	private static ConfigEntry<T> Bind<T>(string section, string key, T value, string description, AcceptableValueBase range, bool adminOnly)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_000d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0026: Expected O, but got Unknown
		//IL_004c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Expected O, but got Unknown
		ConfigurationManagerAttributes val = new ConfigurationManagerAttributes
		{
			IsAdminOnly = adminOnly,
			Order = (order -= 1)
		};
		if (!adminOnly)
		{
			description += " (Not synced.)";
		}
		return config.Bind<T>(section, key, value, new ConfigDescription(description, range, new object[1] { val }));
	}
}
internal class StatusCommand : ConsoleCommand
{
	public override string Name => "portalgate";

	public override string Help => "portalgate status - BossGatedPortals tiers for the world and for you (admin only)";

	public override List<string> CommandOptionList()
	{
		return new List<string> { "status" };
	}

	public override void Run(string[] args, Terminal context)
	{
		if (args.Length == 0 || args[0].ToLowerInvariant() != "status")
		{
			context.AddString("Usage: portalgate status");
			return;
		}
		if (!Settings.EnableStatusCommand.Value)
		{
			context.AddString("portalgate status is turned off (EnableStatusCommand).");
			return;
		}
		if (!SynchronizationManager.Instance.PlayerIsAdmin)
		{
			context.AddString("portalgate status is for server admins only.");
			return;
		}
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null || (Object)(object)ZoneSystem.instance == (Object)null)
		{
			context.AddString("Join a world first.");
			return;
		}
		context.AddString(string.Format("BossGatedPortals {0} - Enabled={1}, ", "0.1.4", Settings.Enabled.Value) + $"WorldKey={Settings.EffectiveRequireWorldKey}, PlayerBossItem={Settings.RequirePlayerBossItem.Value}, " + $"Cumulative={Settings.CumulativeTiers.Value}, AdminBypass={Settings.AdminBypass.Value}");
		foreach (Tier tier in Settings.Tiers)
		{
			bool flag = !string.IsNullOrEmpty(tier.GlobalKey) && ZoneSystem.instance.GetGlobalKey(tier.GlobalKey);
			bool flag2 = !string.IsNullOrEmpty(tier.BossItemToken) && localPlayer.IsMaterialKnown(tier.BossItemToken);
			bool flag3 = Gate.IsTierUnlocked(localPlayer, tier);
			context.AddString(string.Format("T{0} {1}: world {2} ({3}), ", tier.Index, tier.Name, flag ? "UNLOCKED" : "locked", tier.GlobalKey) + "you " + (flag2 ? "have" : "have not") + " held " + tier.BossItem + " -> " + (flag3 ? "UNLOCKED" : "LOCKED") + " for you");
		}
	}
}
internal class Tier
{
	public int Index;

	public string Name;

	public string GlobalKey;

	public string BossItem;

	public string BossItemToken;

	public HashSet<string> Items = new HashSet<string>();

	public string Hint;

	public string UnlockMessage;

	public bool AllowEverything;
}
internal static class Tooltips
{
	[HarmonyPatch(typeof(ItemData), "GetTooltip", new Type[]
	{
		typeof(ItemData),
		typeof(int),
		typeof(bool),
		typeof(float),
		typeof(int),
		typeof(bool)
	})]
	private static class GetTooltipPatch
	{
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Expected O, but got Unknown
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Expected O, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Expected O, but got Unknown
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Expected O, but got Unknown
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Expected O, but got Unknown
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Expected O, but got Unknown
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Expected O, but got Unknown
			//IL_0137: Unknown result type (might be due to invalid IL or missing references)
			//IL_013d: Expected O, but got Unknown
			//IL_014b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0151: Expected O, but got Unknown
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			//IL_016a: Expected O, but got Unknown
			//IL_01c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Expected O, but got Unknown
			//IL_01d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d7: Expected O, but got Unknown
			FieldInfo fieldInfo = AccessTools.Field(typeof(ItemData), "m_shared");
			FieldInfo fieldInfo2 = AccessTools.Field(typeof(SharedData), "m_teleportable");
			MethodInfo methodInfo = AccessTools.Method(typeof(ZoneSystem), "GetGlobalKey", new Type[1] { typeof(GlobalKeys) }, (Type[])null);
			CodeMatcher val = new CodeMatcher(instructions, (ILGenerator)null).MatchStartForward((CodeMatch[])(object)new CodeMatch[10]
			{
				new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldfld, (object)fieldInfo, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldfld, (object)fieldInfo2, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Brtrue || i.opcode == OpCodes.Brtrue_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Call, (object)null, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Ldc_I4 || i.opcode == OpCodes.Ldc_I4_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Callvirt, (object)methodInfo, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Brtrue || i.opcode == OpCodes.Brtrue_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldsfld, (object)null, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldstr, (object)"\n<color=orange>$item_noteleport</color>", (string)null)
			});
			if (val.IsInvalid)
			{
				Logger.LogWarning((object)"AccurateTooltips: couldn't find the teleport line in the item tooltip (game update?). Tooltips will show vanilla text.");
				return instructions;
			}
			object operand = val.InstructionAt(3).operand;
			val.Advance(1).RemoveInstructions(7).Insert((CodeInstruction[])(object)new CodeInstruction[2]
			{
				new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(Tooltips), "ShowNoTeleport", (Type[])null, (Type[])null)),
				new CodeInstruction(OpCodes.Brfalse, operand)
			});
			return val.InstructionEnumeration();
		}
	}

	[HarmonyPatch(typeof(InventoryGrid), "UpdateGui")]
	private static class InventoryGridIconPatch
	{
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Expected O, but got Unknown
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Expected O, but got Unknown
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c6: Expected O, but got Unknown
			//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00da: Expected O, but got Unknown
			//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0102: Expected O, but got Unknown
			//IL_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_0116: Expected O, but got Unknown
			//IL_0138: Unknown result type (might be due to invalid IL or missing references)
			//IL_013e: Expected O, but got Unknown
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0152: Expected O, but got Unknown
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Expected O, but got Unknown
			//IL_0175: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Expected O, but got Unknown
			//IL_019e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a4: Expected O, but got Unknown
			//IL_01b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b9: Expected O, but got Unknown
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ce: Expected O, but got Unknown
			//IL_021c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0222: Expected O, but got Unknown
			FieldInfo fieldInfo = AccessTools.Field(typeof(InventoryElement), "m_noteleport");
			FieldInfo fieldInfo2 = AccessTools.Field(typeof(ItemData), "m_shared");
			FieldInfo fieldInfo3 = AccessTools.Field(typeof(SharedData), "m_teleportable");
			MethodInfo methodInfo = AccessTools.Method(typeof(ZoneSystem), "GetGlobalKey", new Type[1] { typeof(GlobalKeys) }, (Type[])null);
			CodeMatcher val = new CodeMatcher(instructions, (ILGenerator)null).MatchStartForward((CodeMatch[])(object)new CodeMatch[13]
			{
				new CodeMatch((OpCode?)OpCodes.Ldfld, (object)fieldInfo, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => CodeInstructionExtensions.IsLdloc(i, (LocalBuilder)null)), (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldfld, (object)fieldInfo2, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldfld, (object)fieldInfo3, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Brtrue || i.opcode == OpCodes.Brtrue_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Call, (object)null, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Ldc_I4 || i.opcode == OpCodes.Ldc_I4_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Callvirt, (object)methodInfo, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldc_I4_0, (object)null, (string)null),
				new CodeMatch((OpCode?)OpCodes.Ceq, (object)null, (string)null),
				new CodeMatch((Func<CodeInstruction, bool>)((CodeInstruction i) => i.opcode == OpCodes.Br || i.opcode == OpCodes.Br_S), (string)null),
				new CodeMatch((OpCode?)OpCodes.Ldc_I4_0, (object)null, (string)null),
				new CodeMatch((OpCode?)OpCodes.Callvirt, (object)null, (string)null)
			});
			if (val.IsInvalid)
			{
				Logger.LogWarning((object)"AccurateTooltips: couldn't find the no-teleport icon in the inventory grid (game update?). Slot icons will show vanilla state.");
				return instructions;
			}
			val.Advance(2).RemoveInstructions(10).Insert((CodeInstruction[])(object)new CodeInstruction[1]
			{
				new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(Tooltips), "ShowNoTeleport", (Type[])null, (Type[])null))
			});
			return val.InstructionEnumeration();
		}
	}

	public static bool ShowNoTeleport(ItemData item)
	{
		try
		{
			return Decide(item);
		}
		catch (Exception e)
		{
			Failsafe.Report("Teleport tooltip/icon", e);
			try
			{
				return Vanilla(item);
			}
			catch
			{
				return false;
			}
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static bool Decide(ItemData item)
	{
		Player localPlayer = Player.m_localPlayer;
		if (!Settings.Enabled.Value || !Settings.AccurateTooltips.Value || (Object)(object)localPlayer == (Object)null)
		{
			return Vanilla(item);
		}
		return !Gate.CanTeleport(localPlayer, item);
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static bool Vanilla(ItemData item)
	{
		if (!item.m_shared.m_teleportable)
		{
			return !Failsafe.TeleportAllSet();
		}
		return false;
	}
}