Decompiled source of BeenThere v0.3.0

BeenThere.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("BeenThere")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.3.0.0")]
[assembly: AssemblyInformationalVersion("0.3.0+d20677791b35f1ca7b3ad5c47733190e39436f4d")]
[assembly: AssemblyProduct("BeenThere")]
[assembly: AssemblyTitle("BeenThere")]
[assembly: AssemblyVersion("0.3.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace BeenThere
{
	internal static class Attribution
	{
		public static string Current;

		public static string Source;

		public static string Push(HitData hit, GameObject source)
		{
			string current = Current;
			Current = PlayerName(hit);
			Source = Sources.PrefabName(source);
			return current;
		}

		public static string Push(string name, GameObject source)
		{
			string current = Current;
			Current = name;
			Source = Sources.PrefabName(source);
			return current;
		}

		public static void Pop(string prev)
		{
			Current = prev;
			if (prev == null)
			{
				Source = null;
			}
		}

		public static string PlayerName(HitData hit)
		{
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			if (hit == null || ((ZDOID)(ref hit.m_attacker)).IsNone())
			{
				return null;
			}
			Character attacker = hit.GetAttacker();
			Player val = (Player)(object)((attacker is Player) ? attacker : null);
			if (val != null)
			{
				return val.GetPlayerName();
			}
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return null;
			}
			foreach (PlayerInfo player in ZNet.instance.GetPlayerList())
			{
				if (player.m_characterID == hit.m_attacker)
				{
					return player.m_name;
				}
			}
			return null;
		}
	}
	internal static class Scale
	{
		private static float Rng()
		{
			return Random.value;
		}

		private static bool Stackable(GameObject prefab)
		{
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			ItemDrop val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent<ItemDrop>() : null);
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			SharedData shared = val.m_itemData.m_shared;
			if (shared.m_maxStackSize <= 1)
			{
				return false;
			}
			Game instance = Game.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				return !instance.m_nonScaledDropTypes.Contains(shared.m_itemType);
			}
			return true;
		}

		public static float Multiplier(GameObject prefab, bool creature = false)
		{
			Settings effective = Plugin.Effective;
			string name = ((Object)prefab).name;
			bool flag = Stackable(prefab);
			if (effective.Tiers.OfItem(name) == null && (effective.Scope != CatchUpScope.Everywhere || !flag))
			{
				return 1f;
			}
			string sourceTier = ((effective.Scope == CatchUpScope.Everywhere) ? Sources.TierOf(Attribution.Source) : null);
			string current = Attribution.Current;
			if (current != null)
			{
				return Rules.Multiplier(effective, Plugin.Record(current), name, sourceTier, creature, flag);
			}
			Player localPlayer = Player.m_localPlayer;
			return Rules.MultiplierNoAttacker(effective, name, (string key) => (Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.GetGlobalKey(key), ((Object)(object)localPlayer != (Object)null) ? Plugin.Record(localPlayer.GetPlayerName()) : null, sourceTier, creature, flag);
		}

		public static int Count(GameObject prefab, int count, bool creature = false)
		{
			float num = Multiplier(prefab, creature);
			if (num != 1f)
			{
				return Rules.Scale(count, num, Rng);
			}
			return count;
		}

		public static void DropList(List<GameObject> list)
		{
			if (list == null || list.Count == 0)
			{
				return;
			}
			Dictionary<string, float> dictionary = new Dictionary<string, float>();
			List<GameObject> list2 = new List<GameObject>();
			foreach (GameObject item in list)
			{
				if ((Object)(object)item == (Object)null)
				{
					continue;
				}
				if (!dictionary.TryGetValue(((Object)item).name, out var value))
				{
					value = (dictionary[((Object)item).name] = Multiplier(item));
				}
				if (value != 1f)
				{
					for (int num2 = Rules.Scale(1, value, Rng); num2 > 1; num2--)
					{
						list2.Add(item);
					}
				}
			}
			list.AddRange(list2);
		}
	}
	[HarmonyPatch]
	internal static class Patches
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__28_0;

			public static ConsoleOptionsFetcher <>9__28_1;

			internal void <Commands>b__28_0(ConsoleEventArgs args)
			{
				Terminal context = args.Context;
				string text = ((args.Length > 1) ? args[1].ToLowerInvariant() : "status");
				if (text == "catchup" && args.Length > 2)
				{
					Sync.RequestCatchUp(args[2]);
					context.AddString("BeenThere: asked the server for catch-up " + args[2]);
					return;
				}
				switch (text)
				{
				case "set":
				{
					if (!Rules.SplitPlayerAndTier(args.Args, 2, out var player2, out var tier2))
					{
						context.AddString("usage: beenthere set <character name> <tier|none>  (server admins only)");
						break;
					}
					Sync.RequestAdminCatchUp(player2, tier2);
					context.AddString("BeenThere: asked the server to set " + player2 + " catch-up " + tier2);
					break;
				}
				case "credit":
				case "uncredit":
				{
					if (!Rules.SplitPlayerAndTier(args.Args, 2, out var player, out var tier))
					{
						context.AddString("usage: beenthere " + text + " <character name> <tier>  (server admins only)");
						break;
					}
					Sync.RequestAdminCredit(player, tier, text == "credit");
					context.AddString("BeenThere: asked the server to " + text + " " + tier + " for " + player);
					break;
				}
				default:
					if (text != "status")
					{
						context.AddString("usage: beenthere status | beenthere catchup <tier|none> | beenthere set <character name> <tier|none> | beenthere credit|uncredit <character name> <tier>");
					}
					else
					{
						Status(context);
					}
					break;
				}
			}

			internal List<string> <Commands>b__28_1()
			{
				return new List<string> { "status", "catchup", "set", "credit", "uncredit" };
			}
		}

		private static readonly FieldRef<CharacterDrop, Character> dropCharacter = AccessTools.FieldRefAccess<CharacterDrop, Character>("m_character");

		private static readonly FieldRef<Character, HitData> lastHit = AccessTools.FieldRefAccess<Character, HitData>("m_lastHit");

		private static readonly List<KeyValuePair<GameObject, int>> pickExtras = new List<KeyValuePair<GameObject, int>>();

		private static bool spawningExtras;

		private static readonly MethodInfo pickableDrop = AccessTools.Method(typeof(Pickable), "Drop", (Type[])null, (Type[])null);

		[HarmonyPatch(typeof(ZNet), "Awake")]
		[HarmonyPostfix]
		private static void ZNetAwake(ZNet __instance)
		{
			Plugin.Snapshot = new Snapshot();
			Sync.Register();
			if (__instance.IsServer())
			{
				string path = Path.Combine(Path.Combine(Paths.ConfigPath, "BeenThere"), "state.txt");
				Plugin.Server = new Server(Plugin.LocalSettings, path, __instance.GetWorldName());
			}
		}

		[HarmonyPatch(typeof(ZNet), "OnDestroy")]
		[HarmonyPostfix]
		private static void ZNetDestroy()
		{
			Plugin.Server = null;
			Plugin.Snapshot = new Snapshot();
			Attribution.Current = null;
			Attribution.Source = null;
			Sources.Reset();
		}

		[HarmonyPatch(typeof(ZRoutedRpc), "AddPeer")]
		[HarmonyPostfix]
		private static void AddPeer(ZNetPeer peer)
		{
			if (Plugin.Server != null)
			{
				Plugin.Server.OnCharacterConnected(peer.m_playerName);
				Plugin.Server.Broadcast(peer.m_uid);
			}
		}

		[HarmonyPatch(typeof(TreeBase), "RPC_Damage")]
		[HarmonyPrefix]
		private static void TreeBasePre(TreeBase __instance, HitData hit, ref string __state)
		{
			__state = Attribution.Push(hit, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(TreeBase), "RPC_Damage")]
		[HarmonyPostfix]
		private static void TreeBasePost(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(TreeLog), "RPC_Damage")]
		[HarmonyPrefix]
		private static void TreeLogPre(TreeLog __instance, HitData hit, ref string __state)
		{
			__state = Attribution.Push(hit, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(TreeLog), "RPC_Damage")]
		[HarmonyPostfix]
		private static void TreeLogPost(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(MineRock), "RPC_Hit")]
		[HarmonyPrefix]
		private static void MineRockPre(MineRock __instance, HitData hit, ref string __state)
		{
			__state = Attribution.Push(hit, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(MineRock), "RPC_Hit")]
		[HarmonyPostfix]
		private static void MineRockPost(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(MineRock5), "DamageArea")]
		[HarmonyPrefix]
		private static void MineRock5Pre(MineRock5 __instance, HitData hit, ref string __state)
		{
			__state = Attribution.Push(hit, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(MineRock5), "DamageArea")]
		[HarmonyPostfix]
		private static void MineRock5Post(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(Destructible), "Destroy")]
		[HarmonyPrefix]
		private static void DestructiblePre(Destructible __instance, HitData hit, ref string __state)
		{
			__state = Attribution.Push(hit, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(Destructible), "Destroy")]
		[HarmonyPostfix]
		private static void DestructiblePost(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(WearNTear), "Destroy")]
		[HarmonyPrefix]
		private static void WearNTearPre(WearNTear __instance, HitData hitData, ref string __state)
		{
			__state = Attribution.Push(hitData, ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(WearNTear), "Destroy")]
		[HarmonyPostfix]
		private static void WearNTearPost(string __state)
		{
			Attribution.Pop(__state);
		}

		[HarmonyPatch(typeof(DropTable), "GetDropList", new Type[] { })]
		[HarmonyPostfix]
		private static void DropList(ref List<GameObject> __result)
		{
			try
			{
				Scale.DropList(__result);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("drop scaling failed, vanilla drops kept: " + ex));
			}
		}

		[HarmonyPatch(typeof(CharacterDrop), "GenerateDropList")]
		[HarmonyPostfix]
		private static void CreatureDrops(CharacterDrop __instance, ref List<KeyValuePair<GameObject, int>> __result)
		{
			Settings effective = Plugin.Effective;
			if ((!effective.CreatureDrops && effective.Scope != CatchUpScope.Everywhere) || __result == null)
			{
				return;
			}
			try
			{
				Character val = dropCharacter.Invoke(__instance);
				string prev = Attribution.Push(((Object)(object)val != (Object)null) ? lastHit.Invoke(val) : null, ((Object)(object)val != (Object)null) ? ((Component)val).gameObject : ((Component)__instance).gameObject);
				try
				{
					for (int i = 0; i < __result.Count; i++)
					{
						KeyValuePair<GameObject, int> keyValuePair = __result[i];
						if (!((Object)(object)keyValuePair.Key == (Object)null))
						{
							int num = Math.Min(100, Scale.Count(keyValuePair.Key, keyValuePair.Value, creature: true));
							if (num != keyValuePair.Value)
							{
								__result[i] = new KeyValuePair<GameObject, int>(keyValuePair.Key, num);
							}
						}
					}
				}
				finally
				{
					Attribution.Pop(prev);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("creature drop scaling failed, vanilla drops kept: " + ex));
			}
		}

		[HarmonyPatch(typeof(Pickable), "RPC_Pick")]
		[HarmonyPrefix]
		private static void PickPre(Pickable __instance, long sender, ref string __state)
		{
			pickExtras.Clear();
			__state = Attribution.Push(Sync.PlayerNameOfPeer(sender), ((Component)__instance).gameObject);
		}

		[HarmonyPatch(typeof(Pickable), "RPC_Pick")]
		[HarmonyPostfix]
		private static void PickPost(Pickable __instance, string __state)
		{
			try
			{
				spawningExtras = true;
				int num = 100;
				foreach (KeyValuePair<GameObject, int> pickExtra in pickExtras)
				{
					for (int i = 0; i < pickExtra.Value; i++)
					{
						pickableDrop.Invoke(__instance, new object[3]
						{
							pickExtra.Key,
							num++,
							1
						});
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("extra pickable drops failed: " + ex));
			}
			finally
			{
				spawningExtras = false;
				pickExtras.Clear();
				Attribution.Pop(__state);
			}
		}

		[HarmonyPatch(typeof(Pickable), "Drop")]
		[HarmonyPrefix]
		private static void PickDrop(GameObject prefab, ref int stack)
		{
			if ((Object)(object)prefab == (Object)null || spawningExtras)
			{
				return;
			}
			try
			{
				int num = Scale.Count(prefab, stack);
				if (num != stack)
				{
					ItemDrop component = prefab.GetComponent<ItemDrop>();
					if ((Object)(object)component != (Object)null && component.m_itemData.m_shared.m_maxStackSize <= 1)
					{
						pickExtras.Add(new KeyValuePair<GameObject, int>(prefab, num - stack));
					}
					else
					{
						stack = num;
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("pickable scaling failed, vanilla stack kept: " + ex));
			}
		}

		[HarmonyPatch(typeof(Character), "OnDeath")]
		[HarmonyPrefix]
		private static void BossDeath(Character __instance)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (__instance.IsBoss() && __instance.IsOwner())
				{
					ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
					List<string> attackers = BossRecord.Attackers(((Object)(object)component != (Object)null) ? component.GetZDO() : null, KnownNames());
					Sync.ReportBossDeath(Sources.PrefabName(((Component)__instance).gameObject), ((Component)__instance).transform.position, attackers);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("boss death report failed: " + ex));
			}
		}

		private static List<string> KnownNames()
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			List<string> list = new List<string>();
			WorldState worldState = ((Plugin.Server != null) ? Plugin.Server.State : Plugin.Snapshot.State);
			if (worldState != null)
			{
				list.AddRange(worldState.Players.Keys);
			}
			if ((Object)(object)ZNet.instance != (Object)null)
			{
				foreach (PlayerInfo player in ZNet.instance.GetPlayerList())
				{
					list.Add(player.m_name);
				}
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null)
			{
				list.Add(localPlayer.GetPlayerName());
			}
			return list;
		}

		[HarmonyPatch(typeof(ZoneSystem), "RPC_SetGlobalKey")]
		[HarmonyPrefix]
		private static void WorldKeySet(long sender, string name)
		{
			if (Plugin.Server == null)
			{
				return;
			}
			try
			{
				Plugin.Server.OnWorldKey(sender, name);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("world key credit failed: " + ex));
			}
		}

		[HarmonyPatch(typeof(Terminal), "InitTerminal")]
		[HarmonyPostfix]
		private static void Commands()
		{
			//IL_001e: 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_0029: Expected O, but got Unknown
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			object obj = <>c.<>9__28_0;
			if (obj == null)
			{
				ConsoleEvent val = delegate(ConsoleEventArgs args)
				{
					Terminal context = args.Context;
					string text = ((args.Length > 1) ? args[1].ToLowerInvariant() : "status");
					if (text == "catchup" && args.Length > 2)
					{
						Sync.RequestCatchUp(args[2]);
						context.AddString("BeenThere: asked the server for catch-up " + args[2]);
					}
					else
					{
						switch (text)
						{
						case "set":
						{
							if (!Rules.SplitPlayerAndTier(args.Args, 2, out var player2, out var tier2))
							{
								context.AddString("usage: beenthere set <character name> <tier|none>  (server admins only)");
							}
							else
							{
								Sync.RequestAdminCatchUp(player2, tier2);
								context.AddString("BeenThere: asked the server to set " + player2 + " catch-up " + tier2);
							}
							break;
						}
						case "credit":
						case "uncredit":
						{
							if (!Rules.SplitPlayerAndTier(args.Args, 2, out var player, out var tier))
							{
								context.AddString("usage: beenthere " + text + " <character name> <tier>  (server admins only)");
							}
							else
							{
								Sync.RequestAdminCredit(player, tier, text == "credit");
								context.AddString("BeenThere: asked the server to " + text + " " + tier + " for " + player);
							}
							break;
						}
						default:
							if (text != "status")
							{
								context.AddString("usage: beenthere status | beenthere catchup <tier|none> | beenthere set <character name> <tier|none> | beenthere credit|uncredit <character name> <tier>");
							}
							else
							{
								Status(context);
							}
							break;
						}
					}
				};
				<>c.<>9__28_0 = val;
				obj = (object)val;
			}
			object obj2 = <>c.<>9__28_1;
			if (obj2 == null)
			{
				ConsoleOptionsFetcher val2 = () => new List<string> { "status", "catchup", "set", "credit", "uncredit" };
				<>c.<>9__28_1 = val2;
				obj2 = (object)val2;
			}
			new ConsoleCommand("beenthere", "beenthere [status | catchup <tier|none> | set <character name> <tier|none> | credit <character name> <tier> | uncredit <character name> <tier>]: your BeenThere record, ask for a catch-up tier, or (admins) set anyone's catch-up or repair their kill record", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)obj2, false, false, false);
		}

		private static void Status(Terminal t)
		{
			Settings effective = Plugin.Effective;
			Player localPlayer = Player.m_localPlayer;
			string text = ((localPlayer != null) ? localPlayer.GetPlayerName() : null);
			PlayerRecord playerRecord = Plugin.Record(text);
			string text2 = Rules.EffectiveCatchUp(effective, playerRecord);
			t.AddString("BeenThere for " + (text ?? "(no player)") + ": tier rate " + effective.TierRate + "x, catch-up rate " + effective.CatchUpRate + "x, catch-up tier " + ((text2.Length == 0) ? "none" : text2) + ", catch-up scope " + effective.Scope.ToString() + ((effective.Scope == CatchUpScope.Everywhere) ? (" (excluded: " + (effective.ExcludeBossSummons ? "boss summons " : "") + (effective.ExcludeFarmables ? "farmables " : "") + (effective.ExcludeOceanClutter ? "ocean clutter" : "") + ")") : "") + ", kill credit " + effective.KillCredit);
			ZoneSystem instance = ZoneSystem.instance;
			using List<Tier>.Enumerator enumerator = effective.Tiers.Tiers.GetEnumerator();
			Tier current;
			float num;
			int num2;
			object obj;
			string text3;
			bool flag;
			for (; enumerator.MoveNext(); text3 = (string)obj, flag = num2 == 0 && current.WorldKey.Length > 0 && (Object)(object)instance != (Object)null && instance.GetGlobalKey(current.WorldKey), t.AddString("  " + current.Name + ": " + num + "x (" + text3 + (flag ? ", cleared in this world but not credited to you" : "") + ") " + string.Join(", ", current.Items)))
			{
				current = enumerator.Current;
				num = Rules.Multiplier(effective, playerRecord, (current.Items.Length != 0) ? current.Items[0] : "");
				if (playerRecord != null)
				{
					num2 = (playerRecord.Kills.Contains(current.Name) ? 1 : 0);
					if (num2 != 0)
					{
						obj = "killed";
						continue;
					}
				}
				else
				{
					num2 = 0;
				}
				obj = ((num > 1f) ? "catch-up" : "locked");
			}
		}
	}
	[BepInPlugin("syxe.BeenThere", "BeenThere", "0.3.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "syxe.BeenThere";

		public const string Name = "BeenThere";

		public const string Version = "0.3.0";

		public static ManualLogSource Log;

		public static Settings LocalSettings;

		internal static Server Server;

		internal static Snapshot Snapshot = new Snapshot();

		public static Settings Effective
		{
			get
			{
				Settings settings;
				if (Server == null)
				{
					settings = Snapshot.Settings;
					if (settings == null)
					{
						return LocalSettings;
					}
				}
				else
				{
					settings = Server.Settings;
				}
				return settings;
			}
		}

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			LocalSettings = Bind(((BaseUnityPlugin)this).Config);
			BindMyTier(((BaseUnityPlugin)this).Config);
			Harmony.CreateAndPatchAll(typeof(Patches), "syxe.BeenThere");
			Log.LogInfo((object)("BeenThere 0.3.0 loaded; tiers " + string.Join(",", LocalSettings.Tiers.Tiers.ConvertAll((Tier t) => t.Name))));
		}

		private void Update()
		{
			Server?.Poll(Time.unscaledTime);
		}

		public static PlayerRecord Record(string playerName)
		{
			if (playerName == null)
			{
				return null;
			}
			if (Server == null)
			{
				return Snapshot.State?.Get(playerName);
			}
			return Server.State.Get(playerName);
		}

		private static void BindMyTier(ConfigFile c)
		{
			ConfigEntry<string> entry = c.Bind<string>("CatchUp", "MyTier", "", "Your own catch-up tier request (a tier name, or none). Changing this in game asks the server, which applies its allowlist and cap. Does nothing on the server itself.");
			entry.SettingChanged += delegate
			{
				if (Server == null && !((Object)(object)ZNet.instance == (Object)null) && !((Object)(object)Player.m_localPlayer == (Object)null))
				{
					string text = (string.IsNullOrWhiteSpace(entry.Value) ? "none" : entry.Value.Trim());
					Sync.RequestCatchUp(text);
					Log.LogInfo((object)("MyTier changed, asked the server for catch-up " + text));
				}
			};
		}

		private static Settings Bind(ConfigFile c)
		{
			Settings settings = new Settings();
			settings.TierRate = c.Bind<float>("Rates", "TierRate", 3f, "Multiplier on a tier's items for a player who has killed that tier's boss.").Value;
			settings.CatchUpRate = c.Bind<float>("Rates", "CatchUpRate", 3f, "Multiplier on the items of every tier up to a player's catch-up tier, until they kill that tier's boss.").Value;
			settings.DefaultCatchUp = c.Bind<string>("CatchUp", "DefaultTier", "", "Catch-up tier for a character with no record yet; empty for none.").Value;
			settings.AllowedCatchUp = c.Bind<string>("CatchUp", "AllowedTiers", "", "Comma-separated tiers a player may pick with 'beenthere catchup'; empty allows every tier.").Value;
			settings.MaxCatchUp = c.Bind<string>("CatchUp", "MaxTier", "", "Highest tier a player may pick with 'beenthere catchup'; empty for no cap.").Value;
			settings.NoAttacker = c.Bind<NoAttackerRule>("Rules", "NoAttackerRule", NoAttackerRule.WorldKey, "Rate for drops with no player on the hit (chain-felled trees, collapsing ore, fire): WorldKey gives the tier rate once the world's boss key is set, ZoneOwner uses the local player's record, None gives 1x.").Value;
			settings.CreatureDrops = c.Bind<bool>("Rules", "CreatureDrops", false, "Also scale tier items dropped by creatures (black metal scrap from fulings).").Value;
			settings.KillCreditRadius = c.Bind<float>("Rules", "KillCreditRadius", 0f, "Under the Connected rule, credit a boss kill only to connected players within this many metres of the death; 0 credits every connected player.").Value;
			settings.KillCredit = c.Bind<KillCredit>("Rules", "KillCredit", KillCredit.Attackers, "Who a boss death credits. Attackers: every character that landed a hit on the boss, read from the boss's own record (the Connected rule when that record cannot be read). Connected: every connected character, within KillCreditRadius when set.").Value;
			settings.TierRateMetals = c.Bind<bool>("Rules", "TierRateMetals", true, "Ore and scrap count for the tier rate. Off keeps the tier rate to wood and stone; the catch-up still covers them.").Value;
			settings.Scope = c.Bind<CatchUpScope>("CatchUp", "Scope", CatchUpScope.Everywhere, "Everywhere: every stackable drop from a biome at or below the player's catch-up tier (minus the excluded categories). TableOnly: only the tier table's items.").Value;
			settings.ExcludeBossSummons = c.Bind<bool>("CatchUp", "ExcludeBossSummons", true, "Catch-up leaves boss summoning items at vanilla: " + string.Join(", ", Exclusions.BossSummons) + ".").Value;
			settings.ExcludeFarmables = c.Bind<bool>("CatchUp", "ExcludeFarmables", true, "Catch-up leaves planted crops and their seeds at vanilla: " + string.Join(", ", Exclusions.Farmables) + ".").Value;
			settings.ExcludeOceanClutter = c.Bind<bool>("CatchUp", "ExcludeOceanClutter", true, "Catch-up leaves ocean creature parts at vanilla: " + string.Join(", ", Exclusions.OceanClutter) + ".").Value;
			string value = c.Bind<string>("Tiers", "Order", "Meadows,BlackForest,Swamp,Mountain,Plains,Mistlands,Ashlands,DeepNorth", "Tier names from first to last biome. Each needs its own key in this section: bosses|world key|items.").Value;
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			string[] array = value.Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length != 0)
				{
					TierTable.DefaultSpecs.TryGetValue(text, out var value2);
					dictionary[text] = c.Bind<string>("Tiers", text, value2 ?? "", "boss prefab(s)|world key|item prefab(s), comma-separated within a field.").Value;
				}
			}
			string error;
			TierTable tierTable = TierTable.Parse(value, dictionary, out error);
			if (tierTable == null)
			{
				Log.LogError((object)("tier config rejected, using the built-in table: " + error));
				tierTable = TierTable.Default();
			}
			settings.Tiers = tierTable;
			return settings;
		}
	}
	public enum NoAttackerRule
	{
		WorldKey,
		ZoneOwner,
		None
	}
	public enum CatchUpScope
	{
		TableOnly,
		Everywhere
	}
	public enum KillCredit
	{
		Attackers,
		Connected
	}
	public static class Exclusions
	{
		public static readonly HashSet<string> Metals = Set("CopperOre", "TinOre", "IronScrap", "SilverOre", "BlackMetalScrap", "FlametalOreNew", "GoldOre");

		public static readonly HashSet<string> BossSummons = Set("TrophyDeer", "AncientSeed", "WitheredBone", "DragonEgg", "GoblinTotem", "TrophySeekerBrute", "DvergrKeyFragment", "BellFragment", "Bell", "HatefulBlood");

		public static readonly HashSet<string> Farmables = Set("Carrot", "CarrotSeeds", "Turnip", "TurnipSeeds", "Onion", "OnionSeeds", "Barley", "Flax", "MushroomJotunPuffs", "MushroomMagecap", "Kale", "KaleSeeds", "Poteitr", "PoteitrSeeds", "OatSeeds", "Vineberry", "VineberrySeeds", "VineGreenSeeds");

		public static readonly HashSet<string> OceanClutter = Set("Chitin", "SerpentScale", "BonemawSerpentScale");

		private static HashSet<string> Set(params string[] names)
		{
			return new HashSet<string>(names, StringComparer.OrdinalIgnoreCase);
		}

		public static bool IsExcluded(Settings s, string item)
		{
			if ((!s.ExcludeBossSummons || !BossSummons.Contains(item)) && (!s.ExcludeFarmables || !Farmables.Contains(item)))
			{
				if (s.ExcludeOceanClutter)
				{
					return OceanClutter.Contains(item);
				}
				return false;
			}
			return true;
		}
	}
	public sealed class Settings
	{
		public float TierRate = 3f;

		public float CatchUpRate = 3f;

		public string DefaultCatchUp = "";

		public string AllowedCatchUp = "";

		public string MaxCatchUp = "";

		public NoAttackerRule NoAttacker;

		public bool CreatureDrops;

		public float KillCreditRadius;

		public KillCredit KillCredit;

		public bool TierRateMetals = true;

		public CatchUpScope Scope = CatchUpScope.Everywhere;

		public bool ExcludeBossSummons = true;

		public bool ExcludeFarmables = true;

		public bool ExcludeOceanClutter = true;

		public TierTable Tiers = TierTable.Default();

		public IEnumerable<string> Lines()
		{
			yield return "tierrate=" + TierRate.ToString("R", CultureInfo.InvariantCulture);
			yield return "catchuprate=" + CatchUpRate.ToString("R", CultureInfo.InvariantCulture);
			yield return "defaultcatchup=" + DefaultCatchUp;
			yield return "allowedcatchup=" + AllowedCatchUp;
			yield return "maxcatchup=" + MaxCatchUp;
			yield return "noattacker=" + NoAttacker;
			yield return "creaturedrops=" + (CreatureDrops ? "true" : "false");
			yield return "killcreditradius=" + KillCreditRadius.ToString("R", CultureInfo.InvariantCulture);
			yield return "killcredit=" + KillCredit;
			yield return "tierratemetals=" + (TierRateMetals ? "true" : "false");
			yield return "scope=" + Scope;
			yield return "excludebosssummons=" + (ExcludeBossSummons ? "true" : "false");
			yield return "excludefarmables=" + (ExcludeFarmables ? "true" : "false");
			yield return "excludeoceanclutter=" + (ExcludeOceanClutter ? "true" : "false");
			foreach (string item in Tiers.Lines())
			{
				yield return "tier=" + item;
			}
		}

		public static Settings FromLines(IEnumerable<string> lines, out string error)
		{
			Settings settings = new Settings();
			List<string> list = new List<string>();
			foreach (string line in lines)
			{
				int num = line.IndexOf('=');
				if (num < 0)
				{
					error = "settings line without '=': " + line;
					return null;
				}
				string text = line.Substring(0, num).Trim().ToLowerInvariant();
				string text2 = line.Substring(num + 1).Trim();
				switch (text)
				{
				case "tierrate":
					if (!Float(text2, out settings.TierRate))
					{
						error = "bad tierrate";
						return null;
					}
					break;
				case "catchuprate":
					if (!Float(text2, out settings.CatchUpRate))
					{
						error = "bad catchuprate";
						return null;
					}
					break;
				case "defaultcatchup":
					settings.DefaultCatchUp = text2;
					break;
				case "allowedcatchup":
					settings.AllowedCatchUp = text2;
					break;
				case "maxcatchup":
					settings.MaxCatchUp = text2;
					break;
				case "noattacker":
					if (!Enum.TryParse<NoAttackerRule>(text2, ignoreCase: true, out settings.NoAttacker))
					{
						error = "bad noattacker: " + text2;
						return null;
					}
					break;
				case "creaturedrops":
					settings.CreatureDrops = text2.Equals("true", StringComparison.OrdinalIgnoreCase);
					break;
				case "killcreditradius":
					if (!Float(text2, out settings.KillCreditRadius))
					{
						error = "bad killcreditradius";
						return null;
					}
					break;
				case "killcredit":
					if (!Enum.TryParse<KillCredit>(text2, ignoreCase: true, out settings.KillCredit))
					{
						error = "bad killcredit: " + text2;
						return null;
					}
					break;
				case "tierratemetals":
					settings.TierRateMetals = Bool(text2);
					break;
				case "scope":
					if (!Enum.TryParse<CatchUpScope>(text2, ignoreCase: true, out settings.Scope))
					{
						error = "bad scope: " + text2;
						return null;
					}
					break;
				case "excludebosssummons":
					settings.ExcludeBossSummons = Bool(text2);
					break;
				case "excludefarmables":
					settings.ExcludeFarmables = Bool(text2);
					break;
				case "excludeoceanclutter":
					settings.ExcludeOceanClutter = Bool(text2);
					break;
				case "tier":
					list.Add(text2);
					break;
				}
			}
			settings.Tiers = TierTable.FromLines(list, out error);
			if (settings.Tiers != null)
			{
				return settings;
			}
			return null;
		}

		private static bool Bool(string text)
		{
			return text.Equals("true", StringComparison.OrdinalIgnoreCase);
		}

		private static bool Float(string text, out float value)
		{
			return float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
		}
	}
	public sealed class PlayerRecord
	{
		public string Name;

		public readonly HashSet<string> Kills = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		public string CatchUp;

		public PlayerRecord(string name)
		{
			Name = name;
		}
	}
	public static class Rules
	{
		public static float Multiplier(Settings s, PlayerRecord player, string item, string sourceTier = null, bool creature = false, bool stackable = true)
		{
			float num = 1f;
			Tier tier = s.Tiers.OfItem(item);
			if (tier != null && player != null && player.Kills.Contains(tier.Name) && (s.TierRateMetals || !Exclusions.Metals.Contains(item)) && (!creature || s.CreatureDrops))
			{
				num = Math.Max(num, s.TierRate);
			}
			Tier tier2 = s.Tiers.ByName(EffectiveCatchUp(s, player));
			if (tier2 != null && stackable && !Exclusions.IsExcluded(s, item))
			{
				Tier tier3 = tier;
				if (tier3 == null && s.Scope == CatchUpScope.Everywhere)
				{
					tier3 = s.Tiers.ByName(sourceTier);
				}
				bool flag = !creature || s.CreatureDrops || s.Scope == CatchUpScope.Everywhere;
				if (tier3 != null && flag && tier2.Index >= tier3.Index)
				{
					num = Math.Max(num, s.CatchUpRate);
				}
			}
			return num;
		}

		public static float MultiplierNoAttacker(Settings s, string item, Func<string, bool> worldKeySet, PlayerRecord zoneOwner, string sourceTier = null, bool creature = false, bool stackable = true)
		{
			switch (s.NoAttacker)
			{
			case NoAttackerRule.WorldKey:
			{
				Tier tier = s.Tiers.OfItem(item);
				if (tier == null || (creature && !s.CreatureDrops) || (!s.TierRateMetals && Exclusions.Metals.Contains(item)))
				{
					return 1f;
				}
				if (tier.WorldKey.Length <= 0 || worldKeySet == null || !worldKeySet(tier.WorldKey))
				{
					return 1f;
				}
				return s.TierRate;
			}
			case NoAttackerRule.ZoneOwner:
				if (zoneOwner != null)
				{
					return Multiplier(s, zoneOwner, item, sourceTier, creature, stackable);
				}
				return 1f;
			default:
				return 1f;
			}
		}

		public static string EffectiveCatchUp(Settings s, PlayerRecord player)
		{
			string text = player?.CatchUp ?? s.DefaultCatchUp;
			if (!IsNone(text))
			{
				return text;
			}
			return "";
		}

		public static bool CatchUpEndsWith(Settings s, PlayerRecord player, Tier credited)
		{
			Tier tier = s.Tiers.ByName(EffectiveCatchUp(s, player));
			if (tier != null && credited != null)
			{
				return tier.Index <= credited.Index;
			}
			return false;
		}

		public static List<string> Attackers(IEnumerable<string> names, Func<string, bool> flagged)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			List<string> list = new List<string>();
			foreach (string name in names)
			{
				if (!string.IsNullOrEmpty(name) && hashSet.Add(name) && flagged(name))
				{
					list.Add(name);
				}
			}
			return list;
		}

		public static bool IsNone(string tier)
		{
			if (!string.IsNullOrWhiteSpace(tier))
			{
				return tier.Equals("none", StringComparison.OrdinalIgnoreCase);
			}
			return true;
		}

		public static string ValidateCatchUp(Settings s, string requested, out string canonical)
		{
			canonical = "";
			if (IsNone(requested))
			{
				return null;
			}
			Tier tier = s.Tiers.ByName(requested.Trim());
			if (tier == null)
			{
				return "unknown tier " + requested + "; tiers are " + string.Join(", ", s.Tiers.Tiers.Select((Tier t) => t.Name));
			}
			if (!string.IsNullOrWhiteSpace(s.AllowedCatchUp) && !(from a in s.AllowedCatchUp.Split(',')
				select a.Trim() into a
				where a.Length > 0
				select a).Contains<string>(tier.Name, StringComparer.OrdinalIgnoreCase))
			{
				return tier.Name + " is not an allowed catch-up tier; allowed: " + s.AllowedCatchUp;
			}
			Tier tier2 = s.Tiers.ByName(s.MaxCatchUp);
			if (tier2 != null && tier.Index > tier2.Index)
			{
				return "catch-up is capped at " + tier2.Name;
			}
			canonical = tier.Name;
			return null;
		}

		public static string ValidateTier(Settings s, string requested, out string canonical)
		{
			canonical = "";
			if (IsNone(requested))
			{
				return null;
			}
			Tier tier = s.Tiers.ByName(requested.Trim());
			if (tier == null)
			{
				return "unknown tier " + requested + "; tiers are " + string.Join(", ", s.Tiers.Tiers.Select((Tier t) => t.Name));
			}
			canonical = tier.Name;
			return null;
		}

		public static bool SplitPlayerAndTier(string[] args, int start, out string player, out string tier)
		{
			player = (tier = null);
			if (args == null || args.Length < start + 2)
			{
				return false;
			}
			tier = args[^1];
			player = string.Join(" ", args, start, args.Length - 1 - start).Trim();
			return player.Length > 0;
		}

		public static int Scale(int count, float m, Func<float> rng)
		{
			if (count <= 0 || m <= 1f)
			{
				return count;
			}
			float num = (float)count * m;
			int num2 = (int)Math.Floor(num);
			float num3 = num - (float)num2;
			if (num3 > 0f && rng() < num3)
			{
				num2++;
			}
			return Math.Max(count, num2);
		}
	}
	internal static class Sources
	{
		private static Dictionary<string, int> tierIndex;

		private static TierTable builtFor;

		private static readonly Biome[] biomes;

		public static void Reset()
		{
			tierIndex = null;
			builtFor = null;
		}

		public static string TierOf(string prefab)
		{
			if (string.IsNullOrEmpty(prefab))
			{
				return null;
			}
			TierTable tiers = Plugin.Effective.Tiers;
			if (tierIndex == null || builtFor != tiers)
			{
				Build(tiers);
			}
			if (tierIndex == null)
			{
				return null;
			}
			if (!tierIndex.TryGetValue(prefab, out var value))
			{
				return null;
			}
			return tiers.Tiers[value].Name;
		}

		private static void Build(TierTable tiers)
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
			ZoneSystem instance = ZoneSystem.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
			int num = 0;
			foreach (ZoneVegetation item in instance.m_vegetation)
			{
				int num2 = Lowest(tiers, item.m_biome);
				if ((Object)(object)item.m_prefab != (Object)null && num2 >= 0)
				{
					num += Register(dictionary, item.m_prefab, num2, 0);
				}
			}
			SpawnSystem val = (((Object)(object)instance.m_zoneCtrlPrefab != (Object)null) ? instance.m_zoneCtrlPrefab.GetComponent<SpawnSystem>() : null);
			if ((Object)(object)val != (Object)null)
			{
				foreach (SpawnSystemList spawnList in val.m_spawnLists)
				{
					foreach (SpawnData spawner in spawnList.m_spawners)
					{
						int num3 = Lowest(tiers, spawner.m_biome);
						if ((Object)(object)spawner.m_prefab != (Object)null && num3 >= 0)
						{
							num += Register(dictionary, spawner.m_prefab, num3, 0);
						}
					}
				}
			}
			tierIndex = dictionary;
			builtFor = tiers;
			Plugin.Log.LogInfo((object)("source tiers built: " + dictionary.Count + " prefabs from " + instance.m_vegetation.Count + " vegetation entries" + (((Object)(object)val != (Object)null) ? (" and " + val.m_spawnLists.Count + " spawn lists") : ", no spawn lists")));
		}

		private static int Register(Dictionary<string, int> map, GameObject prefab, int idx, int depth)
		{
			if ((Object)(object)prefab == (Object)null || depth > 4)
			{
				return 0;
			}
			int num = 0;
			if (!map.TryGetValue(((Object)prefab).name, out var value) || idx < value)
			{
				map[((Object)prefab).name] = idx;
				num = 1;
			}
			TreeBase component = prefab.GetComponent<TreeBase>();
			if ((Object)(object)component != (Object)null)
			{
				num += Register(map, component.m_logPrefab, idx, depth + 1);
			}
			TreeLog component2 = prefab.GetComponent<TreeLog>();
			if ((Object)(object)component2 != (Object)null)
			{
				num += Register(map, component2.m_subLogPrefab, idx, depth + 1);
			}
			Destructible component3 = prefab.GetComponent<Destructible>();
			if ((Object)(object)component3 != (Object)null)
			{
				num += Register(map, component3.m_spawnWhenDestroyed, idx, depth + 1);
			}
			SpawnArea component4 = prefab.GetComponent<SpawnArea>();
			if ((Object)(object)component4 != (Object)null)
			{
				foreach (SpawnData prefab2 in component4.m_prefabs)
				{
					num += Register(map, prefab2.m_prefab, idx, depth + 1);
				}
			}
			return num;
		}

		private unsafe static int Lowest(TierTable tiers, Biome mask)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: 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_0012: Unknown result type (might be due to invalid IL or missing references)
			int num = -1;
			Biome[] array = biomes;
			for (int i = 0; i < array.Length; i++)
			{
				Biome val = array[i];
				if ((mask & val) != 0)
				{
					Tier tier = tiers.OfBiome(((object)(*(Biome*)(&val))/*cast due to .constrained prefix*/).ToString());
					if (tier != null && (num < 0 || tier.Index < num))
					{
						num = tier.Index;
					}
				}
			}
			return num;
		}

		public static string PrefabName(GameObject go)
		{
			if ((Object)(object)go == (Object)null)
			{
				return null;
			}
			string name = ((Object)go).name;
			int num = name.IndexOf("(Clone)", StringComparison.Ordinal);
			if (num <= 0)
			{
				return name;
			}
			return name.Substring(0, num);
		}

		static Sources()
		{
			Biome[] array = new Biome[9];
			RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
			biomes = (Biome[])(object)array;
		}
	}
	public sealed class WorldState
	{
		public string World = "";

		public readonly Dictionary<string, PlayerRecord> Players = new Dictionary<string, PlayerRecord>(StringComparer.OrdinalIgnoreCase);

		public PlayerRecord Get(string name)
		{
			if (name == null || !Players.TryGetValue(name, out var value))
			{
				return null;
			}
			return value;
		}

		public PlayerRecord GetOrAdd(string name)
		{
			PlayerRecord playerRecord = Get(name);
			if (playerRecord == null)
			{
				playerRecord = (Players[name] = new PlayerRecord(name));
			}
			return playerRecord;
		}

		public bool Credit(IEnumerable<string> names, string tier)
		{
			bool flag = false;
			foreach (string name in names)
			{
				if (ValidName(name))
				{
					flag |= GetOrAdd(name).Kills.Add(tier);
				}
			}
			return flag;
		}

		public bool Credit(IEnumerable<string> names, Tier tier, Settings s, out List<string> disarmed)
		{
			disarmed = new List<string>();
			bool flag = false;
			foreach (string name in names)
			{
				if (ValidName(name))
				{
					PlayerRecord orAdd = GetOrAdd(name);
					flag |= orAdd.Kills.Add(tier.Name);
					if (Rules.CatchUpEndsWith(s, orAdd, tier))
					{
						orAdd.CatchUp = "";
						disarmed.Add(orAdd.Name);
						flag = true;
					}
				}
			}
			return flag;
		}

		public bool Uncredit(string name, string tier)
		{
			return Get(name)?.Kills.Remove(tier) ?? false;
		}

		public bool SetCatchUp(string name, string tier)
		{
			if (!ValidName(name))
			{
				return false;
			}
			PlayerRecord orAdd = GetOrAdd(name);
			if (orAdd.CatchUp == tier)
			{
				return false;
			}
			orAdd.CatchUp = tier;
			return true;
		}

		public static bool ValidName(string name)
		{
			if (!string.IsNullOrWhiteSpace(name))
			{
				return name.IndexOfAny(new char[4] { ';', '=', '\n', '\r' }) < 0;
			}
			return false;
		}

		public string Serialize()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("# BeenThere state, one character per line. Edit while the server runs; it reloads on save.\n");
			stringBuilder.Append("# player=<character name>;kills=<tiers>;catchup=<tier|none>  (no catchup field: the server default applies)\n");
			stringBuilder.Append("world=").Append(World).Append('\n');
			foreach (PlayerRecord item in Players.Values.OrderBy<PlayerRecord, string>((PlayerRecord p) => p.Name, StringComparer.OrdinalIgnoreCase))
			{
				stringBuilder.Append("player=").Append(item.Name);
				stringBuilder.Append(";kills=").Append(string.Join(",", item.Kills.OrderBy<string, string>((string k) => k, StringComparer.OrdinalIgnoreCase)));
				if (item.CatchUp != null)
				{
					stringBuilder.Append(";catchup=").Append((item.CatchUp.Length == 0) ? "none" : item.CatchUp);
				}
				stringBuilder.Append('\n');
			}
			return stringBuilder.ToString();
		}

		public static WorldState Parse(string text, out string error)
		{
			WorldState worldState = new WorldState();
			string[] array = (text ?? "").Split('\n');
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length == 0 || text2[0] == '#')
				{
					continue;
				}
				string[] array2 = text2.Split(';');
				int num = array2[0].IndexOf('=');
				string text3 = ((num < 0) ? "" : array2[0].Substring(0, num).Trim().ToLowerInvariant());
				if (text3 == "world" && array2.Length == 1)
				{
					worldState.World = array2[0].Substring(num + 1).Trim();
					continue;
				}
				if (text3 != "player")
				{
					error = "unknown line: " + text2;
					return null;
				}
				PlayerRecord playerRecord = null;
				string[] array3 = array2;
				foreach (string text4 in array3)
				{
					int num2 = text4.IndexOf('=');
					if (num2 < 0)
					{
						error = "field without '=': " + text2;
						return null;
					}
					string text5 = text4.Substring(0, num2).Trim().ToLowerInvariant();
					string text6 = text4.Substring(num2 + 1).Trim();
					switch (text5)
					{
					case "player":
						if (!ValidName(text6) || worldState.Players.ContainsKey(text6))
						{
							error = "bad or repeated player name: " + text2;
							return null;
						}
						playerRecord = worldState.GetOrAdd(text6);
						break;
					case "kills":
					{
						string[] array4 = text6.Split(',');
						foreach (string text7 in array4)
						{
							if (text7.Trim().Length > 0)
							{
								playerRecord.Kills.Add(text7.Trim());
							}
						}
						break;
					}
					case "catchup":
						playerRecord.CatchUp = (Rules.IsNone(text6) ? "" : text6);
						break;
					default:
						error = "unknown field " + text5 + ": " + text2;
						return null;
					}
				}
			}
			error = null;
			return worldState;
		}
	}
	public static class Wire
	{
		public static string Pack(Settings settings, WorldState state)
		{
			StringBuilder stringBuilder = new StringBuilder("[settings]\n");
			foreach (string item in settings.Lines())
			{
				stringBuilder.Append(item).Append('\n');
			}
			stringBuilder.Append("[state]\n").Append(state.Serialize());
			return stringBuilder.ToString();
		}

		public static bool Unpack(string text, out Settings settings, out WorldState state, out string error)
		{
			settings = null;
			state = null;
			int num = text.IndexOf("[state]\n", StringComparison.Ordinal);
			if (!text.StartsWith("[settings]\n", StringComparison.Ordinal) || num < 0)
			{
				error = "not a BeenThere wire message";
				return false;
			}
			IEnumerable<string> lines = from l in text.Substring(11, num - 11).Split('\n')
				where l.Trim().Length > 0
				select l;
			settings = Settings.FromLines(lines, out error);
			if (settings == null)
			{
				return false;
			}
			state = WorldState.Parse(text.Substring(num + 8), out error);
			return state != null;
		}
	}
	internal sealed class Snapshot
	{
		public Settings Settings;

		public WorldState State;
	}
	internal sealed class Server
	{
		public readonly Settings Settings;

		public WorldState State;

		private readonly string path;

		private readonly string world;

		private DateTime loadedMtime;

		private float nextPoll;

		private readonly Dictionary<string, float> reported = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);

		private const float ReportWindow = 15f;

		public Server(Settings settings, string path, string world)
		{
			Settings = settings;
			this.path = path;
			this.world = world;
			Load();
		}

		private void Load()
		{
			State = null;
			if (File.Exists(path))
			{
				loadedMtime = File.GetLastWriteTimeUtc(path);
				State = WorldState.Parse(File.ReadAllText(path), out var error);
				if (State == null)
				{
					Plugin.Log.LogError((object)("ignoring " + path + ": " + error + "; starting empty (fix the file and it reloads)"));
				}
				else if (State.World != world)
				{
					Plugin.Log.LogWarning((object)(path + " is for world '" + State.World + "', this is '" + world + "'; parking it as .other and starting empty"));
					File.Copy(path, path + ".other", overwrite: true);
					State = null;
				}
			}
			if (State == null)
			{
				State = new WorldState
				{
					World = world
				};
				Save();
			}
			Plugin.Log.LogInfo((object)("state for " + world + ": " + State.Players.Count + " characters from " + path));
		}

		private void Save()
		{
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			File.WriteAllText(path, State.Serialize());
			loadedMtime = File.GetLastWriteTimeUtc(path);
		}

		public void Poll(float now)
		{
			if (now < nextPoll)
			{
				return;
			}
			nextPoll = now + 1f;
			try
			{
				if (File.Exists(path) && !(File.GetLastWriteTimeUtc(path) == loadedMtime))
				{
					DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path);
					string error;
					WorldState worldState = WorldState.Parse(File.ReadAllText(path), out error);
					loadedMtime = lastWriteTimeUtc;
					if (worldState == null)
					{
						Plugin.Log.LogWarning((object)("edited " + path + " ignored: " + error));
						return;
					}
					worldState.World = world;
					State = worldState;
					Plugin.Log.LogInfo((object)("state reloaded from disk: " + State.Players.Count + " characters"));
					Broadcast(0L);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("state poll failed: " + ex));
				nextPoll = now + 30f;
			}
		}

		public void OnCharacterConnected(string name)
		{
			if (!WorldState.ValidName(name))
			{
				return;
			}
			PlayerRecord record = State.Get(name);
			if (record == null)
			{
				State.GetOrAdd(name);
				Save();
				Plugin.Log.LogInfo((object)("new character " + name + " added to state"));
				return;
			}
			ZoneSystem zs = ZoneSystem.instance;
			if (!((Object)(object)zs == (Object)null))
			{
				List<string> list = (from t in Settings.Tiers.Tiers
					where t.WorldKey.Length > 0 && zs.GetGlobalKey(t.WorldKey) && !record.Kills.Contains(t.Name)
					select t.Name).ToList();
				if (list.Count > 0)
				{
					Plugin.Log.LogInfo((object)(name + " connected; cleared in this world but not credited to them: " + string.Join(", ", list) + " (beenthere credit, if they were there)"));
				}
			}
		}

		public void Broadcast(long target)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				ZPackage val = new ZPackage();
				val.Write(Wire.Pack(Settings, State));
				instance.InvokeRoutedRPC(target, "BeenThere_State", new object[1] { val });
			}
		}

		public void OnBossDied(string bossPrefab, Vector3 pos, List<string> attackers)
		{
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			Tier tier = Settings.Tiers.OfBoss(bossPrefab);
			if (tier == null)
			{
				Plugin.Log.LogInfo((object)(bossPrefab + " died; no tier lists it"));
				return;
			}
			reported[tier.Name] = Time.unscaledTime;
			if (Settings.KillCredit == KillCredit.Attackers && attackers != null)
			{
				Credit(tier, attackers, bossPrefab + " died (" + tier.Name + "); hit by");
				return;
			}
			Credit(tier, ConnectedPlayers(pos), bossPrefab + " died (" + tier.Name + "); connected" + ((attackers == null && Settings.KillCredit == KillCredit.Attackers) ? ", the report carried no hit record" : ""));
		}

		public void OnWorldKey(long sender, string key)
		{
			//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_00b0: 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)
			Tier tier = Settings.Tiers.OfWorldKey(key);
			if (tier == null || (reported.TryGetValue(tier.Name, out var value) && Time.unscaledTime - value < 15f))
			{
				return;
			}
			Vector3 pos = PeerPosition(sender);
			if (Settings.KillCredit == KillCredit.Attackers)
			{
				ZDO val = NearestBoss(tier, pos);
				if (val != null)
				{
					Credit(tier, BossRecord.Attackers(val, KnownNames()), key + " set (" + tier.Name + "); hit by");
					return;
				}
				Plugin.Log.LogWarning((object)(key + " set but no " + string.Join("/", tier.Bosses) + " record is loaded here; crediting connected players instead"));
			}
			Credit(tier, ConnectedPlayers(pos), key + " set (" + tier.Name + "); connected");
		}

		private void Credit(Tier tier, List<string> names, string how)
		{
			Plugin.Log.LogInfo((object)(how + ": " + ((names.Count == 0) ? "nobody" : string.Join(", ", names))));
			if (names.Count != 0 && State.Credit(names, tier, Settings, out var disarmed))
			{
				Save();
				Broadcast(0L);
				string text = "BeenThere: " + tier.Name + " credited to " + string.Join(", ", names);
				if (disarmed.Count > 0)
				{
					text = text + "; catch-up ended for " + string.Join(", ", disarmed);
				}
				Sync.Message(0L, text);
			}
		}

		private static ZDO NearestBoss(Tier tier, Vector3 pos)
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			ZDOMan instance = ZDOMan.instance;
			if (instance == null)
			{
				return null;
			}
			ZDO result = null;
			float num = float.MaxValue;
			List<ZDO> list = new List<ZDO>();
			string[] bosses = tier.Bosses;
			foreach (string text in bosses)
			{
				list.Clear();
				int num2 = 0;
				while (!instance.GetAllZDOsWithPrefabIterative(text, list, ref num2))
				{
				}
				foreach (ZDO item in list)
				{
					float num3 = Vector3.Distance(item.GetPosition(), pos);
					if (num3 < num)
					{
						result = item;
						num = num3;
					}
				}
			}
			return result;
		}

		private List<string> KnownNames()
		{
			List<string> list = new List<string>(State.Players.Keys);
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (!string.IsNullOrEmpty(peer.m_playerName) && !list.Contains(peer.m_playerName))
				{
					list.Add(peer.m_playerName);
				}
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null && !list.Contains(localPlayer.GetPlayerName()))
			{
				list.Add(localPlayer.GetPlayerName());
			}
			return list;
		}

		private static Vector3 PeerPosition(long sender)
		{
			//IL_003b: 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_001f: Unknown result type (might be due to invalid IL or missing references)
			if (sender == ZNet.GetUID() && (Object)(object)Player.m_localPlayer != (Object)null)
			{
				return ((Component)Player.m_localPlayer).transform.position;
			}
			return ZNet.instance.GetPeer(sender)?.m_refPos ?? Vector3.zero;
		}

		private List<string> ConnectedPlayers(Vector3 pos)
		{
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			float killCreditRadius = Settings.KillCreditRadius;
			List<string> list = new List<string>();
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer.IsReady() && !string.IsNullOrEmpty(peer.m_playerName) && (!(killCreditRadius > 0f) || !(Vector3.Distance(peer.m_refPos, pos) > killCreditRadius)))
				{
					list.Add(peer.m_playerName);
				}
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null && (killCreditRadius <= 0f || Vector3.Distance(((Component)localPlayer).transform.position, pos) <= killCreditRadius))
			{
				list.Add(localPlayer.GetPlayerName());
			}
			return list;
		}

		public void OnAdminCredit(long sender, string player, string requested, bool add)
		{
			if (!IsAdmin(sender))
			{
				Sync.Message(sender, "BeenThere: only a server admin can credit or uncredit a kill");
				return;
			}
			string canonical;
			string text = Rules.ValidateTier(Settings, requested, out canonical);
			if (text == null && canonical.Length == 0)
			{
				text = "name a tier to " + (add ? "credit" : "uncredit");
			}
			if (text == null && !WorldState.ValidName(player))
			{
				text = "bad character name";
			}
			if (text != null)
			{
				Sync.Message(sender, "BeenThere: " + text);
				return;
			}
			Tier tier = Settings.Tiers.ByName(canonical);
			List<string> disarmed = new List<string>();
			bool flag = (add ? State.Credit(new string[1] { player }, tier, Settings, out disarmed) : State.Uncredit(player, tier.Name));
			if (flag)
			{
				Save();
				Broadcast(0L);
			}
			Plugin.Log.LogInfo((object)("admin " + Sync.PlayerNameOfPeer(sender) + (add ? " credited " : " uncredited ") + tier.Name + " for " + player));
			Sync.Message(sender, "BeenThere: " + player + (add ? " credited with " : " no longer credited with ") + tier.Name + ((disarmed.Count > 0) ? ", catch-up ended" : "") + (flag ? "" : " (no change)"));
		}

		public void OnAdminSetCatchUp(long sender, string player, string requested)
		{
			if (!IsAdmin(sender))
			{
				Sync.Message(sender, "BeenThere: only a server admin can set another player's catch-up");
				return;
			}
			string canonical;
			string text = Rules.ValidateTier(Settings, requested, out canonical);
			if (text == null && !WorldState.ValidName(player))
			{
				text = "bad character name";
			}
			if (text != null)
			{
				Sync.Message(sender, "BeenThere: " + text);
				return;
			}
			if (State.SetCatchUp(player, canonical))
			{
				Save();
				Broadcast(0L);
			}
			Plugin.Log.LogInfo((object)("admin " + Sync.PlayerNameOfPeer(sender) + " set " + player + " catch-up to " + ((canonical.Length == 0) ? "none" : canonical)));
			Sync.Message(sender, "BeenThere: " + player + " catch-up " + ((canonical.Length == 0) ? "cleared" : ("set to " + canonical)));
		}

		private bool IsAdmin(long sender)
		{
			if (sender == ZNet.GetUID())
			{
				return true;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			if (peer != null && peer.m_socket != null)
			{
				return ZNet.instance.IsAdmin(peer.m_socket.GetHostName());
			}
			return false;
		}

		public void OnSetCatchUp(long sender, string requested)
		{
			string text = Sync.PlayerNameOfPeer(sender);
			if (string.IsNullOrEmpty(text))
			{
				return;
			}
			string canonical;
			string text2 = Rules.ValidateCatchUp(Settings, requested, out canonical);
			if (text2 != null)
			{
				Sync.Message(sender, "BeenThere: " + text2);
				return;
			}
			if (State.SetCatchUp(text, canonical))
			{
				Save();
				Broadcast(0L);
			}
			Plugin.Log.LogInfo((object)(text + " set catch-up to " + ((canonical.Length == 0) ? "none" : canonical)));
			Sync.Message(sender, "BeenThere: catch-up " + ((canonical.Length == 0) ? "cleared" : ("set to " + canonical)));
		}
	}
	internal static class BossRecord
	{
		public static List<string> Attackers(ZDO zdo, IEnumerable<string> names)
		{
			if (zdo == null)
			{
				return null;
			}
			return Rules.Attackers(names, (string name) => zdo.GetBool(ZDOVars.s_attackers + name, false));
		}
	}
	internal static class Sync
	{
		public const string StateRpc = "BeenThere_State";

		public const string BossRpc = "BeenThere_BossDied";

		public const string BossRpc2 = "BeenThere_BossDied2";

		public const string CatchUpRpc = "BeenThere_SetCatchUp";

		public const string AdminCatchUpRpc = "BeenThere_AdminSetCatchUp";

		public const string AdminCreditRpc = "BeenThere_AdminCredit";

		private const char NameSeparator = ';';

		public static void Register()
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			instance.Register<ZPackage>("BeenThere_State", (Action<long, ZPackage>)OnState);
			instance.Register<string, Vector3>("BeenThere_BossDied", (Action<long, string, Vector3>)OnBossDied);
			instance.Register<string, Vector3, string>("BeenThere_BossDied2", (Action<long, string, Vector3, string>)OnBossDied2);
			instance.Register<string>("BeenThere_SetCatchUp", (Action<long, string>)OnSetCatchUp);
			instance.Register<string, string>("BeenThere_AdminSetCatchUp", (Action<long, string, string>)OnAdminSetCatchUp);
			instance.Register<string, string, bool>("BeenThere_AdminCredit", (Action<long, string, string, bool>)OnAdminCredit);
		}

		private static void OnAdminSetCatchUp(long sender, string player, string tier)
		{
			Plugin.Server?.OnAdminSetCatchUp(sender, player, tier);
		}

		public static void RequestAdminCatchUp(string player, string tier)
		{
			if (Plugin.Server != null)
			{
				Plugin.Server.OnAdminSetCatchUp(ZNet.GetUID(), player, tier);
				return;
			}
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC("BeenThere_AdminSetCatchUp", new object[2] { player, tier });
			}
		}

		private static void OnAdminCredit(long sender, string player, string tier, bool add)
		{
			Plugin.Server?.OnAdminCredit(sender, player, tier, add);
		}

		public static void RequestAdminCredit(string player, string tier, bool add)
		{
			if (Plugin.Server != null)
			{
				Plugin.Server.OnAdminCredit(ZNet.GetUID(), player, tier, add);
				return;
			}
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC("BeenThere_AdminCredit", new object[3] { player, tier, add });
			}
		}

		private static void OnState(long sender, ZPackage pkg)
		{
			if (Plugin.Server == null)
			{
				if (!Wire.Unpack(pkg.ReadString(), out var settings, out var state, out var error))
				{
					Plugin.Log.LogWarning((object)("server state rejected: " + error));
					return;
				}
				Plugin.Snapshot = new Snapshot
				{
					Settings = settings,
					State = state
				};
				Plugin.Log.LogInfo((object)("server state received: " + state.Players.Count + " characters, tier rate " + settings.TierRate + ", catch-up rate " + settings.CatchUpRate));
			}
		}

		private static void OnBossDied(long sender, string prefab, Vector3 pos)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Server?.OnBossDied(prefab, pos, null);
		}

		private static void OnBossDied2(long sender, string prefab, Vector3 pos, string attackers)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Server?.OnBossDied(prefab, pos, SplitNames(attackers));
		}

		private static void OnSetCatchUp(long sender, string tier)
		{
			Plugin.Server?.OnSetCatchUp(sender, tier);
		}

		public static void ReportBossDeath(string prefab, Vector3 pos, List<string> attackers)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: 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)
			if (Plugin.Server != null)
			{
				Plugin.Server.OnBossDied(prefab, pos, attackers);
				return;
			}
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				if (attackers == null)
				{
					instance.InvokeRoutedRPC("BeenThere_BossDied", new object[2] { prefab, pos });
				}
				else
				{
					instance.InvokeRoutedRPC("BeenThere_BossDied2", new object[3]
					{
						prefab,
						pos,
						string.Join(';'.ToString(), attackers)
					});
				}
			}
		}

		private static List<string> SplitNames(string joined)
		{
			if (!string.IsNullOrEmpty(joined))
			{
				return new List<string>(joined.Split(';'));
			}
			return new List<string>();
		}

		public static void RequestCatchUp(string tier)
		{
			if (Plugin.Server != null)
			{
				Plugin.Server.OnSetCatchUp(ZNet.GetUID(), tier);
				return;
			}
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC("BeenThere_SetCatchUp", new object[1] { tier });
			}
		}

		public static void Message(long target, string text)
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC(target, "ShowMessage", new object[2] { 1, text });
			}
		}

		public static string PlayerNameOfPeer(long uid)
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return null;
			}
			if (uid == ZNet.GetUID())
			{
				Player localPlayer = Player.m_localPlayer;
				if (localPlayer == null)
				{
					return null;
				}
				return localPlayer.GetPlayerName();
			}
			return ZNet.instance.GetPeer(uid)?.m_playerName;
		}
	}
	public sealed class Tier
	{
		public string Name;

		public int Index;

		public string[] Bosses;

		public string WorldKey;

		public string[] Items;
	}
	public sealed class TierTable
	{
		public const string DefaultOrder = "Meadows,BlackForest,Swamp,Mountain,Plains,Mistlands,Ashlands,DeepNorth";

		public static readonly Dictionary<string, string> DefaultSpecs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
		{
			["Meadows"] = "Eikthyr|defeated_eikthyr|Wood",
			["BlackForest"] = "gd_king|defeated_gdking|FineWood,RoundLog,Stone,CopperOre,TinOre",
			["Swamp"] = "Bonemass|defeated_bonemass|ElderBark,IronScrap",
			["Mountain"] = "Dragon|defeated_dragon|SilverOre,Crystal,WolfClaw",
			["Plains"] = "GoblinKing|defeated_goblinking|BlackMetalScrap,Tar",
			["Mistlands"] = "SeekerQueen|defeated_queen|BlackMarble,YggdrasilWood,Softtissue,BlackCore,Lantern",
			["Ashlands"] = "Fader|defeated_fader|Grausten,FlametalOreNew,Blackwood",
			["DeepNorth"] = "FrozenKing_p3|defeated_frozenking_p3|Frostwood,GoldOre"
		};

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

		private readonly Dictionary<string, Tier> byName = new Dictionary<string, Tier>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, Tier> byItem = new Dictionary<string, Tier>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, Tier> byBoss = new Dictionary<string, Tier>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, Tier> byWorldKey = new Dictionary<string, Tier>(StringComparer.OrdinalIgnoreCase);

		public static TierTable Default()
		{
			string error;
			return Parse("Meadows,BlackForest,Swamp,Mountain,Plains,Mistlands,Ashlands,DeepNorth", DefaultSpecs, out error);
		}

		public static TierTable Parse(string order, IDictionary<string, string> specs, out string error)
		{
			List<string> list = new List<string>();
			string[] array = (order ?? "").Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length != 0)
				{
					if (specs == null || !specs.TryGetValue(text, out var value) || string.IsNullOrWhiteSpace(value))
					{
						error = "tier " + text + " is in the order but has no spec";
						return null;
					}
					list.Add(text + "|" + value);
				}
			}
			return FromLines(list, out error);
		}

		public static TierTable FromLines(IEnumerable<string> lines, out string error)
		{
			TierTable tierTable = new TierTable();
			foreach (string line in lines)
			{
				string[] array = line.Split('|');
				if (array.Length != 4)
				{
					error = "tier line needs Name|bosses|key|items: " + line;
					return null;
				}
				Tier tier = new Tier
				{
					Name = array[0].Trim(),
					Index = tierTable.Tiers.Count,
					Bosses = Split(array[1]),
					WorldKey = array[2].Trim(),
					Items = Split(array[3])
				};
				if (tier.Name.Length == 0 || tier.Name.IndexOfAny(new char[4] { ',', ';', '|', '=' }) >= 0)
				{
					error = "bad tier name in: " + line;
					return null;
				}
				if (tierTable.byName.ContainsKey(tier.Name))
				{
					error = "tier " + tier.Name + " listed twice";
					return null;
				}
				string[] items = tier.Items;
				foreach (string text in items)
				{
					if (tierTable.byItem.TryGetValue(text, out var value))
					{
						error = "item " + text + " is in both " + value.Name + " and " + tier.Name;
						return null;
					}
				}
				tierTable.Tiers.Add(tier);
				tierTable.byName[tier.Name] = tier;
				items = tier.Items;
				foreach (string key in items)
				{
					tierTable.byItem[key] = tier;
				}
				items = tier.Bosses;
				foreach (string key2 in items)
				{
					tierTable.byBoss[key2] = tier;
				}
				if (tier.WorldKey.Length > 0)
				{
					tierTable.byWorldKey[tier.WorldKey] = tier;
				}
			}
			if (tierTable.Tiers.Count == 0)
			{
				error = "no tiers";
				return null;
			}
			error = null;
			return tierTable;
		}

		private static string[] Split(string field)
		{
			return (from s in field.Split(',')
				select s.Trim() into s
				where s.Length > 0
				select s).ToArray();
		}

		public Tier ByName(string name)
		{
			if (name == null || !byName.TryGetValue(name, out var value))
			{
				return null;
			}
			return value;
		}

		public Tier OfItem(string prefab)
		{
			if (prefab == null || !byItem.TryGetValue(prefab, out var value))
			{
				return null;
			}
			return value;
		}

		public Tier OfBoss(string prefab)
		{
			if (prefab == null || !byBoss.TryGetValue(prefab, out var value))
			{
				return null;
			}
			return value;
		}

		public Tier OfWorldKey(string key)
		{
			if (key == null || !byWorldKey.TryGetValue(key, out var value))
			{
				return null;
			}
			return value;
		}

		public Tier OfBiome(string biome)
		{
			if (string.IsNullOrEmpty(biome))
			{
				return null;
			}
			if (biome.Equals("Ocean", StringComparison.OrdinalIgnoreCase))
			{
				if (Tiers.Count <= 0)
				{
					return null;
				}
				return Tiers[0];
			}
			return ByName(biome);
		}

		public IEnumerable<string> Lines()
		{
			foreach (Tier tier in Tiers)
			{
				yield return tier.Name + "|" + string.Join(",", tier.Bosses) + "|" + tier.WorldKey + "|" + string.Join(",", tier.Items);
			}
		}
	}
}