Decompiled source of Tollbound v0.10.1

plugins/Tollbound.dll

Decompiled 11 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Jotunn.Configs;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using Tollbound.Config;
using Tollbound.Content;
using Tollbound.Diagnostics;
using Tollbound.Gameplay;
using Tollbound.Gameplay.Backpacks;
using Tollbound.Gameplay.Portals;
using Tollbound.Messages;
using Tollbound.Model;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: IgnoresAccessChecksTo("assembly_guiutils")]
[assembly: IgnoresAccessChecksTo("assembly_utils")]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: AssemblyCompany("RAGEmedia")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("A Valheim mod.")]
[assembly: AssemblyFileVersion("0.10.1.0")]
[assembly: AssemblyInformationalVersion("0.10.1+dbe46162b10bf2699600f4e9ee73a205972b9f16")]
[assembly: AssemblyProduct("Tollbound")]
[assembly: AssemblyTitle("Tollbound")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.10.1.0")]
[module: UnverifiableCode]
[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 Tollbound
{
	[BepInPlugin("com.ragemedia.tollbound", "Tollbound", "0.10.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public class TollboundPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "com.ragemedia.tollbound";

		public const string PluginName = "Tollbound";

		public const string PluginVersion = "0.10.1";

		internal static TollboundPlugin Instance;

		internal readonly Harmony Harmony = new Harmony("com.ragemedia.tollbound");

		private void Awake()
		{
			Instance = this;
			BindConfig();
			VoiceBook.Load();
			Harmony.PatchAll(Assembly.GetExecutingAssembly());
			VerifyPatches();
			PrefabManager.OnVanillaPrefabsAvailable += OnVanillaPrefabsAvailable;
			ItemManager.OnItemsRegistered += OnItemsRegistered;
		}

		private void OnDestroy()
		{
			PrefabManager.OnVanillaPrefabsAvailable -= OnVanillaPrefabsAvailable;
			ItemManager.OnItemsRegistered -= OnItemsRegistered;
			Harmony harmony = Harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}

		private static void OnVanillaPrefabsAvailable()
		{
			PortalPieces.Register();
		}

		private static void OnItemsRegistered()
		{
			CargoRegistry.Build();
			TollboundConfig.ResolveTollItems();
			BackpackBridge.Bind();
			TargetPortalBridge.Bind(Instance.Harmony);
			if (TollboundConfig.WriteItemReport.Value)
			{
				ItemReport.Run();
			}
		}

		private void BindConfig()
		{
			TollboundConfig.Bind(((BaseUnityPlugin)this).Config);
		}

		private void VerifyPatches()
		{
			List<MethodBase> list = Harmony.GetPatchedMethods().ToList();
			LogInfo($"Harmony bound {list.Count} target(s):");
			foreach (MethodBase item in list)
			{
				LogInfo("  bound  " + item.DeclaringType?.FullName + "." + item.Name);
			}
			int num = 0;
			foreach (Type item2 in from t in Assembly.GetExecutingAssembly().GetTypes()
				where t.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Any()
				select t)
			{
				List<HarmonyMethod> list2 = (from HarmonyPatch a in item2.GetCustomAttributes(typeof(HarmonyPatch), inherit: true)
					select ((HarmonyAttribute)a).info).ToList();
				HarmonyMethod val;
				try
				{
					val = HarmonyMethod.Merge(list2);
				}
				catch (Exception ex)
				{
					LogError("  UNRESOLVED  " + item2.Name + ": could not merge patch attributes: " + ex.Message);
					num++;
					continue;
				}
				if (!(val?.declaringType == null) && !string.IsNullOrEmpty(val.methodName) && AccessTools.Method(val.declaringType, val.methodName, val.argumentTypes, (Type[])null) == null)
				{
					num++;
					LogError("  UNRESOLVED  " + item2.Name + " -> " + val.declaringType.Name + "." + val.methodName + " does not exist. This patch will never run. The game version likely changed; re-check the method against the decompiled source.");
				}
			}
			if (num > 0)
			{
				LogError(string.Format("{0}: {1} patch target(s) failed to resolve. ", "Tollbound", num) + "This mod is loaded but not fully functional.");
			}
			else if (list.Count == 0)
			{
				LogWarning("Tollbound: loaded, but bound zero patches. Expected if this mod only adds content via Jotunn.");
			}
			else
			{
				LogInfo("Tollbound v0.10.1 ready.");
			}
		}

		internal static void LogInfo(string msg)
		{
			((BaseUnityPlugin)Instance).Logger.LogInfo((object)("[Tollbound] " + msg));
		}

		internal static void LogWarning(string msg)
		{
			((BaseUnityPlugin)Instance).Logger.LogWarning((object)("[Tollbound] " + msg));
		}

		internal static void LogError(string msg)
		{
			((BaseUnityPlugin)Instance).Logger.LogError((object)("[Tollbound] " + msg));
		}

		internal static void LogVerbose(string msg)
		{
			if (TollboundConfig.VerboseLogging != null && TollboundConfig.VerboseLogging.Value)
			{
				((BaseUnityPlugin)Instance).Logger.LogInfo((object)("[Tollbound] " + msg));
			}
		}
	}
}
namespace Tollbound.Patches
{
	[HarmonyPatch(typeof(TeleportWorld), "GetHoverText")]
	internal static class HoverTextPatch
	{
		private const string Warn = "#E0A34E";

		private const string Bad = "#E06C5C";

		private const string Faint = "#9AA7A9";

		private static void Postfix(TeleportWorld __instance, ref string __result)
		{
			BiomeTier biomeTier = PortalIdentity.TierOf(__instance);
			if (biomeTier == BiomeTier.None)
			{
				return;
			}
			List<string> list = new List<string>();
			bool flag = __instance.HaveTarget();
			BiomeTier far = (flag ? PortalIdentity.TierOfFarEnd(__instance) : BiomeTier.None);
			list.Add(RouteLine(biomeTier, far, flag));
			if (flag)
			{
				string text = CargoLine(PortalIdentity.Ceiling(biomeTier, far));
				if (text != null)
				{
					list.Add(text);
				}
			}
			StringBuilder stringBuilder = new StringBuilder(__result);
			foreach (string item in list)
			{
				stringBuilder.Append("\n").Append(item);
			}
			__result = stringBuilder.ToString();
		}

		private static string RouteLine(BiomeTier near, BiomeTier far, bool connected)
		{
			string text = NameOf(near);
			if (!connected)
			{
				return Dim("Carries " + text + " cargo and below, once linked.");
			}
			if (far == near)
			{
				return text + " crossing.";
			}
			string text2 = ((far == BiomeTier.None) ? "a plain portal" : NameOf(far));
			BiomeTier biomeTier = PortalIdentity.Ceiling(near, far);
			string text3 = ((biomeTier == BiomeTier.None) ? "Carries nothing restricted." : ("Carries " + NameOf(biomeTier) + " cargo and below."));
			return text + " to " + text2 + ". " + text3;
		}

		private static string CargoLine(BiomeTier ceiling)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return null;
			}
			Manifest manifest = Manifest.Cached(localPlayer);
			if (manifest.IsEmpty)
			{
				return null;
			}
			if (manifest.Unrecognized.Count > 0)
			{
				return Colour("#E06C5C", PortalIdentity.DisplayName(manifest.Unrecognized[0]) + " cannot cross here.");
			}
			CargoLot cargoLot = (from l in manifest.Lots
				where l.Tier > ceiling
				orderby l.Tier descending
				select l).FirstOrDefault();
			if (cargoLot != null)
			{
				return Colour("#E06C5C", "Refused: " + PortalIdentity.DisplayName(cargoLot.Prefab) + " (" + NameOf(cargoLot.Tier) + " cargo).");
			}
			List<string> list = new List<string>();
			string text = TollSummary(manifest, localPlayer);
			if (text != null)
			{
				list.Add(text);
			}
			string text2 = RiskSummary(manifest);
			if (text2 != null)
			{
				list.Add(text2);
			}
			if (list.Count != 0)
			{
				return string.Join("  ", list.ToArray());
			}
			return null;
		}

		private static string TollSummary(Manifest manifest, Player player)
		{
			List<string> list = new List<string>();
			bool flag = false;
			foreach (BiomeTier item in manifest.TiersPresent)
			{
				TollDue tollDue = TollGate.Price(item, manifest.UnitsOf(item));
				if (tollDue != null)
				{
					if (Cargo.Count(player, tollDue.ItemPrefab) < tollDue.Amount)
					{
						flag = true;
					}
					string text = $"{PortalIdentity.DisplayName(tollDue.ItemPrefab)} x{tollDue.Amount}";
					list.Add((tollDue.Loads > 1) ? (text + $" ({tollDue.Loads} loads)") : text);
				}
			}
			if (list.Count == 0)
			{
				return null;
			}
			string text2 = "Toll: " + string.Join(", ", list.ToArray());
			if (!flag)
			{
				return text2;
			}
			return Colour("#E06C5C", text2 + " (short)");
		}

		private static string RiskSummary(Manifest manifest)
		{
			List<string> list = new List<string>();
			foreach (BiomeTier item in manifest.TiersPresent)
			{
				if (TollGate.BossIsDead(item))
				{
					continue;
				}
				float num = TollboundConfig.LossRate(item);
				if (!(num <= 0f))
				{
					int num2 = (from l in manifest.Of(item)
						where l.Destructible
						select l).Sum((CargoLot l) => l.Count);
					if (num2 > 0)
					{
						float num3 = (float)num2 * num;
						string text = ((num3 < 0.5f) ? "under 1" : $"~{Mathf.RoundToInt(num3)}");
						list.Add($"{num2} at {num * 100f:0.#}% to {SpiritOf(item)} ({text})");
					}
				}
			}
			if (list.Count != 0)
			{
				return Colour("#E0A34E", "At risk: " + string.Join(", ", list.ToArray()));
			}
			return null;
		}

		private static string NameOf(BiomeTier tier)
		{
			TierInfo tierInfo = Tiers.Get(tier);
			if (tierInfo != null)
			{
				return tierInfo.BiomeName;
			}
			return "nothing";
		}

		private static string SpiritOf(BiomeTier tier)
		{
			TierInfo tierInfo = Tiers.Get(tier);
			if (tierInfo != null)
			{
				return tierInfo.SpiritName;
			}
			return "the spirits";
		}

		private static string Colour(string hex, string text)
		{
			return "<color=" + hex + ">" + text + "</color>";
		}

		private static string Dim(string text)
		{
			return Colour("#9AA7A9", text);
		}
	}
	[HarmonyPatch(typeof(InventoryGrid), "UpdateGui")]
	internal static class InventoryIconPatch
	{
		private static void Postfix(InventoryGrid __instance)
		{
			if (!NearbyPortal.InRange || (Object)(object)__instance == (Object)null || __instance.m_inventory == null)
			{
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || __instance.m_inventory != ((Humanoid)localPlayer).GetInventory())
			{
				return;
			}
			BiomeTier ceiling = NearbyPortal.CeilingHere();
			int width = __instance.m_inventory.GetWidth();
			foreach (ItemData allItem in __instance.m_inventory.GetAllItems())
			{
				if (allItem?.m_shared != null && !allItem.m_shared.m_teleportable)
				{
					Element val = ElementFor(__instance, allItem, width);
					if (!((Object)(object)val?.m_noteleport == (Object)null))
					{
						((Behaviour)val.m_noteleport).enabled = IsBlocked(allItem, ceiling);
					}
				}
			}
		}

		private static bool IsBlocked(ItemData item, BiomeTier ceiling)
		{
			BiomeTier biomeTier = CargoRegistry.TierOfItem(PortalIdentity.PrefabNameOf(item));
			if (biomeTier != BiomeTier.None)
			{
				return biomeTier > ceiling;
			}
			return true;
		}

		private static Element ElementFor(InventoryGrid grid, ItemData item, int width)
		{
			int num = item.m_gridPos.y * width + item.m_gridPos.x;
			if (num < 0 || num >= grid.m_elements.Count)
			{
				return null;
			}
			return grid.m_elements[num];
		}
	}
	[HarmonyPatch(typeof(Player), "TeleportTo")]
	internal static class MapTeleportPatch
	{
		private static bool Prefix(Player __instance, Vector3 pos, bool distantTeleport, ref bool __result)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			if (!TargetPortalBridge.MapCrossingInProgress || !distantTeleport)
			{
				return true;
			}
			BiomeTier biomeTier = PortalIdentity.TierOf(NearbyPortal.Current);
			if (biomeTier == BiomeTier.None)
			{
				return true;
			}
			BiomeTier biomeTier2 = PortalIdentity.TierOfPortalAt(pos);
			Verdict verdict = TollGate.Evaluate(__instance, biomeTier, biomeTier2);
			if (!verdict.Allowed)
			{
				TollboundPlugin.LogVerbose($"Refused a map crossing {biomeTier} -> {biomeTier2} (ceiling {verdict.Ceiling}): " + $"{verdict.Refusal}.");
				Voice.Refused(__instance, verdict);
				__result = false;
				return false;
			}
			List<Loss> list = TollGate.Apply(__instance, verdict);
			Voice.Crossed(__instance, verdict, list);
			TollboundPlugin.LogVerbose($"Allowed a map crossing {biomeTier} -> {biomeTier2} (ceiling {verdict.Ceiling}), " + $"{verdict.Tolls.Count} toll(s), {list.Count} loss group(s).");
			return true;
		}
	}
	[HarmonyPatch(typeof(Inventory), "IsTeleportable")]
	internal static class MapTeleportableDeferPatch
	{
		private static void Postfix(Inventory __instance, ref bool __result)
		{
			if (!__result && TargetPortalBridge.MapCrossingInProgress)
			{
				Player localPlayer = Player.m_localPlayer;
				if (!((Object)(object)localPlayer == (Object)null) && __instance == ((Humanoid)localPlayer).GetInventory() && PortalIdentity.TierOf(NearbyPortal.Current) != BiomeTier.None)
				{
					__result = true;
				}
			}
		}
	}
	[HarmonyPatch(typeof(Game), "Awake")]
	internal static class PortalRegistrationPatch
	{
		private static void Postfix(Game __instance)
		{
			int num = 0;
			TierInfo[] all = Tiers.All;
			for (int i = 0; i < all.Length; i++)
			{
				int stableHashCode = StringExtensionMethods.GetStableHashCode(all[i].PortalPrefab);
				if (!__instance.PortalPrefabHash.Contains(stableHashCode))
				{
					__instance.PortalPrefabHash.Add(stableHashCode);
					num++;
				}
			}
			TollboundPlugin.LogInfo($"Registered {num} biome portal prefab(s) as portals " + $"({__instance.PortalPrefabHash.Count} known to the game in total).");
		}
	}
	[HarmonyPatch(typeof(TeleportWorld), "Teleport")]
	internal static class TeleportGatePatch
	{
		private static bool Prefix(TeleportWorld __instance, Player player)
		{
			BiomeTier biomeTier = PortalIdentity.TierOf(__instance);
			if (biomeTier == BiomeTier.None || (Object)(object)player == (Object)null)
			{
				return true;
			}
			if (!__instance.TargetFound())
			{
				return false;
			}
			if (ZoneSystem.instance.GetGlobalKey((GlobalKeys)27))
			{
				((Character)player).Message((MessageType)2, "$msg_blocked", 0, (Sprite)null);
				return false;
			}
			float num = default(float);
			if (ZoneSystem.instance.GetGlobalKey((GlobalKeys)28) && (RandEventSystem.instance.GetBossEvent() != null || (ZoneSystem.instance.GetGlobalKey((GlobalKeys)38, ref num) && num > 0f)))
			{
				((Character)player).Message((MessageType)2, "$msg_blockedbyboss", 0, (Sprite)null);
				return false;
			}
			BiomeTier biomeTier2 = PortalIdentity.TierOfFarEnd(__instance);
			Verdict verdict = TollGate.Evaluate(player, biomeTier, biomeTier2);
			if (!verdict.Allowed)
			{
				TollboundPlugin.LogVerbose($"Refused a {biomeTier} -> {biomeTier2} crossing (ceiling {verdict.Ceiling}): {verdict.Refusal}.");
				Voice.Refused(player, verdict);
				return false;
			}
			List<Loss> list = TollGate.Apply(player, verdict);
			Voice.Crossed(player, verdict, list);
			TollboundPlugin.LogVerbose($"Allowed a {biomeTier} -> {biomeTier2} crossing (ceiling {verdict.Ceiling}), " + $"{verdict.Tolls.Count} toll(s), {list.Count} loss group(s).");
			Cross(__instance, player);
			return false;
		}

		private static void Cross(TeleportWorld portal, Player player)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			ZDOID connectionZDOID = portal.m_nview.GetZDO().GetConnectionZDOID((ConnectionType)1);
			ZDO zDO = ZDOMan.instance.GetZDO(connectionZDOID);
			if (zDO != null)
			{
				Quaternion rotation = zDO.GetRotation();
				Vector3 val = zDO.GetPosition() + rotation * Vector3.forward * portal.m_exitDistance + Vector3.up;
				((Character)player).TeleportTo(val, rotation, true);
				Game.instance.IncrementPlayerStat((PlayerStatType)15, 1f);
			}
		}
	}
	[HarmonyPatch(typeof(TeleportWorld), "UpdatePortal")]
	internal static class PortalSwirlPatch
	{
		private static void Postfix(TeleportWorld __instance)
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			BiomeTier biomeTier = PortalIdentity.TierOf(__instance);
			if (biomeTier == BiomeTier.None || (Object)(object)__instance.m_proximityRoot == (Object)null || (Object)(object)__instance.m_target_found == (Object)null || (Object)(object)__instance.m_nview == (Object)null || !__instance.m_nview.IsValid())
			{
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if (!((Object)(object)localPlayer == (Object)null))
			{
				float num = Vector3.Distance(((Component)localPlayer).transform.position, __instance.m_proximityRoot.position);
				if (!(num > __instance.m_activationRange))
				{
					NearbyPortal.Report(__instance, num);
					BiomeTier far = PortalIdentity.TierOfFarEnd(__instance);
					Verdict verdict = TollGate.Evaluate(localPlayer, biomeTier, far);
					__instance.m_target_found.SetActive(verdict.Allowed && __instance.TargetFound());
				}
			}
		}
	}
}
namespace Tollbound.Model
{
	internal static class CargoRegistry
	{
		private static readonly Dictionary<string, BiomeTier> TierOf = new Dictionary<string, BiomeTier>(StringComparer.OrdinalIgnoreCase);

		private static readonly HashSet<string> Destructible = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		internal static bool IsBuilt { get; private set; }

		internal static IEnumerable<string> AllCargo => TierOf.Keys;

		internal static void Build()
		{
			TierOf.Clear();
			Destructible.Clear();
			TierInfo[] all = Tiers.All;
			foreach (TierInfo tierInfo in all)
			{
				string[] cargo = tierInfo.Cargo;
				foreach (string key in cargo)
				{
					TierOf[key] = tierInfo.Tier;
				}
				cargo = tierInfo.LossEligible;
				foreach (string item in cargo)
				{
					Destructible.Add(item);
				}
			}
			IsBuilt = true;
			WarnAboutUnclassifiedItems();
			TollboundPlugin.LogInfo($"Cargo registry: {TierOf.Count} restricted items across " + $"{Tiers.All.Length} tiers, {Destructible.Count} of them loss-eligible.");
		}

		internal static BiomeTier TierOfItem(string prefabName)
		{
			if (prefabName == null || !TierOf.TryGetValue(prefabName, out var value))
			{
				return BiomeTier.None;
			}
			return value;
		}

		internal static bool CanBeLost(string prefabName)
		{
			if (prefabName != null)
			{
				return Destructible.Contains(prefabName);
			}
			return false;
		}

		private static void WarnAboutUnclassifiedItems()
		{
			if ((Object)(object)ObjectDB.instance == (Object)null)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (GameObject item in ObjectDB.instance.m_items)
			{
				if (!((Object)(object)item == (Object)null))
				{
					ItemDrop component = item.GetComponent<ItemDrop>();
					if (component?.m_itemData?.m_shared != null && !component.m_itemData.m_shared.m_teleportable && !TierOf.ContainsKey(((Object)item).name))
					{
						list.Add(((Object)item).name);
					}
				}
			}
			if (list.Count != 0)
			{
				TollboundPlugin.LogWarning($"{list.Count} non-teleportable item(s) have no Tollbound tier and " + "will pass through biome portals untolled: " + string.Join(", ", list.OrderBy((string n) => n).ToArray()));
			}
		}
	}
	internal static class PortalIdentity
	{
		internal static BiomeTier TierOf(TeleportWorld portal)
		{
			if (!((Object)(object)portal == (Object)null))
			{
				return Tiers.TierOfPortal(((Object)((Component)portal).gameObject).name);
			}
			return BiomeTier.None;
		}

		internal static BiomeTier TierOfFarEnd(TeleportWorld portal)
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			ZDO val = (((Object)(object)portal == (Object)null || (Object)(object)portal.m_nview == (Object)null) ? null : portal.m_nview.GetZDO());
			if (val == null)
			{
				return BiomeTier.None;
			}
			ZDOID connectionZDOID = val.GetConnectionZDOID((ConnectionType)1);
			if (connectionZDOID == ZDOID.None)
			{
				return BiomeTier.None;
			}
			ZDO val2 = ((ZDOMan.instance == null) ? null : ZDOMan.instance.GetZDO(connectionZDOID));
			if (val2 == null || (Object)(object)ZNetScene.instance == (Object)null)
			{
				return BiomeTier.None;
			}
			GameObject prefab = ZNetScene.instance.GetPrefab(val2.GetPrefab());
			if (!((Object)(object)prefab == (Object)null))
			{
				return Tiers.TierOfPortal(((Object)prefab).name);
			}
			return BiomeTier.None;
		}

		internal static BiomeTier TierOfPortalAt(Vector3 position)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			if (ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null)
			{
				return BiomeTier.None;
			}
			ZDO val = null;
			float num = 25f;
			foreach (ZDO portal in ZDOMan.instance.GetPortals())
			{
				Vector3 val2 = portal.GetPosition() - position;
				float sqrMagnitude = ((Vector3)(ref val2)).sqrMagnitude;
				if (!(sqrMagnitude >= num))
				{
					val = portal;
					num = sqrMagnitude;
				}
			}
			if (val == null)
			{
				return BiomeTier.None;
			}
			GameObject prefab = ZNetScene.instance.GetPrefab(val.GetPrefab());
			if (!((Object)(object)prefab == (Object)null))
			{
				return Tiers.TierOfPortal(((Object)prefab).name);
			}
			return BiomeTier.None;
		}

		internal static BiomeTier Ceiling(BiomeTier near, BiomeTier far)
		{
			if (near >= far)
			{
				return far;
			}
			return near;
		}

		internal static string PrefabNameOf(ItemData item)
		{
			if (item == null)
			{
				return null;
			}
			if ((Object)(object)item.m_dropPrefab != (Object)null)
			{
				return ((Object)item.m_dropPrefab).name;
			}
			GameObject val = default(GameObject);
			if (item.m_shared != null && (Object)(object)ObjectDB.instance != (Object)null && ObjectDB.instance.TryGetItemPrefab(item.m_shared, ref val) && (Object)(object)val != (Object)null)
			{
				return ((Object)val).name;
			}
			return null;
		}

		internal static string DisplayName(string prefabName)
		{
			GameObject val = default(GameObject);
			if ((Object)(object)ObjectDB.instance != (Object)null && ObjectDB.instance.TryGetItemPrefab(prefabName, ref val) && (Object)(object)val != (Object)null)
			{
				ItemDrop component = val.GetComponent<ItemDrop>();
				if ((Object)(object)component != (Object)null && component.m_itemData?.m_shared != null)
				{
					if (Localization.instance == null)
					{
						return component.m_itemData.m_shared.m_name;
					}
					return Localization.instance.Localize(component.m_itemData.m_shared.m_name);
				}
			}
			return prefabName;
		}
	}
	internal enum BiomeTier
	{
		None,
		BlackForest,
		Swamp,
		Mountain,
		Plains,
		Mistlands,
		Ashlands
	}
	internal sealed class Ingredient
	{
		internal readonly string Item;

		internal readonly int Amount;

		internal Ingredient(string item, int amount)
		{
			Item = item;
			Amount = amount;
		}
	}
	internal sealed class TierInfo
	{
		internal BiomeTier Tier;

		internal string BiomeName;

		internal string PortalPrefab;

		internal string NameToken;

		internal string DisplayName;

		internal string Description;

		internal string SpiritName;

		internal string BossKey;

		internal Color Hue;

		internal string DefaultTollItem;

		internal int DefaultTollAmount;

		internal float DefaultLossRate;

		internal string CraftingStation;

		internal Ingredient[] Recipe;

		internal string[] Cargo;

		internal string[] LossEligible;
	}
	internal static class Tiers
	{
		internal static readonly TierInfo[] All = new TierInfo[6]
		{
			new TierInfo
			{
				Tier = BiomeTier.BlackForest,
				BiomeName = "Black Forest",
				PortalPrefab = "portal_tollbound_blackforest",
				NameToken = "$piece_tollbound_blackforest",
				DisplayName = "Portal - Black Forest",
				Description = "Carries copper and tin. The Elder counts what leaves his woods.",
				SpiritName = "the Elder",
				BossKey = "defeated_gdking",
				Hue = new Color(0.25f, 1f, 0.3f),
				DefaultTollItem = "GreydwarfEye",
				DefaultTollAmount = 5,
				DefaultLossRate = 0.08f,
				CraftingStation = CraftingStations.Workbench,
				Recipe = new Ingredient[4]
				{
					new Ingredient("FineWood", 20),
					new Ingredient("GreydwarfEye", 10),
					new Ingredient("SurtlingCore", 2),
					new Ingredient("Copper", 5)
				},
				Cargo = new string[7] { "CopperOre", "TinOre", "Copper", "Tin", "Bronze", "BronzeScrap", "CopperScrap" },
				LossEligible = new string[7] { "CopperOre", "TinOre", "Copper", "Tin", "Bronze", "BronzeScrap", "CopperScrap" }
			},
			new TierInfo
			{
				Tier = BiomeTier.Swamp,
				BiomeName = "Swamp",
				PortalPrefab = "portal_tollbound_swamp",
				NameToken = "$piece_tollbound_swamp",
				DisplayName = "Portal - Swamp",
				Description = "Carries iron. Bonemass keeps what the mire is owed.",
				SpiritName = "Bonemass",
				BossKey = "defeated_bonemass",
				Hue = new Color(0.75f, 0.95f, 0.2f),
				DefaultTollItem = "Entrails",
				DefaultTollAmount = 6,
				DefaultLossRate = 0.11f,
				CraftingStation = CraftingStations.Workbench,
				Recipe = new Ingredient[4]
				{
					new Ingredient("FineWood", 20),
					new Ingredient("ElderBark", 10),
					new Ingredient("SurtlingCore", 3),
					new Ingredient("Iron", 5)
				},
				Cargo = new string[3] { "IronOre", "IronScrap", "Iron" },
				LossEligible = new string[3] { "IronOre", "IronScrap", "Iron" }
			},
			new TierInfo
			{
				Tier = BiomeTier.Mountain,
				BiomeName = "Mountain",
				PortalPrefab = "portal_tollbound_mountain",
				NameToken = "$piece_tollbound_mountain",
				DisplayName = "Portal - Mountain",
				Description = "Carries silver and dragon eggs. Moder counts what you carry.",
				SpiritName = "Moder",
				BossKey = "defeated_dragon",
				Hue = new Color(0.45f, 0.85f, 1f),
				DefaultTollItem = "FreezeGland",
				DefaultTollAmount = 4,
				DefaultLossRate = 0.14f,
				CraftingStation = CraftingStations.Workbench,
				Recipe = new Ingredient[4]
				{
					new Ingredient("FineWood", 20),
					new Ingredient("Obsidian", 10),
					new Ingredient("SurtlingCore", 4),
					new Ingredient("Silver", 5)
				},
				Cargo = new string[3] { "SilverOre", "Silver", "DragonEgg" },
				LossEligible = new string[2] { "SilverOre", "Silver" }
			},
			new TierInfo
			{
				Tier = BiomeTier.Plains,
				BiomeName = "Plains",
				PortalPrefab = "portal_tollbound_plains",
				NameToken = "$piece_tollbound_plains",
				DisplayName = "Portal - Plains",
				Description = "Carries black metal. Yagluth's hand still closes.",
				SpiritName = "Yagluth",
				BossKey = "defeated_goblinking",
				Hue = new Color(1f, 0.72f, 0.2f),
				DefaultTollItem = "Needle",
				DefaultTollAmount = 5,
				DefaultLossRate = 0.17f,
				CraftingStation = CraftingStations.Forge,
				Recipe = new Ingredient[4]
				{
					new Ingredient("FineWood", 20),
					new Ingredient("Needle", 10),
					new Ingredient("SurtlingCore", 6),
					new Ingredient("BlackMetal", 5)
				},
				Cargo = new string[2] { "BlackMetalScrap", "BlackMetal" },
				LossEligible = new string[2] { "BlackMetalScrap", "BlackMetal" }
			},
			new TierInfo
			{
				Tier = BiomeTier.Mistlands,
				BiomeName = "Mistlands",
				PortalPrefab = "portal_tollbound_mistlands",
				NameToken = "$piece_tollbound_mistlands",
				DisplayName = "Portal - Mistlands",
				Description = "Carries extractors and Hildir's chests. Nothing crosses without her leave.",
				SpiritName = "the Queen",
				BossKey = "defeated_queen",
				Hue = new Color(0.62f, 0.35f, 1f),
				DefaultTollItem = "Bilebag",
				DefaultTollAmount = 4,
				DefaultLossRate = 0f,
				CraftingStation = CraftingStations.Forge,
				Recipe = new Ingredient[4]
				{
					new Ingredient("YggdrasilWood", 20),
					new Ingredient("BlackMarble", 10),
					new Ingredient("SurtlingCore", 8),
					new Ingredient("Eitr", 2)
				},
				Cargo = new string[4] { "DvergrNeedle", "chest_hildir1", "chest_hildir2", "chest_hildir3" },
				LossEligible = new string[0]
			},
			new TierInfo
			{
				Tier = BiomeTier.Ashlands,
				BiomeName = "Ashlands",
				PortalPrefab = "portal_tollbound_ashlands",
				NameToken = "$piece_tollbound_ashlands",
				DisplayName = "Portal - Ashlands",
				Description = "Carries flametal. The last flame takes its portion.",
				SpiritName = "Fader",
				BossKey = "defeated_fader",
				Hue = new Color(1f, 0.35f, 0.1f),
				DefaultTollItem = "CharredBone",
				DefaultTollAmount = 5,
				DefaultLossRate = 0.22f,
				CraftingStation = CraftingStations.Forge,
				Recipe = new Ingredient[4]
				{
					new Ingredient("Blackwood", 20),
					new Ingredient("Grausten", 10),
					new Ingredient("SurtlingCore", 10),
					new Ingredient("FlametalNew", 5)
				},
				Cargo = new string[7] { "FlametalOre", "FlametalOreNew", "Flametal", "FlametalNew", "CharredCogwheel", "MechanicalSpring", "Ironpit" },
				LossEligible = new string[4] { "FlametalOre", "FlametalOreNew", "Flametal", "FlametalNew" }
			}
		};

		private static readonly Dictionary<BiomeTier, TierInfo> ByTier = All.ToDictionary((TierInfo t) => t.Tier);

		private static readonly Dictionary<string, TierInfo> ByPortalPrefab = All.ToDictionary<TierInfo, string>((TierInfo t) => t.PortalPrefab, StringComparer.OrdinalIgnoreCase);

		internal static TierInfo Get(BiomeTier tier)
		{
			if (!ByTier.TryGetValue(tier, out var value))
			{
				return null;
			}
			return value;
		}

		internal static BiomeTier TierOfPortal(string prefabName)
		{
			if (string.IsNullOrEmpty(prefabName))
			{
				return BiomeTier.None;
			}
			string key = (prefabName.EndsWith("(Clone)") ? prefabName.Substring(0, prefabName.Length - 7) : prefabName);
			if (!ByPortalPrefab.TryGetValue(key, out var value))
			{
				return BiomeTier.None;
			}
			return value.Tier;
		}

		internal static bool IsTollboundPortal(string prefabName)
		{
			return TierOfPortal(prefabName) != BiomeTier.None;
		}
	}
}
namespace Tollbound.Messages
{
	internal static class DefaultVoice
	{
		internal const string Text = "# Tollbound - the voice of the spirits\n#\n# Sections are [spirit.mood.slot]. Lines starting with # are ignored.\n#\n#   spirit   blackforest swamp mountain plains mistlands ashlands, or fallback\n#   mood     restless (the boss still lives) or sated (you have killed it)\n#   slot     opener   atmosphere, shown first, never names the spirit\n#            refused  the crossing will not carry what you have\n#            unpaid   you cannot afford the toll\n#            toll     the toll was taken and you crossed\n#\n# A message is one opener plus one line from the matching slot, so six of each\n# gives thirty-six. Any spirit or slot with no lines falls back to [fallback].\n#\n# TWO RULES, and the whole thing falls apart without them:\n#\n#   1. Every line is a COMPLETE SENTENCE with its own punctuation. They are\n#      assembled, never joined, so any opener can precede any demand.\n#   2. NEVER put an item name, a count or a number in a line. Those live in the\n#      separate ledger message. This is what makes broken grammar impossible.\n#\n# This file is never rewritten, so your edits are safe. Updates only append\n# sections that are missing entirely. To silence a spirit, empty its section\n# rather than deleting it -- a deleted section counts as missing and comes back.\n\n\n# ---------------------------------------------------------------- Black Forest\n\n[blackforest.restless.opener]\nThe roots tighten around the stones.\nSomething old shifts beneath the moss.\nBark splits somewhere behind you.\nThe forest leans in to watch.\nSap runs black down the frame.\nThe trees stand too still.\n\n[blackforest.restless.refused]\nThe Elder will not have it.\nThese woods do not open that far.\nRoots close over the way.\nHis reach ends here, and so does yours.\n\n[blackforest.restless.unpaid]\nThe Elder will have his due.\nNothing leaves these woods unweighed.\nPay in eyes, or turn back.\nHe counts every stone taken from him.\n\n[blackforest.restless.toll]\nThe Elder takes his share.\nRoots part, grudgingly.\nThe debt is noted.\nHe allows it. He remembers it.\n\n[blackforest.sated.opener]\nThe moss lies quiet.\nNothing stirs in the branches.\nThe roots part without complaint.\nThe forest is only a forest now.\nOld wood creaks and settles.\nLight falls where it did not before.\n\n[blackforest.sated.refused]\nEven so, the way is closed.\nKilling him did not widen the road.\nThis much the woods still refuse.\n\n[blackforest.sated.unpaid]\nA dead king is owed all the same.\nThe forest keeps its tolls.\nPay him. He is past arguing.\n\n[blackforest.sated.toll]\nThe old debt, settled again.\nHe takes it without stirring.\nThe woods let you by.\n\n\n# ----------------------------------------------------------------------- Swamp\n\n[swamp.restless.opener]\nThe bog exhales.\nSomething vast turns over in the mud.\nThe water goes still and green.\nFlies lift off the reeds all at once.\nThe reeds bow with no wind to bow them.\nA sound like breathing, from under.\n\n[swamp.restless.refused]\nThe mire will not swallow that.\nToo much. The mud refuses it.\nThat does not pass through here.\n\n[swamp.restless.unpaid]\nThe mire keeps what it is owed.\nFeed it, or keep what you carry.\nNothing leaves the marsh unweighed.\nBonemass counts in guts, not coin.\n\n[swamp.restless.toll]\nThe mass takes its share.\nSomething under the water is satisfied.\nThe bog closes over the debt.\n\n[swamp.sated.opener]\nThe mud parts where you walk.\nThe marsh is quiet now.\nThe water runs almost clear.\nNothing rises to meet you.\nThe reeds stand where they are put.\n\n[swamp.sated.refused]\nQuiet as it is, the way stays shut.\nThe marsh drowned him and kept his rules.\n\n[swamp.sated.unpaid]\nIt still remembers the debt.\nThe mire outlived him. So did the toll.\n\n[swamp.sated.toll]\nBonemass takes his share and lets you go.\nThe mud accepts it, as it always has.\n\n\n# -------------------------------------------------------------------- Mountain\n\n[mountain.restless.opener]\nWind screams down the ridge.\nFrost creeps across the stones.\nA shadow crosses the snow. Nothing casts it.\nThe cold finds the seams in your pack.\nSomething wheels, far above the cloud.\nThe air thins, and keeps thinning.\n\n[mountain.restless.refused]\nThe peak will not let that pass.\nToo heavy for this thin air.\nShe has seen it, and she says no.\nThe way narrows. That does not fit.\n\n[mountain.restless.unpaid]\nModer counts what you carry.\nThe mountain takes its share in glands.\nPay the cold, or climb down.\nNothing crosses her sky for free.\n\n[mountain.restless.toll]\nModer takes her share.\nThe wind drops, briefly.\nSomething above is satisfied.\nPaid. The cold lets go of you.\n\n[mountain.sated.opener]\nThe ridge is silent.\nNo shadow crosses the snow now.\nThe wind is only wind.\nFrost gathers, and nothing watches it.\nThe peak keeps its own counsel.\n\n[mountain.sated.refused]\nEven her death did not open that.\nThe mountain is older than she was.\nStill no. Some things do not fly.\n\n[mountain.sated.unpaid]\nHer nest is cold. The toll is not.\nThe peak collects for her.\nPay it. She is beyond spending it.\n\n[mountain.sated.toll]\nThe mountain takes what she is owed.\nPaid, to nothing in particular.\nThe wind lets you through.\n\n\n# ---------------------------------------------------------------------- Plains\n\n[plains.restless.opener]\nAsh drifts across the barrow.\nSomething buried grinds against stone.\nHeat rises off the grass with no sun to give it.\nThe tar goes still.\nA hand's shadow falls where there is no hand.\nThe ground remembers a weight.\n\n[plains.restless.refused]\nThe ruined king will not permit it.\nThat is not yours to carry out.\nHis hand closes. The way with it.\nRefused, and not gently.\n\n[plains.restless.unpaid]\nYagluth's hand still closes. Pay it.\nThe ruined king demands tribute.\nBlack metal answers to him first.\nTribute, or turn around.\n\n[plains.restless.toll]\nYagluth takes his tribute.\nThe ash settles, satisfied.\nThe buried hand opens.\nPaid to a king who has no kingdom.\n\n[plains.sated.opener]\nThe barrow is quiet at last.\nAsh falls and stays fallen.\nNothing stirs under the stones.\nThe tar lies flat and cold.\nThe plains are only grass now.\n\n[plains.sated.refused]\nBroken as he is, not that.\nThe ruin holds its last rule.\nEven now his hand is closed on this.\n\n[plains.sated.unpaid]\nA dead king still takes tribute.\nPay the ruin. It does not forgive.\nHis hand is dust. It is still open.\n\n[plains.sated.toll]\nThe ruin takes its tribute.\nAsh accepts, as ash does.\nPaid, and the way is clear.\n\n\n# ------------------------------------------------------------------- Mistlands\n\n[mistlands.restless.opener]\nThe mist thickens without wind.\nTen thousand small sounds stop at once.\nSomething in the fog counts you.\nThe stone hums a note you cannot hold.\nWings, somewhere, out of time with each other.\nThe fog leans closer.\n\n[mistlands.restless.refused]\nThe hive will not pass it.\nShe has weighed it and found it hers.\nNot through her mist.\nThe counting stops. So do you.\n\n[mistlands.restless.unpaid]\nThe Queen's brood attends the ledger.\nThe hive marks what leaves the mist.\nNothing crosses without her leave.\nPay her, or be counted twice.\n\n[mistlands.restless.toll]\nThe Queen takes her portion.\nThe counting resumes.\nSomething in the fog is content.\nThe mist opens, precisely.\n\n[mistlands.sated.opener]\nThe mist drifts, and nothing moves in it.\nThe small sounds are only insects now.\nThe fog has stopped counting.\nNothing hums in the stone.\nThe Mistlands keep their quiet.\n\n[mistlands.sated.refused]\nHer hive is dead. Its rules are not.\nStill counted. Still refused.\nNot even now.\n\n[mistlands.sated.unpaid]\nThe brood keeps her books.\nPay what she is no longer here to take.\nThe ledger outlived the Queen.\n\n[mistlands.sated.toll]\nThe hive takes its portion.\nThe mist parts, out of habit.\nPaid, and counted, and let by.\n\n\n# -------------------------------------------------------------------- Ashlands\n\n[ashlands.restless.opener]\nEmbers rise where there is no fire.\nThe ground beneath goes hot.\nSomething enormous draws breath.\nAsh falls upward for a moment.\nThe stone under your boots ticks as it cools.\nFar off, something turns in the fire.\n\n[ashlands.restless.refused]\nThe last flame refuses it.\nThat does not leave the burning.\nFader will not have it carried.\nThe fire closes over the way.\n\n[ashlands.restless.unpaid]\nFader knows the weight of flametal.\nThe ash keeps a tally.\nThe last flame takes its portion. Pay it.\nNothing leaves the fire unweighed.\n\n[ashlands.restless.toll]\nFader takes his portion.\nThe embers settle.\nSomething enormous lets out its breath.\nThe fire allows it.\n\n[ashlands.sated.opener]\nThe ash falls straight down.\nNothing breathes in the deep fire.\nThe ground has gone cool enough to stand on.\nThe embers rise and go out.\nThe burning is only burning now.\n\n[ashlands.sated.refused]\nThe fire is out. The rule is not.\nEven ended, he will not have it.\nNot through the ash.\n\n[ashlands.sated.unpaid]\nThe ash still tallies.\nPay the fire that killed him.\nHe is ash. The toll is not.\n\n[ashlands.sated.toll]\nThe ash takes its portion.\nPaid to the last flame, long gone out.\nThe fire lets you through.\n\n\n# -------------------------------------------------------------------- fallback\n# Used by any spirit with nothing authored. Keep these unspecific.\n\n[fallback.restless.opener]\nThe air changes at the threshold.\nSomething on the other side takes notice.\nThe stones hum, low and wrong.\nA cold draught comes through the frame.\n\n[fallback.restless.refused]\nThe way will not take it.\nThis crossing is closed to that.\nIt does not pass.\n\n[fallback.restless.unpaid]\nThe spirits want their due.\nNothing crosses unpaid.\nPayment first.\n\n[fallback.restless.toll]\nThe toll is taken.\nSomething on the other side is satisfied.\nThe way opens.\n\n[fallback.sated.opener]\nThe threshold is quiet.\nNothing contests the crossing.\nThe air settles.\n\n[fallback.sated.refused]\nEven now, not this.\nThe way stays shut to that.\n\n[fallback.sated.unpaid]\nThe dead are owed all the same.\nPay, though no one is left to ask.\n\n[fallback.sated.toll]\nThe toll is taken, as ever.\nThe way opens.\n";
	}
	internal static class Voice
	{
		internal static void Refused(Player player, Verdict verdict)
		{
			switch (verdict.Refusal)
			{
			case Refusal.AboveCeiling:
				Speak(player, verdict.OffendingTier, VoiceSlot.Refused);
				TopLeft(player, CeilingExplanation(verdict));
				break;
			case Refusal.TollUnpaid:
			{
				TollDue unaffordable = verdict.Unaffordable;
				Speak(player, unaffordable.Tier, VoiceSlot.Unpaid);
				string arg = ((unaffordable.Loads > 1) ? $" ({unaffordable.Loads} loads at {unaffordable.BaseAmount})" : "");
				TopLeft(player, Capitalize(SpiritOf(unaffordable.Tier)) + " demands " + Item(unaffordable.ItemPrefab) + " " + $"x{unaffordable.Amount}{arg}. You carry {unaffordable.Held}.");
				break;
			}
			case Refusal.Unrecognized:
				Speak(player, BiomeTier.None, VoiceSlot.Refused);
				TopLeft(player, Item(verdict.UnrecognizedItem) + " answers to no spirit here. It cannot cross.");
				break;
			}
		}

		internal static void Crossed(Player player, Verdict verdict, List<Loss> losses)
		{
			if (verdict.Tolls.Count > 0 || losses.Count > 0)
			{
				Speak(player, SpeakerFor(verdict, losses), VoiceSlot.Toll);
			}
			foreach (TollDue toll in verdict.Tolls)
			{
				string text = ((toll.Loads > 1) ? $" ({toll.Loads} loads)" : "");
				TopLeft(player, $"{Item(toll.ItemPrefab)} x{toll.Amount} - paid to {SpiritOf(toll.Tier)}{text}");
			}
			foreach (Loss loss in losses)
			{
				TopLeft(player, $"{Item(loss.Prefab)} x{loss.Count} - claimed by {SpiritOf(loss.Tier)}");
			}
		}

		private static BiomeTier SpeakerFor(Verdict verdict, List<Loss> losses)
		{
			if (losses.Count > 0)
			{
				return losses.OrderByDescending((Loss l) => l.Count).First().Tier;
			}
			if (verdict.Tolls.Count <= 0)
			{
				return BiomeTier.None;
			}
			return verdict.Tolls.OrderByDescending((TollDue t) => (int)t.Tier).First().Tier;
		}

		private static void Speak(Player player, BiomeTier tier, VoiceSlot slot)
		{
			if (TollboundConfig.SpiritDialogue.Value)
			{
				bool sated = tier != BiomeTier.None && TollGate.BossIsDead(tier);
				string text = VoiceBook.Line(tier, sated, VoiceSlot.Opener);
				string text2 = VoiceBook.Line(tier, sated, slot);
				string text3 = (string.IsNullOrEmpty(text) ? text2 : (text + " " + text2));
				if (!string.IsNullOrEmpty(text3))
				{
					((Character)player).Message((MessageType)2, text3, 0, (Sprite)null);
				}
			}
		}

		private static string CeilingExplanation(Verdict verdict)
		{
			string text = Item(verdict.OffendingItem);
			if (verdict.Ceiling == BiomeTier.None)
			{
				return "This crossing will not carry " + text + ".";
			}
			TierInfo tierInfo = Tiers.Get(verdict.Ceiling);
			TierInfo tierInfo2 = Tiers.Get(verdict.OffendingTier);
			return "A " + tierInfo.BiomeName + " crossing will not carry " + text + " (" + tierInfo2.BiomeName + ").";
		}

		private static string SpiritOf(BiomeTier tier)
		{
			TierInfo tierInfo = Tiers.Get(tier);
			if (tierInfo != null)
			{
				return tierInfo.SpiritName;
			}
			return "the spirits";
		}

		private static string Item(string prefabName)
		{
			return PortalIdentity.DisplayName(prefabName);
		}

		private static string Capitalize(string s)
		{
			if (!string.IsNullOrEmpty(s))
			{
				return char.ToUpperInvariant(s[0]) + s.Substring(1);
			}
			return s;
		}

		private static void TopLeft(Player player, string text)
		{
			((Character)player).Message((MessageType)1, text, 0, (Sprite)null);
		}
	}
	internal enum VoiceSlot
	{
		Opener,
		Refused,
		Unpaid,
		Toll
	}
	internal static class VoiceBook
	{
		private const string FileName = "voice.txt";

		private const int MemoryDepth = 3;

		private static readonly Dictionary<string, List<string>> Pools = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

		private static readonly Dictionary<string, Queue<string>> Recent = new Dictionary<string, Queue<string>>(StringComparer.OrdinalIgnoreCase);

		internal static bool IsLoaded { get; private set; }

		internal static void Load()
		{
			try
			{
				string text = Path.Combine(Path.Combine(Paths.ConfigPath, "Tollbound"), "voice.txt");
				if (!File.Exists(text))
				{
					Directory.CreateDirectory(Path.GetDirectoryName(text));
					File.WriteAllText(text, "# Tollbound - the voice of the spirits\n#\n# Sections are [spirit.mood.slot]. Lines starting with # are ignored.\n#\n#   spirit   blackforest swamp mountain plains mistlands ashlands, or fallback\n#   mood     restless (the boss still lives) or sated (you have killed it)\n#   slot     opener   atmosphere, shown first, never names the spirit\n#            refused  the crossing will not carry what you have\n#            unpaid   you cannot afford the toll\n#            toll     the toll was taken and you crossed\n#\n# A message is one opener plus one line from the matching slot, so six of each\n# gives thirty-six. Any spirit or slot with no lines falls back to [fallback].\n#\n# TWO RULES, and the whole thing falls apart without them:\n#\n#   1. Every line is a COMPLETE SENTENCE with its own punctuation. They are\n#      assembled, never joined, so any opener can precede any demand.\n#   2. NEVER put an item name, a count or a number in a line. Those live in the\n#      separate ledger message. This is what makes broken grammar impossible.\n#\n# This file is never rewritten, so your edits are safe. Updates only append\n# sections that are missing entirely. To silence a spirit, empty its section\n# rather than deleting it -- a deleted section counts as missing and comes back.\n\n\n# ---------------------------------------------------------------- Black Forest\n\n[blackforest.restless.opener]\nThe roots tighten around the stones.\nSomething old shifts beneath the moss.\nBark splits somewhere behind you.\nThe forest leans in to watch.\nSap runs black down the frame.\nThe trees stand too still.\n\n[blackforest.restless.refused]\nThe Elder will not have it.\nThese woods do not open that far.\nRoots close over the way.\nHis reach ends here, and so does yours.\n\n[blackforest.restless.unpaid]\nThe Elder will have his due.\nNothing leaves these woods unweighed.\nPay in eyes, or turn back.\nHe counts every stone taken from him.\n\n[blackforest.restless.toll]\nThe Elder takes his share.\nRoots part, grudgingly.\nThe debt is noted.\nHe allows it. He remembers it.\n\n[blackforest.sated.opener]\nThe moss lies quiet.\nNothing stirs in the branches.\nThe roots part without complaint.\nThe forest is only a forest now.\nOld wood creaks and settles.\nLight falls where it did not before.\n\n[blackforest.sated.refused]\nEven so, the way is closed.\nKilling him did not widen the road.\nThis much the woods still refuse.\n\n[blackforest.sated.unpaid]\nA dead king is owed all the same.\nThe forest keeps its tolls.\nPay him. He is past arguing.\n\n[blackforest.sated.toll]\nThe old debt, settled again.\nHe takes it without stirring.\nThe woods let you by.\n\n\n# ----------------------------------------------------------------------- Swamp\n\n[swamp.restless.opener]\nThe bog exhales.\nSomething vast turns over in the mud.\nThe water goes still and green.\nFlies lift off the reeds all at once.\nThe reeds bow with no wind to bow them.\nA sound like breathing, from under.\n\n[swamp.restless.refused]\nThe mire will not swallow that.\nToo much. The mud refuses it.\nThat does not pass through here.\n\n[swamp.restless.unpaid]\nThe mire keeps what it is owed.\nFeed it, or keep what you carry.\nNothing leaves the marsh unweighed.\nBonemass counts in guts, not coin.\n\n[swamp.restless.toll]\nThe mass takes its share.\nSomething under the water is satisfied.\nThe bog closes over the debt.\n\n[swamp.sated.opener]\nThe mud parts where you walk.\nThe marsh is quiet now.\nThe water runs almost clear.\nNothing rises to meet you.\nThe reeds stand where they are put.\n\n[swamp.sated.refused]\nQuiet as it is, the way stays shut.\nThe marsh drowned him and kept his rules.\n\n[swamp.sated.unpaid]\nIt still remembers the debt.\nThe mire outlived him. So did the toll.\n\n[swamp.sated.toll]\nBonemass takes his share and lets you go.\nThe mud accepts it, as it always has.\n\n\n# -------------------------------------------------------------------- Mountain\n\n[mountain.restless.opener]\nWind screams down the ridge.\nFrost creeps across the stones.\nA shadow crosses the snow. Nothing casts it.\nThe cold finds the seams in your pack.\nSomething wheels, far above the cloud.\nThe air thins, and keeps thinning.\n\n[mountain.restless.refused]\nThe peak will not let that pass.\nToo heavy for this thin air.\nShe has seen it, and she says no.\nThe way narrows. That does not fit.\n\n[mountain.restless.unpaid]\nModer counts what you carry.\nThe mountain takes its share in glands.\nPay the cold, or climb down.\nNothing crosses her sky for free.\n\n[mountain.restless.toll]\nModer takes her share.\nThe wind drops, briefly.\nSomething above is satisfied.\nPaid. The cold lets go of you.\n\n[mountain.sated.opener]\nThe ridge is silent.\nNo shadow crosses the snow now.\nThe wind is only wind.\nFrost gathers, and nothing watches it.\nThe peak keeps its own counsel.\n\n[mountain.sated.refused]\nEven her death did not open that.\nThe mountain is older than she was.\nStill no. Some things do not fly.\n\n[mountain.sated.unpaid]\nHer nest is cold. The toll is not.\nThe peak collects for her.\nPay it. She is beyond spending it.\n\n[mountain.sated.toll]\nThe mountain takes what she is owed.\nPaid, to nothing in particular.\nThe wind lets you through.\n\n\n# ---------------------------------------------------------------------- Plains\n\n[plains.restless.opener]\nAsh drifts across the barrow.\nSomething buried grinds against stone.\nHeat rises off the grass with no sun to give it.\nThe tar goes still.\nA hand's shadow falls where there is no hand.\nThe ground remembers a weight.\n\n[plains.restless.refused]\nThe ruined king will not permit it.\nThat is not yours to carry out.\nHis hand closes. The way with it.\nRefused, and not gently.\n\n[plains.restless.unpaid]\nYagluth's hand still closes. Pay it.\nThe ruined king demands tribute.\nBlack metal answers to him first.\nTribute, or turn around.\n\n[plains.restless.toll]\nYagluth takes his tribute.\nThe ash settles, satisfied.\nThe buried hand opens.\nPaid to a king who has no kingdom.\n\n[plains.sated.opener]\nThe barrow is quiet at last.\nAsh falls and stays fallen.\nNothing stirs under the stones.\nThe tar lies flat and cold.\nThe plains are only grass now.\n\n[plains.sated.refused]\nBroken as he is, not that.\nThe ruin holds its last rule.\nEven now his hand is closed on this.\n\n[plains.sated.unpaid]\nA dead king still takes tribute.\nPay the ruin. It does not forgive.\nHis hand is dust. It is still open.\n\n[plains.sated.toll]\nThe ruin takes its tribute.\nAsh accepts, as ash does.\nPaid, and the way is clear.\n\n\n# ------------------------------------------------------------------- Mistlands\n\n[mistlands.restless.opener]\nThe mist thickens without wind.\nTen thousand small sounds stop at once.\nSomething in the fog counts you.\nThe stone hums a note you cannot hold.\nWings, somewhere, out of time with each other.\nThe fog leans closer.\n\n[mistlands.restless.refused]\nThe hive will not pass it.\nShe has weighed it and found it hers.\nNot through her mist.\nThe counting stops. So do you.\n\n[mistlands.restless.unpaid]\nThe Queen's brood attends the ledger.\nThe hive marks what leaves the mist.\nNothing crosses without her leave.\nPay her, or be counted twice.\n\n[mistlands.restless.toll]\nThe Queen takes her portion.\nThe counting resumes.\nSomething in the fog is content.\nThe mist opens, precisely.\n\n[mistlands.sated.opener]\nThe mist drifts, and nothing moves in it.\nThe small sounds are only insects now.\nThe fog has stopped counting.\nNothing hums in the stone.\nThe Mistlands keep their quiet.\n\n[mistlands.sated.refused]\nHer hive is dead. Its rules are not.\nStill counted. Still refused.\nNot even now.\n\n[mistlands.sated.unpaid]\nThe brood keeps her books.\nPay what she is no longer here to take.\nThe ledger outlived the Queen.\n\n[mistlands.sated.toll]\nThe hive takes its portion.\nThe mist parts, out of habit.\nPaid, and counted, and let by.\n\n\n# -------------------------------------------------------------------- Ashlands\n\n[ashlands.restless.opener]\nEmbers rise where there is no fire.\nThe ground beneath goes hot.\nSomething enormous draws breath.\nAsh falls upward for a moment.\nThe stone under your boots ticks as it cools.\nFar off, something turns in the fire.\n\n[ashlands.restless.refused]\nThe last flame refuses it.\nThat does not leave the burning.\nFader will not have it carried.\nThe fire closes over the way.\n\n[ashlands.restless.unpaid]\nFader knows the weight of flametal.\nThe ash keeps a tally.\nThe last flame takes its portion. Pay it.\nNothing leaves the fire unweighed.\n\n[ashlands.restless.toll]\nFader takes his portion.\nThe embers settle.\nSomething enormous lets out its breath.\nThe fire allows it.\n\n[ashlands.sated.opener]\nThe ash falls straight down.\nNothing breathes in the deep fire.\nThe ground has gone cool enough to stand on.\nThe embers rise and go out.\nThe burning is only burning now.\n\n[ashlands.sated.refused]\nThe fire is out. The rule is not.\nEven ended, he will not have it.\nNot through the ash.\n\n[ashlands.sated.unpaid]\nThe ash still tallies.\nPay the fire that killed him.\nHe is ash. The toll is not.\n\n[ashlands.sated.toll]\nThe ash takes its portion.\nPaid to the last flame, long gone out.\nThe fire lets you through.\n\n\n# -------------------------------------------------------------------- fallback\n# Used by any spirit with nothing authored. Keep these unspecific.\n\n[fallback.restless.opener]\nThe air changes at the threshold.\nSomething on the other side takes notice.\nThe stones hum, low and wrong.\nA cold draught comes through the frame.\n\n[fallback.restless.refused]\nThe way will not take it.\nThis crossing is closed to that.\nIt does not pass.\n\n[fallback.restless.unpaid]\nThe spirits want their due.\nNothing crosses unpaid.\nPayment first.\n\n[fallback.restless.toll]\nThe toll is taken.\nSomething on the other side is satisfied.\nThe way opens.\n\n[fallback.sated.opener]\nThe threshold is quiet.\nNothing contests the crossing.\nThe air settles.\n\n[fallback.sated.refused]\nEven now, not this.\nThe way stays shut to that.\n\n[fallback.sated.unpaid]\nThe dead are owed all the same.\nPay, though no one is left to ask.\n\n[fallback.sated.toll]\nThe toll is taken, as ever.\nThe way opens.\n");
					TollboundPlugin.LogInfo("Wrote default spirit lines to " + text);
				}
				Parse(File.ReadAllText(text));
				MergeNewSections(text);
				IsLoaded = true;
				int num = 0;
				foreach (List<string> value in Pools.Values)
				{
					num += value.Count;
				}
				TollboundPlugin.LogInfo($"Spirit voice: {num} line(s) across {Pools.Count} pool(s).");
			}
			catch (Exception ex)
			{
				TollboundPlugin.LogError("Could not load spirit lines, falling back to silence: " + ex.Message);
				IsLoaded = false;
			}
		}

		private static void MergeNewSections(string path)
		{
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			string text = null;
			string[] array = "# Tollbound - the voice of the spirits\n#\n# Sections are [spirit.mood.slot]. Lines starting with # are ignored.\n#\n#   spirit   blackforest swamp mountain plains mistlands ashlands, or fallback\n#   mood     restless (the boss still lives) or sated (you have killed it)\n#   slot     opener   atmosphere, shown first, never names the spirit\n#            refused  the crossing will not carry what you have\n#            unpaid   you cannot afford the toll\n#            toll     the toll was taken and you crossed\n#\n# A message is one opener plus one line from the matching slot, so six of each\n# gives thirty-six. Any spirit or slot with no lines falls back to [fallback].\n#\n# TWO RULES, and the whole thing falls apart without them:\n#\n#   1. Every line is a COMPLETE SENTENCE with its own punctuation. They are\n#      assembled, never joined, so any opener can precede any demand.\n#   2. NEVER put an item name, a count or a number in a line. Those live in the\n#      separate ledger message. This is what makes broken grammar impossible.\n#\n# This file is never rewritten, so your edits are safe. Updates only append\n# sections that are missing entirely. To silence a spirit, empty its section\n# rather than deleting it -- a deleted section counts as missing and comes back.\n\n\n# ---------------------------------------------------------------- Black Forest\n\n[blackforest.restless.opener]\nThe roots tighten around the stones.\nSomething old shifts beneath the moss.\nBark splits somewhere behind you.\nThe forest leans in to watch.\nSap runs black down the frame.\nThe trees stand too still.\n\n[blackforest.restless.refused]\nThe Elder will not have it.\nThese woods do not open that far.\nRoots close over the way.\nHis reach ends here, and so does yours.\n\n[blackforest.restless.unpaid]\nThe Elder will have his due.\nNothing leaves these woods unweighed.\nPay in eyes, or turn back.\nHe counts every stone taken from him.\n\n[blackforest.restless.toll]\nThe Elder takes his share.\nRoots part, grudgingly.\nThe debt is noted.\nHe allows it. He remembers it.\n\n[blackforest.sated.opener]\nThe moss lies quiet.\nNothing stirs in the branches.\nThe roots part without complaint.\nThe forest is only a forest now.\nOld wood creaks and settles.\nLight falls where it did not before.\n\n[blackforest.sated.refused]\nEven so, the way is closed.\nKilling him did not widen the road.\nThis much the woods still refuse.\n\n[blackforest.sated.unpaid]\nA dead king is owed all the same.\nThe forest keeps its tolls.\nPay him. He is past arguing.\n\n[blackforest.sated.toll]\nThe old debt, settled again.\nHe takes it without stirring.\nThe woods let you by.\n\n\n# ----------------------------------------------------------------------- Swamp\n\n[swamp.restless.opener]\nThe bog exhales.\nSomething vast turns over in the mud.\nThe water goes still and green.\nFlies lift off the reeds all at once.\nThe reeds bow with no wind to bow them.\nA sound like breathing, from under.\n\n[swamp.restless.refused]\nThe mire will not swallow that.\nToo much. The mud refuses it.\nThat does not pass through here.\n\n[swamp.restless.unpaid]\nThe mire keeps what it is owed.\nFeed it, or keep what you carry.\nNothing leaves the marsh unweighed.\nBonemass counts in guts, not coin.\n\n[swamp.restless.toll]\nThe mass takes its share.\nSomething under the water is satisfied.\nThe bog closes over the debt.\n\n[swamp.sated.opener]\nThe mud parts where you walk.\nThe marsh is quiet now.\nThe water runs almost clear.\nNothing rises to meet you.\nThe reeds stand where they are put.\n\n[swamp.sated.refused]\nQuiet as it is, the way stays shut.\nThe marsh drowned him and kept his rules.\n\n[swamp.sated.unpaid]\nIt still remembers the debt.\nThe mire outlived him. So did the toll.\n\n[swamp.sated.toll]\nBonemass takes his share and lets you go.\nThe mud accepts it, as it always has.\n\n\n# -------------------------------------------------------------------- Mountain\n\n[mountain.restless.opener]\nWind screams down the ridge.\nFrost creeps across the stones.\nA shadow crosses the snow. Nothing casts it.\nThe cold finds the seams in your pack.\nSomething wheels, far above the cloud.\nThe air thins, and keeps thinning.\n\n[mountain.restless.refused]\nThe peak will not let that pass.\nToo heavy for this thin air.\nShe has seen it, and she says no.\nThe way narrows. That does not fit.\n\n[mountain.restless.unpaid]\nModer counts what you carry.\nThe mountain takes its share in glands.\nPay the cold, or climb down.\nNothing crosses her sky for free.\n\n[mountain.restless.toll]\nModer takes her share.\nThe wind drops, briefly.\nSomething above is satisfied.\nPaid. The cold lets go of you.\n\n[mountain.sated.opener]\nThe ridge is silent.\nNo shadow crosses the snow now.\nThe wind is only wind.\nFrost gathers, and nothing watches it.\nThe peak keeps its own counsel.\n\n[mountain.sated.refused]\nEven her death did not open that.\nThe mountain is older than she was.\nStill no. Some things do not fly.\n\n[mountain.sated.unpaid]\nHer nest is cold. The toll is not.\nThe peak collects for her.\nPay it. She is beyond spending it.\n\n[mountain.sated.toll]\nThe mountain takes what she is owed.\nPaid, to nothing in particular.\nThe wind lets you through.\n\n\n# ---------------------------------------------------------------------- Plains\n\n[plains.restless.opener]\nAsh drifts across the barrow.\nSomething buried grinds against stone.\nHeat rises off the grass with no sun to give it.\nThe tar goes still.\nA hand's shadow falls where there is no hand.\nThe ground remembers a weight.\n\n[plains.restless.refused]\nThe ruined king will not permit it.\nThat is not yours to carry out.\nHis hand closes. The way with it.\nRefused, and not gently.\n\n[plains.restless.unpaid]\nYagluth's hand still closes. Pay it.\nThe ruined king demands tribute.\nBlack metal answers to him first.\nTribute, or turn around.\n\n[plains.restless.toll]\nYagluth takes his tribute.\nThe ash settles, satisfied.\nThe buried hand opens.\nPaid to a king who has no kingdom.\n\n[plains.sated.opener]\nThe barrow is quiet at last.\nAsh falls and stays fallen.\nNothing stirs under the stones.\nThe tar lies flat and cold.\nThe plains are only grass now.\n\n[plains.sated.refused]\nBroken as he is, not that.\nThe ruin holds its last rule.\nEven now his hand is closed on this.\n\n[plains.sated.unpaid]\nA dead king still takes tribute.\nPay the ruin. It does not forgive.\nHis hand is dust. It is still open.\n\n[plains.sated.toll]\nThe ruin takes its tribute.\nAsh accepts, as ash does.\nPaid, and the way is clear.\n\n\n# ------------------------------------------------------------------- Mistlands\n\n[mistlands.restless.opener]\nThe mist thickens without wind.\nTen thousand small sounds stop at once.\nSomething in the fog counts you.\nThe stone hums a note you cannot hold.\nWings, somewhere, out of time with each other.\nThe fog leans closer.\n\n[mistlands.restless.refused]\nThe hive will not pass it.\nShe has weighed it and found it hers.\nNot through her mist.\nThe counting stops. So do you.\n\n[mistlands.restless.unpaid]\nThe Queen's brood attends the ledger.\nThe hive marks what leaves the mist.\nNothing crosses without her leave.\nPay her, or be counted twice.\n\n[mistlands.restless.toll]\nThe Queen takes her portion.\nThe counting resumes.\nSomething in the fog is content.\nThe mist opens, precisely.\n\n[mistlands.sated.opener]\nThe mist drifts, and nothing moves in it.\nThe small sounds are only insects now.\nThe fog has stopped counting.\nNothing hums in the stone.\nThe Mistlands keep their quiet.\n\n[mistlands.sated.refused]\nHer hive is dead. Its rules are not.\nStill counted. Still refused.\nNot even now.\n\n[mistlands.sated.unpaid]\nThe brood keeps her books.\nPay what she is no longer here to take.\nThe ledger outlived the Queen.\n\n[mistlands.sated.toll]\nThe hive takes its portion.\nThe mist parts, out of habit.\nPaid, and counted, and let by.\n\n\n# -------------------------------------------------------------------- Ashlands\n\n[ashlands.restless.opener]\nEmbers rise where there is no fire.\nThe ground beneath goes hot.\nSomething enormous draws breath.\nAsh falls upward for a moment.\nThe stone under your boots ticks as it cools.\nFar off, something turns in the fire.\n\n[ashlands.restless.refused]\nThe last flame refuses it.\nThat does not leave the burning.\nFader will not have it carried.\nThe fire closes over the way.\n\n[ashlands.restless.unpaid]\nFader knows the weight of flametal.\nThe ash keeps a tally.\nThe last flame takes its portion. Pay it.\nNothing leaves the fire unweighed.\n\n[ashlands.restless.toll]\nFader takes his portion.\nThe embers settle.\nSomething enormous lets out its breath.\nThe fire allows it.\n\n[ashlands.sated.opener]\nThe ash falls straight down.\nNothing breathes in the deep fire.\nThe ground has gone cool enough to stand on.\nThe embers rise and go out.\nThe burning is only burning now.\n\n[ashlands.sated.refused]\nThe fire is out. The rule is not.\nEven ended, he will not have it.\nNot through the ash.\n\n[ashlands.sated.unpaid]\nThe ash still tallies.\nPay the fire that killed him.\nHe is ash. The toll is not.\n\n[ashlands.sated.toll]\nThe ash takes its portion.\nPaid to the last flame, long gone out.\nThe fire lets you through.\n\n\n# -------------------------------------------------------------------- fallback\n# Used by any spirit with nothing authored. Keep these unspecific.\n\n[fallback.restless.opener]\nThe air changes at the threshold.\nSomething on the other side takes notice.\nThe stones hum, low and wrong.\nA cold draught comes through the frame.\n\n[fallback.restless.refused]\nThe way will not take it.\nThis crossing is closed to that.\nIt does not pass.\n\n[fallback.restless.unpaid]\nThe spirits want their due.\nNothing crosses unpaid.\nPayment first.\n\n[fallback.restless.toll]\nThe toll is taken.\nSomething on the other side is satisfied.\nThe way opens.\n\n[fallback.sated.opener]\nThe threshold is quiet.\nNothing contests the crossing.\nThe air settles.\n\n[fallback.sated.refused]\nEven now, not this.\nThe way stays shut to that.\n\n[fallback.sated.unpaid]\nThe dead are owed all the same.\nPay, though no one is left to ask.\n\n[fallback.sated.toll]\nThe toll is taken, as ever.\nThe way opens.\n".Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length == 0 || text2[0] == '#')
				{
					continue;
				}
				if (text2[0] == '[' && text2[text2.Length - 1] == ']')
				{
					text = text2.Substring(1, text2.Length - 2).Trim().ToLowerInvariant();
					if (Pools.ContainsKey(text))
					{
						text = null;
						continue;
					}
					list.Add(text);
					list2.Add("");
					list2.Add("[" + text + "]");
					Pools[text] = new List<string>();
				}
				else if (text != null)
				{
					list2.Add(text2);
					Pools[text].Add(text2);
				}
			}
			if (list.Count != 0)
			{
				File.AppendAllText(path, "\n\n# Added by a Tollbound update. Edit freely; this file is never rewritten.\n" + string.Join("\n", list2.ToArray()) + "\n");
				TollboundPlugin.LogInfo($"Added {list.Count} new spirit section(s) to voice.txt: " + string.Join(", ", list.ToArray()));
			}
		}

		private static void Parse(string text)
		{
			Pools.Clear();
			Recent.Clear();
			string text2 = null;
			string[] array = text.Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text3 = array[i].Trim();
				if (text3.Length == 0 || text3[0] == '#')
				{
					continue;
				}
				if (text3[0] == '[' && text3[text3.Length - 1] == ']')
				{
					text2 = text3.Substring(1, text3.Length - 2).Trim().ToLowerInvariant();
				}
				else if (text2 != null)
				{
					if (!Pools.TryGetValue(text2, out var value))
					{
						value = new List<string>();
						Pools[text2] = value;
					}
					value.Add(text3);
				}
			}
		}

		internal static string Line(BiomeTier tier, bool sated, VoiceSlot slot)
		{
			if (!IsLoaded)
			{
				return null;
			}
			string text = (sated ? "sated" : "restless");
			string text2 = slot.ToString().ToLowerInvariant();
			return Pick(SpiritKey(tier) + "." + text + "." + text2) ?? Pick("fallback." + text + "." + text2);
		}

		private static string Pick(string key)
		{
			if (!Pools.TryGetValue(key, out var value) || value.Count == 0)
			{
				return null;
			}
			if (!Recent.TryGetValue(key, out var value2))
			{
				value2 = new Queue<string>();
				Recent[key] = value2;
			}
			int num = Math.Min(3, value.Count - 1);
			string text = null;
			for (int i = 0; i < 8; i++)
			{
				string text2 = value[Random.Range(0, value.Count)];
				if (num <= 0 || !value2.Contains(text2))
				{
					text = text2;
					break;
				}
			}
			if (text == null)
			{
				text = value[Random.Range(0, value.Count)];
			}
			value2.Enqueue(text);
			while (value2.Count > num)
			{
				value2.Dequeue();
			}
			return text;
		}

		private static string SpiritKey(BiomeTier tier)
		{
			return tier.ToString().ToLowerInvariant();
		}
	}
}
namespace Tollbound.Gameplay
{
	internal sealed class CargoLot
	{
		internal string Prefab;

		internal BiomeTier Tier;

		internal int Count;

		internal bool Destructible;
	}
	internal sealed class Manifest
	{
		internal readonly List<CargoLot> Lots = new List<CargoLot>();

		internal readonly List<string> Unrecognized = new List<string>();

		private static Manifest _cached;

		private static float _cachedAt = float.NegativeInfinity;

		internal bool IsEmpty
		{
			get
			{
				if (Lots.Count == 0)
				{
					return Unrecognized.Count == 0;
				}
				return false;
			}
		}

		internal List<BiomeTier> TiersPresent => (from t in Lots.Select((CargoLot l) => l.Tier).Distinct()
			orderby (int)t
			select t).ToList();

		internal BiomeTier HighestTier
		{
			get
			{
				if (Lots.Count != 0)
				{
					return Lots.Max((CargoLot l) => l.Tier);
				}
				return BiomeTier.None;
			}
		}

		internal IEnumerable<CargoLot> Of(BiomeTier tier)
		{
			return Lots.Where((CargoLot l) => l.Tier == tier);
		}

		internal int UnitsOf(BiomeTier tier)
		{
			return Of(tier).Sum((CargoLot l) => l.Count);
		}

		internal static Manifest Cached(Player player)
		{
			if (_cached != null && Time.time - _cachedAt < 0.25f)
			{
				return _cached;
			}
			_cached = Build(player);
			_cachedAt = Time.time;
			return _cached;
		}

		internal static Manifest Build(Player player)
		{
			Manifest manifest = new Manifest();
			Inventory val = (((Object)(object)player == (Object)null) ? null : ((Humanoid)player).GetInventory());
			if (val == null)
			{
				return manifest;
			}
			Dictionary<string, CargoLot> byPrefab = new Dictionary<string, CargoLot>();
			foreach (ItemData allItem in val.GetAllItems())
			{
				string text = PortalIdentity.PrefabNameOf(allItem);
				if (text == null)
				{
					continue;
				}
				BiomeTier biomeTier = CargoRegistry.TierOfItem(text);
				if (biomeTier == BiomeTier.None)
				{
					if (IsRestrictedByGame(text) && !manifest.Unrecognized.Contains(text))
					{
						manifest.Unrecognized.Add(text);
					}
					continue;
				}
				if (!byPrefab.TryGetValue(text, out var value))
				{
					value = new CargoLot
					{
						Prefab = text,
						Tier = biomeTier,
						Destructible = CargoRegistry.CanBeLost(text)
					};
					byPrefab[text] = value;
					manifest.Lots.Add(value);
				}
				value.Count += allItem.m_stack;
			}
			BackpackBridge.CollectCargo(player, delegate(string prefab, int count)
			{
				Record(manifest, byPrefab, prefab, count);
			});
			return manifest;
		}

		private static void Record(Manifest manifest, Dictionary<string, CargoLot> byPrefab, string prefab, int count)
		{
			if (count <= 0)
			{
				return;
			}
			BiomeTier biomeTier = CargoRegistry.TierOfItem(prefab);
			if (biomeTier == BiomeTier.None)
			{
				if (!manifest.Unrecognized.Contains(prefab) && IsRestrictedByGame(prefab))
				{
					manifest.Unrecognized.Add(prefab);
				}
				return;
			}
			if (!byPrefab.TryGetValue(prefab, out var value))
			{
				value = (byPrefab[prefab] = new CargoLot
				{
					Prefab = prefab,
					Tier = biomeTier,
					Destructible = CargoRegistry.CanBeLost(prefab)
				});
				manifest.Lots.Add(value);
			}
			value.Count += count;
		}

		private static bool IsRestrictedByGame(string prefab)
		{
			GameObject val = default(GameObject);
			if ((Object)(object)ObjectDB.instance == (Object)null || !ObjectDB.instance.TryGetItemPrefab(prefab, ref val) || (Object)(object)val == (Object)null)
			{
				return false;
			}
			ItemDrop component = val.GetComponent<ItemDrop>();
			if (component?.m_itemData?.m_shared != null)
			{
				return !component.m_itemData.m_shared.m_teleportable;
			}
			return false;
		}
	}
	internal static class Cargo
	{
		internal static int Count(Player player, string prefabName)
		{
			Inventory val = (((Object)(object)player == (Object)null) ? null : ((Humanoid)player).GetInventory());
			if (val == null || prefabName == null)
			{
				return 0;
			}
			return (from i in val.GetAllItems()
				where PortalIdentity.PrefabNameOf(i) == prefabName
				select i).Sum((ItemData i) => i.m_stack) + BackpackBridge.Count(player, prefabName);
		}

		internal static int Remove(Player player, string prefabName, int amount)
		{
			Inventory val = (((Object)(object)player == (Object)null) ? null : ((Humanoid)player).GetInventory());
			if (val == null || prefabName == null || amount <= 0)
			{
				return 0;
			}
			int num = 0;
			foreach (ItemData item in val.GetAllItems().ToList())
			{
				if (num >= amount)
				{
					break;
				}
				if (!(PortalIdentity.PrefabNameOf(item) != prefabName))
				{
					int num2 = Math.Min(item.m_stack, amount - num);
					val.RemoveItem(item, num2);
					num += num2;
				}
			}
			if (num < amount)
			{
				num += BackpackBridge.Remove(player, prefabName, amount - num);
			}
			return num;
		}
	}
	internal static class NearbyPortal
	{
		private const float StaleAfter = 1.5f;

		private static TeleportWorld _portal;

		private static float _distance;

		private static float _reportedAt = float.NegativeInfinity;

		internal static TeleportWorld Current
		{
			get
			{
				if (!(Time.time - _reportedAt > 1.5f) && !((Object)(object)_portal == (Object)null))
				{
					return _portal;
				}
				return null;
			}
		}

		internal static bool InRange => (Object)(object)Current != (Object)null;

		internal static void Report(TeleportWorld portal, float distance)
		{
			if (Time.time - _reportedAt > 1.5f || (Object)(object)_portal == (Object)null || (Object)(object)_portal == (Object)(object)portal || distance <= _distance)
			{
				_portal = portal;
				_distance = distance;
			}
			_reportedAt = Time.time;
		}

		internal static BiomeTier CeilingHere()
		{
			TeleportWorld current = Current;
			if ((Object)(object)current == (Object)null)
			{
				return BiomeTier.None;
			}
			return PortalIdentity.Ceiling(PortalIdentity.TierOf(current), PortalIdentity.TierOfFarEnd(current));
		}
	}
	internal enum Refusal
	{
		None,
		AboveCeiling,
		TollUnpaid,
		Unrecognized
	}
	internal sealed class TollDue
	{
		internal BiomeTier Tier;

		internal string ItemPrefab;

		internal int BaseAmount;

		internal int Loads = 1;

		internal int Amount;

		internal int Held;

		internal bool Affordable => Held >= Amount;
	}
	internal sealed class Loss
	{
		internal BiomeTier Tier;

		internal string Prefab;

		internal int Count;
	}
	internal sealed class Verdict
	{
		internal bool Allowed;

		internal Refusal Refusal;

		internal BiomeTier Ceiling;

		internal string OffendingItem;

		internal BiomeTier OffendingTier;

		internal TollDue Unaffordable;

		internal string UnrecognizedItem;

		internal readonly List<TollDue> Tolls = new List<TollDue>();

		internal Manifest Manifest;
	}
	internal static class TollGate
	{
		internal static Verdict Evaluate(Player player, BiomeTier near, BiomeTier far)
		{
			BiomeTier ceiling = PortalIdentity.Ceiling(near, far);
			Verdict verdict = new Verdict
			{
				Ceiling = ceiling,
				Manifest = Manifest.Build(player)
			};
			if (verdict.Manifest.IsEmpty)
			{
				verdict.Allowed = true;
				return verdict;
			}
			if (verdict.Manifest.Unrecognized.Count > 0)
			{
				verdict.Refusal = Refusal.Unrecognized;
				verdict.UnrecognizedItem = verdict.Manifest.Unrecognized[0];
				return verdict;
			}
			CargoLot cargoLot = (from l in verdict.Manifest.Lots
				where l.Tier > ceiling
				orderby l.Tier descending
				select l).FirstOrDefault();
			if (cargoLot != null)
			{
				verdict.Refusal = Refusal.AboveCeiling;
				verdict.OffendingItem = cargoLot.Prefab;
				verdict.OffendingTier = cargoLot.Tier;
				return verdict;
			}
			foreach (BiomeTier item in verdict.Manifest.TiersPresent)
			{
				TollDue tollDue = Price(item, verdict.Manifest.UnitsOf(item));
				if (tollDue != null)
				{
					tollDue.Held = Cargo.Count(player, tollDue.ItemPrefab);
					verdict.Tolls.Add(tollDue);
				}
			}
			TollDue tollDue2 = verdict.Tolls.FirstOrDefault((TollDue t) => !t.Affordable);
			if (tollDue2 != null)
			{
				verdict.Refusal = Refusal.TollUnpaid;
				verdict.Unaffordable = tollDue2;
				return verdict;
			}
			verdict.Allowed = true;
			return verdict;
		}

		internal static TollDue Price(BiomeTier tier, int unitsCarried)
		{
			int num = TollboundConfig.TollAmount(tier);
			string text = TollboundConfig.TollItem(tier);
			if (num <= 0 || string.IsNullOrEmpty(text))
			{
				return null;
			}
			int num2 = 1;
			if (TollboundConfig.Scaling.Value == TollScaling.PerLoad)
			{
				int num3 = Math.Max(1, TollboundConfig.LoadSize(tier));
				num2 = Math.Max(1, (unitsCarried + num3 - 1) / num3);
			}
			return new TollDue
			{
				Tier = tier,
				ItemPrefab = text,
				BaseAmount = num,
				Loads = num2,
				Amount = num * num2
			};
		}

		internal static List<Loss> Apply(Player player, Verdict verdict)
		{
			foreach (TollDue toll in verdict.Tolls)
			{
				Cargo.Remove(player, toll.ItemPrefab, toll.Amount);
			}
			List<Loss> list = new List<Loss>();
			foreach (CargoLot lot in verdict.Manifest.Lots)
			{
				bool flag = BossIsDead(lot.Tier);
				float num = TollboundConfig.LossRate(lot.Tier);
				bool flag2 = lot.Destructible && !flag && num > 0f;
				int num2 = (flag2 ? RollLosses(lot.Count, num) : 0);
				TollboundPlugin.LogVerbose($"Loss check {lot.Prefab} x{lot.Count} ({lot.Tier}): " + $"destructible={lot.Destructible}, bossDead={flag}, rate={num:0.###} " + "-> " + (flag2 ? (num2 + " lost") : "skipped"));
				if (num2 > 0)
				{
					int num3 = Cargo.Remove(player, lot.Prefab, num2);
					if (num3 > 0)
					{
						list.Add(new Loss
						{
							Tier = lot.Tier,
							Prefab = lot.Prefab,
							Count = num3
						});
					}
				}
			}
			return list;
		}

		private static int RollLosses(int units, float rate)
		{
			int num = 0;
			for (int i = 0; i < units; i++)
			{
				if (Random.value < rate)
				{
					num++;
				}
			}
			return num;
		}

		internal static bool BossIsDead(BiomeTier tier)
		{
			TierInfo tierInfo = Tiers.Get(tier);
			if (tierInfo == null || string.IsNullOrEmpty(tierInfo.BossKey) || (Object)(object)ZoneSystem.instance == (Object)null)
			{
				return false;
			}
			return ZoneSystem.instance.GetGlobalKey(tierInfo.BossKey);
		}
	}
}
namespace Tollbound.Gameplay.Portals
{
	internal static class TargetPortalBridge
	{
		internal const string Guid = "org.bepinex.plugins.targetportal";

		private static bool _bound;

		internal static bool MapCrossingInProgress { get; private set; }

		internal static bool Loaded { get; private set; }

		internal static void Bind(Harmony harmony)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			//IL_007b: Expected O, but got Unknown
			if (_bound)
			{
				return;
			}
			_bound = true;
			Type type = FindType("TargetPortal.Map");
			if (type == null)
			{
				return;
			}
			try
			{
				MethodInfo methodInfo = AccessTools.Method(type, "HandlePortalClick", (Type[])null, (Type[])null);
				if (methodInfo == null)
				{
					TollboundPlugin.LogWarning("TargetPortal is installed but HandlePortalClick was not found. Map travel will fall back to its own restrictions, which block all restricted cargo rather than applying Tollbound's rules.");
					return;
				}
				harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(TargetPortalBridge), "Open", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(TargetPortalBridge), "Close", (Type[])null), (HarmonyMethod)null);
				Loaded = true;
				TollboundPlugin.LogInfo("TargetPortal detected. Map travel from a biome portal is now tolled.");
			}
			catch (Exception ex)
			{
				TollboundPlugin.LogWarning("Could not hook TargetPortal (" + ex.Message + "). Map travel will fall back to its own restrictions.");
			}
		}

		private static void Open()
		{
			MapCrossingInProgress = true;
		}

		private static void Close()
		{
			MapCrossingInProgress = false;
		}

		private static Type FindType(string fullName)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					Type type = assembly.GetType(fullName, throwOnError: false);
					if (type != null)
					{
						return type;
					}
				}
				catch
				{
				}
			}
			return null;
		}
	}
}
namespace Tollbound.Gameplay.Backpacks
{
	internal static class BackpackBridge
	{
		internal const string AdventureBackpacksGuid = "vapok.mods.adventurebackpacks";

		internal const string BlaxxunBackpacksGuid = "org.bepinex.plugins.backpacks";

		private static bool _bound;

		private static MethodInfo _abIsBackpack;

		private static MethodInfo _abGetBackpack;

		private static MethodInfo _abGetEquipped;

		private static FieldInfo _abInventoryField;

		private static MethodInfo _bxInventories;

		private static MethodInfo _bxDelete;

		private static readonly Dictionary<string, string> TokenCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

		internal static bool AdventureBackpacksLoaded => _abGetEquipped != null;

		internal static bool BlaxxunBackpacksLoaded => _bxInventories != null;

		internal static bool AnyLoaded
		{
			get
			{
				if (!AdventureBackpacksLoaded)
				{
					return BlaxxunBackpacksLoaded;
				}
				return true;
			}
		}

		internal static void Bind()
		{
			if (_bound)
			{
				return;
			}
			_bound = true;
			BindAdventureBackpacks();
			BindBlaxxunBackpacks();
			if (!AnyLoaded)
			{
				TollboundPlugin.LogInfo("No backpack mod detected.");
				return;
			}
			List<string> list = new List<string>();
			if (AdventureBackpacksLoaded)
			{
				list.Add("AdventureBackpacks");
			}
			if (BlaxxunBackpacksLoaded)
			{
				list.Add("Backpacks");
			}
			TollboundPlugin.LogInfo("Backpack support active for: " + string.Join(", ", list.ToArray()) + ". Cargo in a pack is tolled and taxed like anything else.");
		}

		private static void BindAdventureBackpacks()
		{
			Type type = FindType("AdventureBackpacks.API.ABAPI");
			if (type == null)
			{
				return;
			}
			try
			{
				_abIsBackpack = type.GetMethod("IsBackpack", new Type[1] { typeof(ItemData) });
				_abGetBackpack = type.GetMethod("GetBackpack", new Type[1] { typeof(ItemData) });
				_abGetEquipped = type.GetMethod("GetEquippedBackpack", new Type[1] { typeof(Player) });
				if (_abIsBackpack == null || _abGetBackpack == null || _abGetEquipped == null)
				{
					Unbind("AdventureBackpacks", "expected API methods were not found");
				}
			}
			catch (Exception ex)
			{
				Unbind("AdventureBackpacks", ex.Message);
			}
		}

		private static void BindBlaxxunBackpacks()
		{
			Type type = FindType("Backpacks.API");
			if (type == null)
			{
				return;
			}
			try
			{
				_bxInventories = type.GetMethod("GetAllBackpackInventories", new Type[1] { typeof(Inventory) });
				_bxDelete = type.GetMethod("DeleteItemsFromBackpacks", new Type[3]
				{
					typeof(Inventory),
					typeof(string),
					typeof(int)
				});
				if (_bxInventories == null || _bxDelete == null)
				{
					Unbind("Backpacks", "expected API methods were not found");
				}
			}
			catch (Exception ex)
			{
				Unbind("Backpacks", ex.Message);
			}
		}

		private static void Unbind(string which, string why)
		{
			TollboundPlugin.LogWarning(which + " is installed but its API did not look as expected (" + why + "). Cargo inside its backpacks will NOT be tolled. Report this, it is a compatibility break rather than a config problem.");
			if (which == "AdventureBackpacks")
			{
				_abIsBackpack = (_abGetBackpack = (_abGetEquipped = null));
			}
			else
			{
				_bxInventories = (_bxDelete = null);
			}
		}

		private static List<Inventory> OpenInventories(Player player)
		{
			List<Inventory> list = new List<Inventory>();
			if ((Object)(object)player == (Object)null)
			{
				return list;
			}
			if (BlaxxunBackpacksLoaded)
			{
				try
				{
					if (_bxInventories.Invoke(null, new object[1] { ((Humanoid)player).GetInventory() }) is IEnumerable<Inventory> enumerable)
					{
						foreach (Inventory item in enumerable)
						{
							Add(list, item);
						}
					}
				}
				catch (Exception ex)
				{
					TollboundPlugin.LogVerbose("Backpacks read failed: " + ex.Message);
				}
			}
			if (!AdventureBackpacksLoaded)
			{
				return list;
			}
			try
			{
				Add(list, InventoryOf(_abGetEquipped.Invoke(null, new object[1] { player })));
				foreach (ItemData allItem in ((Humanoid)player).GetInventory().GetAllItems())
				{
					if (allItem != null && (bool)_abIsBackpack.Invoke(null, new object[1] { allItem }))
					{
						Add(list, InventoryOf(_abGetBackpack.Invoke(null, new object[1] { allItem })));
					}
				}
			}
			catch (Exception ex2)
			{
				TollboundPlugin.LogVerbose("AdventureBackpacks read failed: " + ex2.Message);
			}
			return list;
		}

		private static void Add(List<Inventory> list, Inventory inventory)
		{
			if (inventory != null && !list.Contains(inventory))
			{
				list.Add(inventory);
			}
		}

		private static Inventory InventoryOf(object backpack)
		{
			if (backpack == null)
			{
				return null;
			}
			if (_abInventoryField == null)
			{
				_abInventoryField = backpack.GetType().GetField("Inventory");
				if (_abInventoryField == null)
				{
					return null;
				}
			}
			object? value = _abInventoryField.GetValue(backpack);
			return (Inventory)((value is Inventory) ? value : null);
		}

		internal static void CollectCargo(Player player, Action<string, int> onCargo)
		{
			if (!AnyLoaded || (Object)(object)player == (Object)null)
			{
				return;
			}
			foreach (Inventory item in OpenInventories(player))
			{
				foreach (ItemData allItem in item.GetAllItems())
				{
					string text = PortalIdentity.PrefabNameOf(allItem);
					if (text != null)
					{
						onCargo(text, allItem.m_stack);
					}
				}
			}
		}

		internal static int Count(Player player, string prefabName)
		{
			if (!AnyLoaded || (Object)(object)player == (Object)null || string.IsNullOrEmpty(prefabName))
			{
				return 0;
			}
			return OpenInventories(player).Sum((Inventory inv) => (from i in inv.GetAllItems()
				where PortalIdentity.PrefabNameOf(i) == prefabName
				select i).Sum((ItemData i) => i.m_stack));
		}

		internal static int Remove(Player player, string prefabName, int amount)
		{
			if (!AnyLoaded || (Object)(object)player == (Object)null || amount <= 0)
			{
				return 0;
			}
			int num = 0;
			num += RemoveViaToken(player, prefabName, amount);
			foreach (Inventory item in OpenInventories(player))
			{
				foreach (ItemData item2 in item.GetAllItems().ToList())
				{
					if (num >= amount)
					{
						break;
					}
					if (!(PortalIdentity.PrefabNameOf(item2) != prefabName))
					{
						int num2 = Math.Min(item2.m_stack, amount - num);
						item.RemoveItem(item2, num2);
						num += num2;
					}
				}
			}
			return num;
		}

		private static int RemoveViaToken(Player player, string prefabName, int amount)
		{
			if (!BlaxxunBackpacksLoaded || amount <= 0)
			{
				return 0;
			}
			string text = TokenFor(prefabName);
			if (text == null)
			{
				return 0;
			}
			try
			{
				int num = Count(player, prefabName);
				_bxDelete.Invoke(null, new object[3]
				{
					((Humanoid)player).GetInventory(),
					text,
					amount
				});
				return Math.Max(0, num - Count(player, prefabName));
			}
			catch (Exception ex)
			{
				TollboundPlugin.LogVerbose("Backpacks delete failed: " + ex.Message);
				return 0;
			}
		}

		private static string TokenFor(string prefabName)
		{
			if (TokenCache.TryGetValue(prefabName, out var value))
			{
				return value;
			}
			string text = null;
			GameObject val = default(GameObject);
			if ((Object)(object)ObjectDB.instance != (Object)null && ObjectDB.instance.TryGetItemPrefab(prefabName, ref val) && (Object)(object)val != (Object)null)
			{
				text = val.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_name;
			}
			TokenCache[prefabName] = text;
			return text;
		}

		private static Type FindType(string fullName)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					Type type = assembly.GetType(fullName, throwOnError: false);
					if (type != null)
					{
						return type;
					}
				}
				catch
				{
				}
			}
			return null;
		}
	}
}
namespace Tollbound.Diagnostics
{
	internal static class ItemReport
	{
		private static readonly List<KeyValuePair<string, string[]>> Candidates = new List<KeyValuePair<string, string[]>>
		{
			new KeyValuePair<string, string[]>("Toll items", new string[6] { "GreydwarfEye", "Entrails", "FreezeGland", "Needle", "Bilebag", "CharredBone" }),
			new KeyValuePair<string, string[]>("Portal recipe - metal", new string[7] { "Copper", "Iron", "Silver", "BlackMetal", "Eitr", "Flametal", "FlametalNew" }),
			new KeyValuePair<string, string[]>("Portal recipe - frame", new string[8] { "FineWood", "YggdrasilWood", "BlackMarble", "Blackwood", "Grausten", "AncientBark", "Obsidian", "SurtlingCore" }),
			new KeyValuePair<string, string[]>("Restricted cargo - assumed", new string[10] { "CopperOre", "TinOre", "Tin", "Bronze", "IronOre", "IronScrap", "SilverOre", "BlackMetalScrap", "FlametalOre", "FlametalOreNew" }),
			new KeyValuePair<string, string[]>("Recipe candidates for the tweak pass", new string[11]
			{
				"BlackCore", "MoltenCore", "Wisp", "Sap", "Softtissue", "Carapace", "Thunderstone", "Crystal", "JuteRed", "AskHide",
				"ProustitePowder"
			}),
			new KeyValuePair<string, string[]>("Named in design, existence unconfirmed", new string[6] { "BronzeScrap", "CopperScrap", "DragonEgg", "MechanicalSpring", "CharredCogwheel", "DvergrKeyFragment" })
		};

		private static readonly string[] PortalPrefabs = new string[2] { "portal_wood", "portal_stone" };

		private static bool _hasRun;

		internal static void Run()
		{
			if (_hasRun)
			{
				return;
			}
			try
			{
				if ((Object)(object)ObjectDB.instance == (Object)null)
				{
					TollboundPlugin.LogWarning("ItemReport: ObjectDB not ready, skipping.");
					return;
				}
				string text = Write();
				_hasRun = true;
				TollboundPlugin.LogInfo("Item report written to " + text);
			}
			catch (Exception arg)
			{
				TollboundPlugin.LogError($"ItemReport failed: {arg}");
			}
		}

		private static string Write()
		{
			List<SharedData> list = new List<SharedData>();
			Dictionary<string, SharedData> dictionary = new Dictionary<string, SharedData>(StringComparer.OrdinalIgnoreCase);
			foreach (GameObject item in ObjectDB.instance.m_items)
			{
				if ((Object)(object)item == (Object)null)
				{
					continue;
				}
				ItemDrop component = item.GetComponent<ItemDrop>();
				if (!((Object)(object)component == (Object)null) && component.m_itemData?.m_shared != null)
				{
					SharedData shared = component.m_itemData.m_shared;
					dictionary[((Object)item).name] = shared;
					if (!shared.m_teleportable)
					{
						list.Add(shared);
					}
				}
			}
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# Tollbound item report");
			stringBuilder.AppendLine();
			stringBuilder.AppendLine($"Generated {DateTime.Now:yyyy-MM-dd HH:mm}  ");
			stringBuilder.AppendLine("Tollbound v0.10.1  ");
			stringBuilder.AppendLine($"{ObjectDB.instance.m_items.Count} item prefabs in ObjectDB, " + $"{list.Count} of them non-teleportable.");
			stringBuilder.AppendLine();
			AppendRestricted(stringBuilder, list, dictionary);
			AppendCandidates(stringBuilder, dictionary);
			AppendPortals(stringBuilder);
			string text = Path.Combine(Paths.ConfigPath, "Tollbound");
			Directory.CreateDirectory(text);
			string text2 = Path.Combine(text, "item-report.md");
			File.WriteAllText(text2, stringBuilder.ToString());
			return text2;
		}

		private static void AppendRestricted(StringBuilder sb, List<SharedData> restricted, Dictionary<string, SharedData> byPrefab)
		{
			sb.AppendLine("## Non-teleportable items");
			sb.AppendLine();
			sb.AppendLine("Everything vanilla (and any other loaded mod) marks `m_teleportable = false`. This is the candidate set for Tollbound's cargo tiers.");
			sb.AppendLine();
			sb.AppendLine("The last two columns are Tollbound's own classification, so a `none` tier here is an item that would cross a biome portal untolled.");
			sb.AppendLine();
			sb.AppendLine("| Prefab | Name | Type | Stack | Tier | Can be lost |");
			sb.AppendLine("|---|---|---|---|---|---|");
			Dictionary<SharedData, string> prefabOf = new Dictionary<SharedData, string>();
			foreach (KeyValuePair<string, SharedData> item in byPrefab)
			{
				prefabOf[item.Value] = item.Key;
			}
			foreach (var item2 in restricted.Select(delegate(SharedData s)
			{
				string value;
				string text = (prefabOf.TryGetValue(s, out value) ? value : "?");
				BiomeTier biomeTier = CargoRegistry.TierOfItem(text);
				return new
				{
					Prefab = text,
					Name = Localize(s.m_name),
					Type = ((object)Unsafe.As<ItemType, ItemType>(ref s.m_itemType)/*cast due to .constrained prefix*/).ToString(),
					Stack = s.m_maxStackSize,
					Tier = ((biomeTier == BiomeTier.None) ? "**none**" : biomeTier.ToString()),
					Lost = (CargoRegistry.CanBeLost(text) ? "yes" : "protected")
				};
			}).OrderBy(r => r.Prefab, StringComparer.OrdinalIgnoreCase))
			{
				sb.AppendLine($"| `{item2.Prefab}` | {item2.Name} | {item2.Type} | {item2.Stack} | " + item2.Tier + " | " + item2.Lost + " |");
			}
			sb.AppendLine();
		}

		private static void AppendCandidates(StringBuilder sb, Dictionary<string, SharedData> byPrefab)
		{
			sb.AppendLine("## Design assumptions");
			sb.AppendLine();
			sb.AppendLine("Prefab names the spec currently relies on. A `MISSING` row is a design decision that needs a different item.");
			sb.AppendLine();
			foreach (KeyValuePair<string, string[]> candidate in Candidates)
			{
				sb.AppendLine("### " + candidate.Key);
				sb.AppendLine();
				sb.AppendLine("| Prefab | Found | Name | Teleportable |");
				sb.AppendLine("|---|---|---|---|");
				string[] value = candidate.Value;
				foreach (string text in value)
				{
					if (byPrefab.TryGetValue(text, out var value2))
					{
						string text2 = (value2.m_teleportable ? "yes" : "**no**");
						sb.AppendLine("| `" + text + "` | ok | " + Localize(value2.m_name) + " | " + text2 + " |");
					}
					else
					{
						sb.AppendLine("| `" + text + "` | **MISSING** | — | — |");
					}
				}
				sb.AppendLine();
			}
		}

		private static void AppendPortals(StringBuilder sb)
		{
			//IL_0154: Unknown result type (might be due to invalid IL or missing references)
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			sb.AppendLine("## Vanilla portals");
			sb.AppendLine();
			sb.AppendLine("Tollbound clones `portal_wood` and must leave both of these alone. `m_allowAllItems` is the flag that decides whether vanilla runs its own teleport check at all.");
			sb.AppendLine();
			string[] portalPrefabs = PortalPrefabs;
			foreach (string text in portalPrefabs)
			{
				PrefabManager instance = PrefabManager.Instance;
				GameObject val = ((instance != null) ? instance.GetPrefab(text) : null);
				if ((Object)(object)val == (Object)null)
				{
					sb.AppendLine("- `" + text + "` — **MISSING**");
					continue;
				}
				TeleportWorld component = val.GetComponent<TeleportWorld>();
				if ((Object)(object)component == (Object)null)
				{
					sb.AppendLine("- `" + text + "` — found, but has no TeleportWorld component");
					continue;
				}
				Piece component2 = val.GetComponent<Piece>();
				string text2 = (((Object)(object)component2 != (Object)null) ? Localize(component2.m_name) : "(no Piece)");
				sb.AppendLine("- `" + text + "` — " + text2 + "  ");
				sb.AppendLine($"  - `m_allowAllItems` = **{component.m_allowAllItems}**");
				sb.AppendLine($"  - `m_activationRange` = {component.m_activationRange}, " + $"`m_exitDistance` = {component.m_exitDistance}");
				sb.AppendLine("  - unconnected " + ToHex(component.m_colorUnconnected) + ", target-found " + ToHex(component.m_colorTargetfound));
				sb.AppendLine("  - effect lists: connected " + $"{(component.m_connected?.m_effectPrefabs?.Length).GetValueOrDefault()}, " + "model " + (((Object)(object)component.m_model != (Object)null) ? "present" : "null"));
				if (component2?.m_resources != null)
				{
					string text3 = string.Join(", ", from r in component2.m_resources
						where (Object)(object)r?.m_resItem != (Object)null
						select $"{((Object)r.m_resItem).name} x{r.m_amount}");
					sb.AppendLine("  - build cost: " + text3);
				}
			}
			sb.AppendLine();
		}

		private static string Localize(string token)
		{
			if (string.IsNullOrEmpty(token))
			{
				return "";
			}
			if (Localization.instance == null)
			{
				return token;
			}
			return Localization.instance.Localize(token);
		}

		private static string ToHex(Color c)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			return $"#{ColorUtility.ToHtmlStringRGB(c)} (a={c.a:0.##}, hdr max={Mathf.Max(new float[3] { c.r, c.g, c.b }):0.##})";
		}
	}
}
namespace Tollbound.Content
{
	internal static class PortalPieces
	{
		private const string BasePortal = "portal_wood";

		private static bool _registered;

		internal static void Register()
		{
			if (_registered)
			{
				return;
			}
			if ((Object)(object)PrefabManager.Instance.GetPrefab("portal_wood") == (Object)null)
			{
				TollboundPlugin.LogError("'portal_wood' not found. No biome portals were created. This is a game-version problem, not a config one.");
				return;
			}
			int num = 0;
			TierInfo[] all = Tiers.All;
			for (int i = 0; i < all.Length; i++)
			{
				if (TryCreate(all[i]))
				{
					num++;
				}
			}
			_registered = true;
			TollboundPlugin.LogInfo($"Registered {num} of {Tiers.All.Length} biome portals.");
		}

		private static bool TryCreate(TierInfo tier)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or