Decompiled source of FishermansFixes v1.0.1

BepInEx/plugins/dannanboi_FishermansFixes.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("FishermansFixes")]
[assembly: AssemblyDescription("Configurable fishing bait, progression, Fishing skill requirements, rod recipes and treasure removal.")]
[assembly: AssemblyCompany("dannanboi")]
[assembly: AssemblyProduct("FishermansFixes")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyVersion("1.0.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 FishermansFixes
{
	[BepInPlugin("dannanboi.fishermansfixes", "FishermansFixes", "1.0.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(ZNet), "Awake")]
		private static class NetworkAwake
		{
			private static void Postfix(ZNet __instance)
			{
				Instance?.BeginNetwork(__instance);
			}
		}

		[HarmonyPatch(typeof(ZNet), "Shutdown")]
		private static class NetworkShutdown
		{
			private static void Postfix(ZNet __instance)
			{
				if ((Object)(object)Instance != (Object)null && Instance.network == __instance)
				{
					Instance.BeginNetwork(null);
				}
			}
		}

		private sealed class Connection
		{
			internal ZNetPeer Peer;

			internal string Version;

			internal float StartedAt;

			internal bool Rejected;

			internal float CloseAt;
		}

		[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
		private static class NewConnection
		{
			[HarmonyPriority(800)]
			private static void Prefix(ZNet __instance, ZNetPeer peer)
			{
				Instance?.RegisterConnection(__instance, peer);
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		private static class PeerInfo
		{
			[HarmonyPriority(800)]
			private static bool Prefix(ZRpc rpc)
			{
				if (!((Object)(object)Instance == (Object)null))
				{
					return Instance.GatePeerInfo(rpc);
				}
				return true;
			}
		}

		[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
		private static class ShowError
		{
			private static void Postfix(FejdStartup __instance)
			{
				//IL_0007: Unknown result type (might be due to invalid IL or missing references)
				//IL_000d: Invalid comparison between Unknown and I4
				if (connectionError != null && (int)ZNet.GetConnectionStatus() == 3)
				{
					object? value = ErrorText.GetValue(__instance);
					TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null);
					if ((Object)(object)val != (Object)null)
					{
						val.text = connectionError;
					}
				}
			}
		}

		public const string Guid = "dannanboi.fishermansfixes";

		public const string ModName = "FishermansFixes";

		public const string PluginVersion = "1.0.0";

		public const string ModVersion = "1.0.0";

		internal static Plugin Instance;

		private LiveConfig settings;

		private Harmony core;

		private SectionPatches sections;

		private RecipeRegistry recipes;

		private ZNet network;

		private bool dirty;

		private int refresh;

		private float nextPublish;

		private float nextPeerCheck;

		private float nextYamlPoll;

		private FileInfo yamlInfo;

		private DateTime yamlWrite;

		private long yamlLength;

		private bool yamlObserved;

		private bool yamlPending;

		private float yamlReloadAt;

		private string yamlError;

		private byte[] settingsPacket;

		private Settings encodedSettings;

		private const string HelloRpc = "dannanboi.fishermansfixes.Hello";

		private const string SettingsRpc = "dannanboi.fishermansfixes.Settings";

		private const string RejectedRpc = "dannanboi.fishermansfixes.Rejected";

		private const int Protocol = 1;

		private static string connectionError;

		private static readonly FieldInfo ConnectionStatus = AccessTools.Field(typeof(ZNet), "m_connectionStatus");

		private static readonly FieldInfo ErrorText = AccessTools.Field(typeof(FejdStartup), "m_connectionFailedError");

		private readonly Dictionary<ZRpc, Connection> connections = new Dictionary<ZRpc, Connection>();

		private readonly List<ZRpc> removedConnections = new List<ZRpc>();

		private bool authority => !settings.Client;

		private bool settingsReady => settings.Ready;

		private void Awake()
		{
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Expected O, but got Unknown
			Instance = this;
			settings = new LiveConfig(((BaseUnityPlugin)this).Config);
			recipes = new RecipeRegistry();
			settings.Refresh += QueueRefresh;
			settings.AuthorityChanged += Changed;
			core = new Harmony("dannanboi.fishermansfixes");
			Type[] array = new Type[2]
			{
				typeof(SaveLocalPreferences),
				typeof(ReloadLocalPreferences)
			};
			foreach (Type type in array)
			{
				core.CreateClassProcessor(type).Patch();
			}
			array = typeof(Plugin).GetNestedTypes(BindingFlags.NonPublic);
			foreach (Type type2 in array)
			{
				if (Attribute.IsDefined(type2, typeof(HarmonyPatch)))
				{
					core.CreateClassProcessor(type2).Patch();
				}
			}
			sections = new SectionPatches();
			BeginNetwork(ZNet.instance);
			QueueRefresh();
			Refresh();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"FishermansFixes 1.0.0 loaded.");
		}

		internal static void Warn(string message)
		{
			Plugin instance = Instance;
			if (instance != null)
			{
				((BaseUnityPlugin)instance).Logger.LogWarning((object)message);
			}
		}

		private void Changed()
		{
			dirty = true;
			QueueRefresh();
		}

		private void QueueRefresh()
		{
			Interlocked.Exchange(ref refresh, 1);
		}

		internal void Database(ObjectDB db)
		{
			recipes.Register(db);
			recipes.Apply(settings.Current, settingsReady);
		}

		private static void Unregister(ZRpc rpc)
		{
			rpc.Unregister("dannanboi.fishermansfixes.Hello");
			rpc.Unregister("dannanboi.fishermansfixes.Settings");
			rpc.Unregister("dannanboi.fishermansfixes.Rejected");
		}

		private void BeginNetwork(ZNet value)
		{
			if (network == value)
			{
				return;
			}
			foreach (ZRpc key in connections.Keys)
			{
				Unregister(key);
			}
			connections.Clear();
			network = value;
			connectionError = null;
			dirty = false;
			settingsPacket = null;
			encodedSettings = null;
			nextPublish = (nextPeerCheck = 0f);
			settings.SetClient(client: false);
			settings.SetClient((Object)(object)value != (Object)null && !value.IsServer());
			yamlInfo = null;
			yamlObserved = false;
			QueueRefresh();
			BaitRules.Configure(settings.Current, ready: false);
			recipes.Apply(settings.Current, ready: false);
		}

		private void Refresh()
		{
			if (authority && settings.Current.Bait)
			{
				if (yamlInfo == null)
				{
					string text = Path.Combine(Paths.ConfigPath, "FishermansFixes");
					Directory.CreateDirectory(text);
					string text2 = Path.Combine(text, "baits.yaml");
					if (!File.Exists(text2))
					{
						File.WriteAllText(text2, "# FishermansFixes\n# Server/host controls this file in multiplayer; reloads automatically while Bait is enabled.\n# Top-level keys and fish lists use case-sensitive console prefab names.\n# Section headings: 2 spaces. Dashed rows: 4 spaces, matching Food Knowledge style.\n# levelUnlock: Fishing skill 0-100; 0 is unrestricted. Temporary bonuses count.\n# recipe: \"\" keeps native recipes. Otherwise use Item1[amount],Item2[amount].\n# Existing recipes retain their station, station level and output amount.\n# A bait without a native recipe gets 20 bait per craft at a level 1 Workbench.\n# Lower Tier Progression order: Meadows, BlackForest, Swamp, Ocean, Mountain,\n# Plains, Mistlands, Ashlands, DeepNorth. Lists are inherited from lower biomes.\n# Custom bait must already exist in the game; specify biome to include it in progression.\n# An omitted bait or omitted fish section uses native fish rules. fish: [] disallows direct catches (lower tiers still apply).\n\nFishingBait:\n  basics:\n    - biome: Meadows\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish1\n    - Fish2\n\nFishingBaitForest:\n  basics:\n    - biome: BlackForest\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish5\n\nFishingBaitSwamp:\n  basics:\n    - biome: Swamp\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish6\n\nFishingBaitOcean:\n  basics:\n    - biome: Ocean\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish3\n    - Fish8\n\nFishingBaitCave:\n  basics:\n    - biome: Mountain\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish4\n    - Fish4_cave\n\nFishingBaitPlains:\n  basics:\n    - biome: Plains\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish7\n\nFishingBaitMistlands:\n  basics:\n    - biome: Mistlands\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish9\n    - Fish12\n\nFishingBaitAshlands:\n  basics:\n    - biome: Ashlands\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish11\n\nFishingBaitDeepNorth:\n  basics:\n    - biome: DeepNorth\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish10\n\n");
					}
					yamlInfo = new FileInfo(text2);
					LoadYaml();
				}
			}
			else
			{
				yamlInfo = null;
				yamlObserved = (yamlPending = false);
			}
			Settings current = settings.Current;
			BaitRules.Configure(current, settingsReady);
			sections.Refresh(current, settingsReady);
			recipes.Register(ObjectDB.instance);
			recipes.Apply(current, settingsReady);
			Interlocked.Exchange(ref refresh, 0);
		}

		private void LoadYaml()
		{
			try
			{
				yamlInfo.Refresh();
				if (yamlInfo.Exists)
				{
					if (yamlInfo.Length > 131072)
					{
						throw new FormatException("baits.yaml exceeds 128 KiB.");
					}
					string yaml = File.ReadAllText(yamlInfo.FullName);
					settings.SetYaml(yaml);
					yamlWrite = yamlInfo.LastWriteTimeUtc;
					yamlLength = yamlInfo.Length;
					yamlObserved = true;
					yamlPending = false;
					yamlError = null;
				}
			}
			catch (Exception ex)
			{
				if (yamlError != ex.Message)
				{
					Warn("baits.yaml was not applied; keeping the last valid definitions: " + ex.Message);
				}
				yamlError = ex.Message;
				yamlObserved = true;
				yamlPending = false;
				yamlInfo.Refresh();
				yamlWrite = yamlInfo.LastWriteTimeUtc;
				yamlLength = (yamlInfo.Exists ? yamlInfo.Length : (-1));
			}
		}

		private void PollYaml(float now)
		{
			if (yamlInfo == null || now < nextYamlPoll)
			{
				return;
			}
			nextYamlPoll = now + 1f;
			try
			{
				yamlInfo.Refresh();
				if (yamlInfo.Exists)
				{
					if (!yamlObserved || yamlWrite != yamlInfo.LastWriteTimeUtc || yamlLength != yamlInfo.Length)
					{
						yamlWrite = yamlInfo.LastWriteTimeUtc;
						yamlLength = yamlInfo.Length;
						yamlObserved = true;
						yamlPending = true;
						yamlReloadAt = now + 0.25f;
					}
					else if (yamlPending && now >= yamlReloadAt)
					{
						LoadYaml();
					}
				}
			}
			catch (Exception ex)
			{
				if (yamlError != ex.Message)
				{
					Warn("Could not reload baits.yaml: " + ex.Message);
				}
				yamlError = ex.Message;
			}
		}

		private void Update()
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			BeginNetwork(ZNet.instance);
			settings.PollFile(realtimeSinceStartup);
			PollYaml(realtimeSinceStartup);
			if (Interlocked.Exchange(ref refresh, 0) != 0)
			{
				Refresh();
			}
			if (Object.op_Implicit((Object)(object)network))
			{
				if (dirty && authority && realtimeSinceStartup >= nextPublish)
				{
					dirty = false;
					nextPublish = realtimeSinceStartup + 0.25f;
					BroadcastSettings();
				}
				if (realtimeSinceStartup >= nextPeerCheck)
				{
					nextPeerCheck = realtimeSinceStartup + 1f;
					PumpConnections(realtimeSinceStartup);
				}
			}
		}

		private void OnDestroy()
		{
			if (settings != null)
			{
				settings.Refresh -= QueueRefresh;
				settings.AuthorityChanged -= Changed;
				foreach (ZRpc key in connections.Keys)
				{
					Unregister(key);
				}
				connections.Clear();
				settings.Dispose();
			}
			sections?.Dispose();
			recipes?.Dispose();
			Harmony obj = core;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		internal static bool VersionMatches(string received)
		{
			return string.Equals(received, "1.0.0", StringComparison.Ordinal);
		}

		private void RegisterConnection(ZNet net, ZNetPeer peer)
		{
			if ((Object)(object)net == (Object)null || peer == null || peer.m_rpc == null)
			{
				return;
			}
			BeginNetwork(net);
			if (!connections.ContainsKey(peer.m_rpc))
			{
				if (!net.IsServer() && peer.m_server)
				{
					connectionError = null;
					settings.WaitForServer();
				}
				Connection value = new Connection
				{
					Peer = peer,
					StartedAt = Time.realtimeSinceStartup
				};
				connections[peer.m_rpc] = value;
				peer.m_rpc.Register<string>("dannanboi.fishermansfixes.Hello", (Action<ZRpc, string>)ReceiveHello);
				peer.m_rpc.Register<ZPackage>("dannanboi.fishermansfixes.Settings", (Action<ZRpc, ZPackage>)ReceiveSettings);
				peer.m_rpc.Register<string>("dannanboi.fishermansfixes.Rejected", (Action<ZRpc, string>)ReceiveRejection);
				peer.m_rpc.Invoke("dannanboi.fishermansfixes.Hello", new object[1] { "1.0.0" });
			}
		}

		private bool IsServerConnection(ZRpc rpc)
		{
			if (Object.op_Implicit((Object)(object)network) && !network.IsServer() && connections.TryGetValue(rpc, out var value))
			{
				return value.Peer.m_server;
			}
			return false;
		}

		private void ReceiveHello(ZRpc rpc, string version)
		{
			if (!Object.op_Implicit((Object)(object)network) || !connections.TryGetValue(rpc, out var value) || value.Rejected || value.Version != null)
			{
				return;
			}
			value.Version = ((version != null && version.Length <= 32) ? version : "");
			if (!VersionMatches(value.Version))
			{
				if (network.IsServer())
				{
					Reject(rpc, value, Requirement(version));
				}
				else if (IsServerConnection(rpc))
				{
					FailClient("This server requires FishermansFixes " + SafeVersion(version) + ". You have 1.0.0. Install the matching version to join.");
				}
			}
			else if (authority)
			{
				SendSettings(rpc);
			}
		}

		private static string SafeVersion(string value)
		{
			if (string.IsNullOrEmpty(value) || value.Length > 32)
			{
				return "(unknown version)";
			}
			foreach (char c in value)
			{
				if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '+' && c != '_')
				{
					return "(unknown version)";
				}
			}
			return value;
		}

		private static string Requirement(string found)
		{
			return "This server requires FishermansFixes 1.0.0. " + (string.IsNullOrEmpty(found) ? "The mod is missing. " : ("Your version is " + SafeVersion(found) + ". ")) + "Install the matching version to join.";
		}

		private void Reject(ZRpc rpc, Connection connection, string message)
		{
			if (!connection.Rejected)
			{
				connection.Rejected = true;
				connection.CloseAt = Time.realtimeSinceStartup + 2f;
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				bool flag = connection.Version != null;
				if (flag)
				{
					rpc.Invoke("dannanboi.fishermansfixes.Rejected", new object[1] { message });
				}
				rpc.Invoke("Error", new object[1] { flag ? 3 : 5 });
			}
		}

		private void ReceiveRejection(ZRpc rpc, string message)
		{
			if (IsServerConnection(rpc) && !connections[rpc].Rejected)
			{
				if (string.IsNullOrEmpty(message) || message.Length > 512)
				{
					message = Requirement(null);
				}
				FailClient(message.Replace("<", "").Replace(">", ""));
			}
		}

		private void FailClient(string message)
		{
			connectionError = message;
			settings.WaitForServer();
			foreach (Connection value in connections.Values)
			{
				value.Rejected = true;
				value.CloseAt = Time.realtimeSinceStartup;
			}
			ConnectionStatus.SetValue(null, (object)(ConnectionStatus)3);
			((BaseUnityPlugin)this).Logger.LogError((object)message);
		}

		private bool GatePeerInfo(ZRpc rpc)
		{
			if (!connections.TryGetValue(rpc, out var value))
			{
				if (Object.op_Implicit((Object)(object)network) && network.IsServer())
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)(Requirement(null) + " Connection registration was not verified."));
					rpc.Invoke("Error", new object[1] { 5 });
				}
				else
				{
					FailClient("The connection could not verify FishermansFixes 1.0.0.");
				}
				return false;
			}
			if (value.Rejected)
			{
				return false;
			}
			if (VersionMatches(value.Version) && (network.IsServer() || settingsReady))
			{
				return true;
			}
			if (network.IsServer())
			{
				Reject(rpc, value, Requirement(value.Version));
			}
			else if (connectionError == null)
			{
				FailClient("The server must also run FishermansFixes 1.0.0 and send its settings before you can join.");
			}
			return false;
		}

		private void SendSettings(ZRpc rpc)
		{
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Expected O, but got Unknown
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a5: Expected O, but got Unknown
			if (authority && settings.Current.Bait && yamlInfo == null)
			{
				Refresh();
			}
			Settings current = settings.Current;
			if (settingsPacket == null || !current.SamePacket(encodedSettings))
			{
				ZPackage val = new ZPackage();
				val.Write(1);
				current.Write(val);
				settingsPacket = val.GetArray();
				encodedSettings = current;
				if (settingsPacket.Length > 262144)
				{
					throw new FormatException("Settings packet is too large.");
				}
			}
			rpc.Invoke("dannanboi.fishermansfixes.Settings", new object[1] { (object)new ZPackage(settingsPacket) });
		}

		private void BroadcastSettings()
		{
			if (!Object.op_Implicit((Object)(object)network) || !network.IsServer() || connections.Count == 0 || settings.Current.SamePacket(encodedSettings))
			{
				return;
			}
			foreach (KeyValuePair<ZRpc, Connection> connection in connections)
			{
				if (VersionMatches(connection.Value.Version) && !connection.Value.Rejected && connection.Key.IsConnected())
				{
					SendSettings(connection.Key);
				}
			}
		}

		private void ReceiveSettings(ZRpc rpc, ZPackage data)
		{
			if (!IsServerConnection(rpc))
			{
				return;
			}
			Connection connection = connections[rpc];
			if (!VersionMatches(connection.Version) || connection.Rejected)
			{
				return;
			}
			try
			{
				if (data == null || data.Size() < 7 || data.Size() > 262144 || data.ReadInt() != 1)
				{
					throw new FormatException("Invalid settings packet or protocol.");
				}
				Settings values = Settings.Read(data);
				if (data.GetPos() != data.Size())
				{
					throw new FormatException("Unexpected data in settings packet.");
				}
				settings.Receive(values);
			}
			catch (Exception ex)
			{
				FailClient("FishermansFixes 1.0.0: " + ex.Message);
			}
		}

		private void PumpConnections(float now)
		{
			if (connections.Count == 0)
			{
				return;
			}
			removedConnections.Clear();
			bool flag = network.IsServer();
			foreach (KeyValuePair<ZRpc, Connection> connection in connections)
			{
				Connection value = connection.Value;
				if (!connection.Key.IsConnected())
				{
					removedConnections.Add(connection.Key);
					continue;
				}
				if (flag && !value.Rejected && value.Version == null && now - value.StartedAt > 30f)
				{
					Reject(connection.Key, value, Requirement(null));
				}
				else if (!flag && value.Peer.m_server && !settingsReady && connectionError == null && now - value.StartedAt > 30f)
				{
					FailClient("The server did not complete the FishermansFixes 1.0.0 handshake.");
				}
				if (value.Rejected && now >= value.CloseAt)
				{
					network.Disconnect(value.Peer);
					removedConnections.Add(connection.Key);
				}
			}
			foreach (ZRpc removedConnection in removedConnections)
			{
				Unregister(removedConnection);
				connections.Remove(removedConnection);
			}
			removedConnections.Clear();
		}
	}
	internal sealed class RecipeRegistry : IDisposable
	{
		private sealed class Plan
		{
			internal ObjectDB DB;

			internal Recipe Source;

			internal ItemDrop Item;

			internal Requirement[] Costs;

			internal CraftingStation Station;

			internal bool Rod;
		}

		private readonly List<ObjectDB> databases = new List<ObjectDB>();

		private readonly List<Recipe> created = new List<Recipe>();

		private readonly Dictionary<Recipe, bool> previous = new Dictionary<Recipe, bool>();

		private string lastError;

		internal void Register(ObjectDB db)
		{
			if (Object.op_Implicit((Object)(object)db) && !databases.Contains(db))
			{
				databases.Add(db);
			}
		}

		private static ItemDrop Item(ObjectDB db, string name)
		{
			GameObject val = db.m_items.Find((GameObject p) => Object.op_Implicit((Object)(object)p) && ((Object)p).name == name);
			if (!Object.op_Implicit((Object)(object)val))
			{
				return null;
			}
			return val.GetComponent<ItemDrop>();
		}

		private static CraftingStation Workbench(ObjectDB db)
		{
			foreach (Recipe recipe in db.m_recipes)
			{
				if (Object.op_Implicit((Object)(object)recipe) && Object.op_Implicit((Object)(object)recipe.m_craftingStation) && ((Object)((Component)recipe.m_craftingStation).gameObject).name == "piece_workbench")
				{
					return recipe.m_craftingStation;
				}
			}
			ZNetScene instance = ZNetScene.instance;
			GameObject val = (Object.op_Implicit((Object)(object)instance) ? instance.GetPrefab("piece_workbench") : null);
			if (!Object.op_Implicit((Object)(object)val))
			{
				return null;
			}
			return val.GetComponent<CraftingStation>();
		}

		private static Requirement[] Costs(ObjectDB db, string recipe)
		{
			//IL_0055: 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_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Expected O, but got Unknown
			List<MaterialCost> list = Materials.Parse(recipe);
			Requirement[] array = (Requirement[])(object)new Requirement[list.Count];
			for (int i = 0; i < array.Length; i++)
			{
				ItemDrop val = Item(db, list[i].Item);
				if (!Object.op_Implicit((Object)(object)val))
				{
					throw new FormatException("Unknown material prefab '" + list[i].Item + "'.");
				}
				array[i] = new Requirement
				{
					m_resItem = val,
					m_amount = list[i].Amount,
					m_amountPerLevel = 0,
					m_recover = true
				};
			}
			return array;
		}

		private void AddPlans(List<Plan> plans, ObjectDB db, string itemName, string text, bool rod)
		{
			ItemDrop val = Item(db, itemName);
			if (!Object.op_Implicit((Object)(object)val))
			{
				throw new FormatException("Unknown item prefab '" + itemName + "'.");
			}
			Requirement[] costs = Costs(db, text);
			bool flag = false;
			foreach (Recipe recipe in db.m_recipes)
			{
				if (Object.op_Implicit((Object)(object)recipe) && !created.Contains(recipe) && Object.op_Implicit((Object)(object)recipe.m_item) && !(((Object)recipe.m_item).name != itemName))
				{
					plans.Add(new Plan
					{
						DB = db,
						Source = recipe,
						Item = val,
						Costs = costs,
						Rod = rod,
						Station = (rod ? Workbench(db) : recipe.m_craftingStation)
					});
					flag = true;
				}
			}
			if (!flag)
			{
				plans.Add(new Plan
				{
					DB = db,
					Item = val,
					Costs = costs,
					Rod = rod,
					Station = Workbench(db)
				});
			}
			if ((rod || !flag) && !Object.op_Implicit((Object)(object)Workbench(db)))
			{
				throw new FormatException("Workbench prefab is not ready.");
			}
		}

		internal void Apply(Settings s, bool ready)
		{
			bool flag = ready && s.Bait;
			bool flag2 = ready && s.Rods && s.Craft;
			if (!flag && !flag2)
			{
				Restore();
				return;
			}
			Restore();
			try
			{
				List<Plan> list = new List<Plan>();
				databases.RemoveAll((ObjectDB db) => !Object.op_Implicit((Object)(object)db));
				HashSet<List<Recipe>> hashSet = new HashSet<List<Recipe>>();
				foreach (ObjectDB database in databases)
				{
					if (database.m_items == null || database.m_recipes == null || !Object.op_Implicit((Object)(object)Item(database, "FishingRod")) || !hashSet.Add(database.m_recipes))
					{
						continue;
					}
					if (flag)
					{
						foreach (BaitDefinition value2 in s.Catalog.Baits.Values)
						{
							if (!string.IsNullOrWhiteSpace(value2.Recipe))
							{
								AddPlans(list, database, value2.Name, value2.Recipe, rod: false);
							}
						}
					}
					if (flag2)
					{
						AddPlans(list, database, "FishingRod", s.RodRecipe, rod: true);
					}
				}
				foreach (Plan item in list)
				{
					Recipe val = (Object.op_Implicit((Object)(object)item.Source) ? Object.Instantiate<Recipe>(item.Source) : ScriptableObject.CreateInstance<Recipe>());
					((Object)val).name = "dbff_Recipe_" + ((Object)item.Item).name + "_" + created.Count;
					val.m_item = item.Item;
					val.m_resources = item.Costs;
					val.m_requireOnlyOneIngredient = false;
					bool flag3 = !Object.op_Implicit((Object)(object)item.Source) || item.Source.m_enabled;
					if (Object.op_Implicit((Object)(object)item.Source))
					{
						if (previous.TryGetValue(item.Source, out var value))
						{
							flag3 = value;
						}
						else
						{
							previous.Add(item.Source, flag3);
						}
						item.Source.m_enabled = false;
					}
					if (item.Rod)
					{
						val.m_amount = 1;
						val.m_minStationLevel = 2;
						val.m_craftingStation = (val.m_repairStation = item.Station);
						val.m_requireOnlyOneIngredient = false;
					}
					else if (!Object.op_Implicit((Object)(object)item.Source))
					{
						val.m_amount = 20;
						val.m_minStationLevel = 1;
						val.m_craftingStation = (val.m_repairStation = item.Station);
					}
					val.m_enabled = item.Rod || flag3;
					created.Add(val);
					item.DB.m_recipes.Add(val);
				}
				lastError = null;
			}
			catch (Exception ex)
			{
				Restore();
				string text = "Recipes were not updated; using original recipes until valid ingredients are available: " + ex.Message;
				if (text != lastError)
				{
					Plugin.Warn(text);
				}
				lastError = text;
			}
		}

		private void Restore()
		{
			foreach (ObjectDB database in databases)
			{
				if (Object.op_Implicit((Object)(object)database) && database.m_recipes != null)
				{
					database.m_recipes.RemoveAll((Recipe r) => created.Contains(r));
				}
			}
			foreach (KeyValuePair<Recipe, bool> previou in previous)
			{
				if (Object.op_Implicit((Object)(object)previou.Key))
				{
					previou.Key.m_enabled = previou.Value;
				}
			}
			foreach (Recipe item in created)
			{
				if (Object.op_Implicit((Object)(object)item))
				{
					Object.Destroy((Object)(object)item);
				}
			}
			created.Clear();
			previous.Clear();
		}

		public void Dispose()
		{
			Restore();
			databases.Clear();
		}
	}
	internal sealed class MaterialCost
	{
		internal string Item;

		internal int Amount;
	}
	internal static class Materials
	{
		internal static List<MaterialCost> Parse(string text)
		{
			if (string.IsNullOrWhiteSpace(text) || text.Length > 16000)
			{
				throw new FormatException("Use Item1[amount],Item2[amount] with positive whole amounts.");
			}
			List<MaterialCost> list = new List<MaterialCost>();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			string[] array = text.Split(',');
			foreach (string text2 in array)
			{
				string text3 = text2.Trim();
				int num = text3.IndexOf('[');
				if (num <= 0 || !text3.EndsWith("]", StringComparison.Ordinal) || !int.TryParse(text3.Substring(num + 1, text3.Length - num - 2), NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 1)
				{
					throw new FormatException("Invalid material '" + text2 + "'. Use Wood[20].");
				}
				string text4 = text3.Substring(0, num).Trim();
				BaitCatalog.ValidateName(text4);
				if (!hashSet.Add(text4))
				{
					throw new FormatException("Duplicate material '" + text4 + "'.");
				}
				if (list.Count == 32)
				{
					throw new FormatException("At most 32 ingredients are allowed.");
				}
				list.Add(new MaterialCost
				{
					Item = text4,
					Amount = result
				});
			}
			return list;
		}
	}
	internal static class BaitRules
	{
		private sealed class FishInfo
		{
			internal string Name;

			internal float Fallback = 1f;

			internal readonly Dictionary<string, float> Chances = new Dictionary<string, float>(StringComparer.Ordinal);
		}

		private static ConditionalWeakTable<Fish, FishInfo> cache = new ConditionalWeakTable<Fish, FishInfo>();

		private static readonly FieldRef<Fish, List<BaitSetting>> nativeBaits = AccessTools.FieldRefAccess<Fish, List<BaitSetting>>("m_baits");

		private static Dictionary<string, HashSet<string>> allowed = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);

		private static BaitCatalog catalog = BaitCatalog.Empty;

		internal static void Configure(Settings s, bool ready)
		{
			catalog = ((ready && s.Bait) ? s.Catalog : BaitCatalog.Empty);
			allowed = catalog.BuildAllowed(s.Lower);
			cache = new ConditionalWeakTable<Fish, FishInfo>();
		}

		internal static string PrefabName(GameObject obj)
		{
			if (!Object.op_Implicit((Object)(object)obj))
			{
				return "";
			}
			string name = ((Object)obj).name;
			int num = name.IndexOf("(Clone)", StringComparison.Ordinal);
			if (num >= 0)
			{
				return name.Substring(0, num);
			}
			return name;
		}

		private static FishInfo ReadFish(Fish fish)
		{
			FishInfo fishInfo = new FishInfo
			{
				Name = PrefabName(((Component)fish).gameObject)
			};
			float num = 0f;
			List<BaitSetting> list = nativeBaits.Invoke(fish);
			if (list != null)
			{
				foreach (BaitSetting item in list)
				{
					if (item != null && Object.op_Implicit((Object)(object)item.m_bait))
					{
						float num2 = Mathf.Clamp01(item.m_chance);
						string name = ((Object)item.m_bait).name;
						fishInfo.Chances.TryGetValue(name, out var value);
						fishInfo.Chances[name] = 1f - (1f - value) * (1f - num2);
						num = Mathf.Max(num, num2);
					}
				}
			}
			if (num > 0f)
			{
				fishInfo.Fallback = num;
			}
			return fishInfo;
		}

		internal static bool Test(Fish fish, FishingFloat ff, out bool result)
		{
			result = false;
			if (!Object.op_Implicit((Object)(object)fish) || !Object.op_Implicit((Object)(object)ff))
			{
				return false;
			}
			string bait = ff.GetBait();
			if (!allowed.TryGetValue(bait, out var value))
			{
				return false;
			}
			FishInfo value2 = cache.GetValue(fish, ReadFish);
			if (!value.Contains(value2.Name))
			{
				return true;
			}
			if (!value2.Chances.TryGetValue(bait, out var value3))
			{
				value3 = value2.Fallback;
			}
			result = Random.value < value3;
			return true;
		}

		internal static int Required(ItemData item)
		{
			if (item == null || !Object.op_Implicit((Object)(object)item.m_dropPrefab))
			{
				return 0;
			}
			if (!catalog.Baits.TryGetValue(((Object)item.m_dropPrefab).name, out var value))
			{
				return 0;
			}
			return value.Level;
		}

		internal static bool CanUse(Humanoid player, ItemData bait, bool message)
		{
			if (!IsLocalPlayer(player))
			{
				return true;
			}
			int num = Required(bait);
			if (num <= 0 || !Object.op_Implicit((Object)(object)player) || BaitCatalog.MeetsLevel(((Character)player).GetSkillLevel((SkillType)104), num))
			{
				return true;
			}
			if (message)
			{
				ShowLockMessage(num);
			}
			return false;
		}

		internal static bool IsLocalPlayer(Humanoid player)
		{
			if (Object.op_Implicit((Object)(object)player))
			{
				return (object)player == Player.m_localPlayer;
			}
			return false;
		}

		private static void ShowLockMessage(int level)
		{
			if (Object.op_Implicit((Object)(object)MessageHud.instance))
			{
				MessageHud.instance.ShowMessage((MessageType)2, "Requires Fishing level " + level + " to use this bait.", 0, (Sprite)null, false, false);
			}
		}

		internal static bool IsFishingRod(ItemData item)
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Invalid comparison between Unknown and I4
			if (item?.m_shared != null)
			{
				if (!Object.op_Implicit((Object)(object)item.m_dropPrefab) || !(((Object)item.m_dropPrefab).name == "FishingRod"))
				{
					return (int)item.m_shared.m_skillType == 104;
				}
				return true;
			}
			return false;
		}

		internal static ItemData NativeAmmo(Humanoid player, ItemData weapon)
		{
			ItemData ammoItem = player.GetAmmoItem();
			Inventory inventory = player.GetInventory();
			string ammoType = weapon.m_shared.m_ammoType;
			if (ammoItem != null && inventory.ContainsItem(ammoItem) && ammoItem.m_shared.m_ammoType == ammoType)
			{
				return ammoItem;
			}
			return inventory.GetAmmoItem(ammoType, (string)null);
		}

		internal static ItemData UnlockedAmmo(Humanoid player, ItemData weapon)
		{
			ItemData val = NativeAmmo(player, weapon);
			if (!IsLocalPlayer(player) || val == null)
			{
				return val;
			}
			int num = Required(val);
			if (num <= 0)
			{
				return val;
			}
			float skillLevel = ((Character)player).GetSkillLevel((SkillType)104);
			if (BaitCatalog.MeetsLevel(skillLevel, num))
			{
				return val;
			}
			foreach (ItemData allItem in player.GetInventory().GetAllItems())
			{
				if (allItem.m_shared.m_ammoType == weapon.m_shared.m_ammoType && allItem.m_stack > 0 && BaitCatalog.MeetsLevel(skillLevel, Required(allItem)))
				{
					return allItem;
				}
			}
			return null;
		}

		internal static bool CanCast(Humanoid player, ItemData weapon)
		{
			if (!IsLocalPlayer(player) || !IsFishingRod(weapon))
			{
				return true;
			}
			ItemData val = NativeAmmo(player, weapon);
			if (val == null || UnlockedAmmo(player, weapon) != null)
			{
				return true;
			}
			ShowLockMessage(Required(val));
			return false;
		}
	}
	[HarmonyPatch(typeof(Fish), "TestBate")]
	internal static class BaitCatch
	{
		private static bool Prefix(Fish __instance, FishingFloat ff, ref bool __result)
		{
			if (!BaitRules.Test(__instance, ff, out var result))
			{
				return true;
			}
			__result = result;
			return false;
		}
	}
	[HarmonyPatch(typeof(Humanoid), "EquipItem")]
	internal static class BaitEquip
	{
		private static bool Prefix(Humanoid __instance, ItemData item, ref bool __result)
		{
			if (BaitRules.CanUse(__instance, item, message: true))
			{
				return true;
			}
			__result = false;
			return false;
		}
	}
	[HarmonyPatch(typeof(Attack), "FindAmmo")]
	internal static class BaitFindAmmo
	{
		private static void Postfix(Humanoid character, ItemData weapon, ref ItemData __result)
		{
			if (BaitRules.IsLocalPlayer(character) && BaitRules.IsFishingRod(weapon))
			{
				__result = BaitRules.UnlockedAmmo(character, weapon);
			}
		}
	}
	[HarmonyPatch(typeof(Attack), "EquipAmmoItem")]
	internal static class BaitAutoEquip
	{
		private static bool Prefix(Humanoid character, ItemData weapon, ref bool __result)
		{
			if (!BaitRules.IsLocalPlayer(character) || !BaitRules.IsFishingRod(weapon))
			{
				return true;
			}
			ItemData val = BaitRules.UnlockedAmmo(character, weapon);
			if (val == null)
			{
				ItemData val2 = BaitRules.NativeAmmo(character, weapon);
				if (val2 != null)
				{
					BaitRules.CanUse(character, val2, message: true);
				}
				__result = false;
			}
			else
			{
				__result = character.EquipItem(val, true);
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(Attack), "StartDraw")]
	internal static class BaitDraw
	{
		private static bool Prefix(Humanoid character, ItemData weapon, ref bool __result)
		{
			if (BaitRules.CanCast(character, weapon))
			{
				return true;
			}
			__result = false;
			return false;
		}
	}
	[HarmonyPatch(typeof(Humanoid), "StartAttack")]
	internal static class BaitCast
	{
		private static readonly FieldRef<Player, float> queuedAttack = AccessTools.FieldRefAccess<Player, float>("m_queuedAttackTimer");

		private static bool Prefix(Humanoid __instance, bool secondaryAttack, ref bool __result)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Expected O, but got Unknown
			if (secondaryAttack || !BaitRules.IsLocalPlayer(__instance))
			{
				return true;
			}
			if (BaitRules.CanCast(__instance, __instance.GetCurrentWeapon()))
			{
				return true;
			}
			queuedAttack.Invoke((Player)__instance) = 0f;
			__result = false;
			return false;
		}
	}
	internal static class TreasureRules
	{
		private static readonly FieldRef<Fish, DropTable> extra = AccessTools.FieldRefAccess<Fish, DropTable>("m_extraDrops");

		private static readonly DropTable empty = new DropTable();

		internal static bool IsVanillaFish(string name)
		{
			if (name == "Fish4_cave")
			{
				return true;
			}
			if (!name.StartsWith("Fish", StringComparison.Ordinal))
			{
				return false;
			}
			if (int.TryParse(name.Substring(4), out var result) && result >= 1 && result <= 12)
			{
				return name == "Fish" + result;
			}
			return false;
		}

		internal static DropTable GetDrops(Fish fish)
		{
			if (!IsVanillaFish(BaitRules.PrefabName(((Component)fish).gameObject)))
			{
				return extra.Invoke(fish);
			}
			return empty;
		}
	}
	[HarmonyPatch(typeof(FishingFloat), "Catch")]
	internal static class RemoveTreasure
	{
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			FieldInfo field = AccessTools.Field(typeof(Fish), "m_extraDrops");
			MethodInfo replace = AccessTools.Method(typeof(TreasureRules), "GetDrops", (Type[])null, (Type[])null);
			int changed = 0;
			foreach (CodeInstruction instruction in instructions)
			{
				if (CodeInstructionExtensions.LoadsField(instruction, field, false))
				{
					instruction.opcode = OpCodes.Call;
					instruction.operand = replace;
					changed++;
				}
				yield return instruction;
			}
			if (changed == 0)
			{
				throw new MissingMethodException("Cannot locate the native fishing treasure table.");
			}
		}
	}
	internal static class DefaultBaits
	{
		internal const string Text = "# FishermansFixes\n# Server/host controls this file in multiplayer; reloads automatically while Bait is enabled.\n# Top-level keys and fish lists use case-sensitive console prefab names.\n# Section headings: 2 spaces. Dashed rows: 4 spaces, matching Food Knowledge style.\n# levelUnlock: Fishing skill 0-100; 0 is unrestricted. Temporary bonuses count.\n# recipe: \"\" keeps native recipes. Otherwise use Item1[amount],Item2[amount].\n# Existing recipes retain their station, station level and output amount.\n# A bait without a native recipe gets 20 bait per craft at a level 1 Workbench.\n# Lower Tier Progression order: Meadows, BlackForest, Swamp, Ocean, Mountain,\n# Plains, Mistlands, Ashlands, DeepNorth. Lists are inherited from lower biomes.\n# Custom bait must already exist in the game; specify biome to include it in progression.\n# An omitted bait or omitted fish section uses native fish rules. fish: [] disallows direct catches (lower tiers still apply).\n\nFishingBait:\n  basics:\n    - biome: Meadows\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish1\n    - Fish2\n\nFishingBaitForest:\n  basics:\n    - biome: BlackForest\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish5\n\nFishingBaitSwamp:\n  basics:\n    - biome: Swamp\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish6\n\nFishingBaitOcean:\n  basics:\n    - biome: Ocean\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish3\n    - Fish8\n\nFishingBaitCave:\n  basics:\n    - biome: Mountain\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish4\n    - Fish4_cave\n\nFishingBaitPlains:\n  basics:\n    - biome: Plains\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish7\n\nFishingBaitMistlands:\n  basics:\n    - biome: Mistlands\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish9\n    - Fish12\n\nFishingBaitAshlands:\n  basics:\n    - biome: Ashlands\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish11\n\nFishingBaitDeepNorth:\n  basics:\n    - biome: DeepNorth\n    - levelUnlock: 0\n    - recipe: \"\"\n  fish:\n    - Fish10\n\n";
	}
	internal sealed class SectionPatches : IDisposable
	{
		private sealed class Group
		{
			private readonly Harmony harmony;

			private readonly Type[] types;

			private bool active;

			internal Group(string name, params Type[] patches)
			{
				//IL_0012: Unknown result type (might be due to invalid IL or missing references)
				//IL_001c: Expected O, but got Unknown
				harmony = new Harmony("dannanboi.fishermansfixes." + name);
				types = patches;
			}

			internal void Set(bool enabled)
			{
				if (active == enabled)
				{
					return;
				}
				if (enabled)
				{
					try
					{
						Type[] array = types;
						foreach (Type type in array)
						{
							harmony.CreateClassProcessor(type).Patch();
						}
					}
					catch
					{
						harmony.UnpatchSelf();
						throw;
					}
				}
				else
				{
					harmony.UnpatchSelf();
				}
				active = enabled;
			}
		}

		private readonly Group bait = new Group("Bait", typeof(BaitCatch));

		private readonly Group locks = new Group("BaitLocks", typeof(BaitEquip), typeof(BaitFindAmmo), typeof(BaitAutoEquip), typeof(BaitDraw), typeof(BaitCast));

		private readonly Group recipes = new Group("Recipes", typeof(DatabaseAwake), typeof(DatabaseCopy), typeof(SceneAwake));

		private readonly Group treasure = new Group("Treasure", typeof(RemoveTreasure));

		internal void Refresh(Settings s, bool ready)
		{
			bool flag = ready && s.Bait;
			bool flag2 = false;
			if (flag)
			{
				foreach (BaitDefinition value in s.Catalog.Baits.Values)
				{
					if (value.HasFish)
					{
						flag2 = true;
						break;
					}
				}
			}
			bait.Set(flag && flag2);
			bool flag3 = false;
			bool flag4 = false;
			if (flag)
			{
				foreach (BaitDefinition value2 in s.Catalog.Baits.Values)
				{
					if (value2.Level > 0)
					{
						flag3 = true;
					}
					if (value2.Recipe.Length != 0)
					{
						flag4 = true;
					}
				}
			}
			locks.Set((!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsDedicated()) && flag && s.Lock && flag3);
			recipes.Set((flag && flag4) || (ready && s.Rods && s.Craft));
			treasure.Set(ready && s.Treasure);
		}

		public void Dispose()
		{
			bait.Set(enabled: false);
			locks.Set(enabled: false);
			recipes.Set(enabled: false);
			treasure.Set(enabled: false);
		}
	}
	[HarmonyPatch(typeof(ObjectDB), "Awake")]
	internal static class DatabaseAwake
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix(ObjectDB __instance)
		{
			Plugin.Instance?.Database(__instance);
		}
	}
	[HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")]
	internal static class DatabaseCopy
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix(ObjectDB __instance)
		{
			Plugin.Instance?.Database(__instance);
		}
	}
	[HarmonyPatch(typeof(ZNetScene), "Awake")]
	internal static class SceneAwake
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix()
		{
			Plugin.Instance?.Database(ObjectDB.instance);
		}
	}
	internal sealed class BaitDefinition
	{
		internal string Name;

		internal string Recipe = "";

		internal int Tier = -1;

		internal int Level;

		internal bool HasFish;

		internal readonly HashSet<string> Fish = new HashSet<string>(StringComparer.Ordinal);
	}
	internal sealed class BaitCatalog
	{
		internal const int MaxBytes = 131072;

		internal static readonly BaitCatalog Empty = new BaitCatalog();

		internal readonly Dictionary<string, BaitDefinition> Baits = new Dictionary<string, BaitDefinition>(StringComparer.Ordinal);

		internal Dictionary<string, HashSet<string>> BuildAllowed(bool lower)
		{
			Dictionary<string, HashSet<string>> dictionary = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
			foreach (KeyValuePair<string, BaitDefinition> bait in Baits)
			{
				if (!bait.Value.HasFish)
				{
					continue;
				}
				HashSet<string> hashSet = new HashSet<string>(bait.Value.Fish, StringComparer.Ordinal);
				if (lower && bait.Value.Tier >= 0)
				{
					foreach (BaitDefinition value in Baits.Values)
					{
						if (value.Tier >= 0 && value.Tier < bait.Value.Tier)
						{
							hashSet.UnionWith(value.Fish);
						}
					}
				}
				dictionary.Add(bait.Key, hashSet);
			}
			return dictionary;
		}

		internal static bool MeetsLevel(float effective, int required)
		{
			if (required > 0)
			{
				if (!float.IsNaN(effective))
				{
					return effective >= (float)required;
				}
				return false;
			}
			return true;
		}

		internal static int Tier(string biome)
		{
			switch ((biome ?? "").Replace(" ", "").ToLowerInvariant())
			{
			case "meadows":
				return 0;
			case "blackforest":
				return 1;
			case "swamp":
				return 2;
			case "ocean":
				return 3;
			case "mountain":
			case "mountains":
				return 4;
			case "plains":
				return 5;
			case "mistlands":
				return 6;
			case "ashlands":
				return 7;
			case "deepnorth":
				return 8;
			default:
				throw new FormatException("Unknown biome '" + biome + "'.");
			}
		}

		internal static int NativeTier(string bait)
		{
			return bait switch
			{
				"FishingBait" => 0, 
				"FishingBaitForest" => 1, 
				"FishingBaitSwamp" => 2, 
				"FishingBaitOcean" => 3, 
				"FishingBaitCave" => 4, 
				"FishingBaitPlains" => 5, 
				"FishingBaitMistlands" => 6, 
				"FishingBaitAshlands" => 7, 
				"FishingBaitDeepNorth" => 8, 
				_ => -1, 
			};
		}

		internal static void ValidateName(string s)
		{
			if (string.IsNullOrEmpty(s) || s.Length > 128)
			{
				throw new FormatException("Missing or oversized prefab name.");
			}
			foreach (char c in s)
			{
				if (!char.IsLetterOrDigit(c) && c != '_')
				{
					throw new FormatException("Invalid prefab name '" + s + "'.");
				}
			}
		}

		internal static BaitCatalog Parse(string text)
		{
			if (text == null || Encoding.UTF8.GetByteCount(text) > 131072)
			{
				throw new FormatException("baits.yaml exceeds 128 KiB.");
			}
			BaitCatalog baitCatalog = new BaitCatalog();
			BaitDefinition baitDefinition = null;
			string text2 = null;
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			HashSet<string> hashSet2 = new HashSet<string>(StringComparer.Ordinal);
			string[] array = text.TrimStart('\ufeff').Replace("\r\n", "\n").Replace('\r', '\n')
				.Split('\n');
			bool flag = false;
			bool flag2 = false;
			for (int i = 0; i < array.Length; i++)
			{
				try
				{
					string text3 = array[i];
					if (text3.IndexOf('\t') >= 0)
					{
						throw new FormatException("Use spaces, not tabs.");
					}
					string text4 = Uncomment(text3).Trim();
					if (text4.Length == 0)
					{
						continue;
					}
					int num = text3.Length - text3.TrimStart(' ').Length;
					if (text4 == "---" && num == 0 && !flag2)
					{
						flag2 = true;
						continue;
					}
					if ((text4 == "..." || text4 == "{}") && num == 0)
					{
						flag = true;
						continue;
					}
					if (flag)
					{
						throw new FormatException("Multiple documents or fields after document end are not supported.");
					}
					flag2 = true;
					if (num == 0)
					{
						int num2 = text4.IndexOf(':');
						if (num2 < 1 || text4.Substring(num2 + 1).Trim().Length != 0)
						{
							throw new FormatException("Start each bait with PrefabName: followed by indented fields.");
						}
						string text5 = Scalar(text4.Substring(0, num2));
						ValidateName(text5);
						if (baitCatalog.Baits.Count >= 256)
						{
							throw new FormatException("At most 256 baits are allowed.");
						}
						if (baitCatalog.Baits.ContainsKey(text5))
						{
							throw new FormatException("Duplicate bait '" + text5 + "'.");
						}
						baitDefinition = new BaitDefinition
						{
							Name = text5,
							Tier = NativeTier(text5)
						};
						baitCatalog.Baits.Add(text5, baitDefinition);
						hashSet.Clear();
						hashSet2.Clear();
						text2 = null;
						continue;
					}
					if (baitDefinition == null)
					{
						throw new FormatException("Start with a bait prefab name.");
					}
					switch (num)
					{
					case 2:
					{
						int num4 = text4.IndexOf(':');
						if (num4 < 1)
						{
							throw new FormatException("Expected a field or section.");
						}
						string text8 = text4.Substring(0, num4).Trim();
						string text9 = text4.Substring(num4 + 1).Trim();
						if (text8 == "fish" || text8 == "basics")
						{
							if (!hashSet2.Add(text8))
							{
								throw new FormatException("Duplicate section '" + text8 + "'.");
							}
							if (text9.Length != 0 && (!(text8 == "fish") || !(text9 == "[]")))
							{
								throw new FormatException("Sections require indented rows; fish: [] is allowed.");
							}
							if (text8 == "fish")
							{
								baitDefinition.HasFish = true;
							}
							text2 = text8;
						}
						else
						{
							text2 = null;
							Field(baitDefinition, hashSet, text8, text9);
						}
						break;
					}
					case 4:
						if (text2 != null && text4.StartsWith("- ", StringComparison.Ordinal))
						{
							string text6 = text4.Substring(2).Trim();
							if (text2 == "fish")
							{
								string text7 = Scalar(text6);
								ValidateName(text7);
								if (baitDefinition.Fish.Count >= 512)
								{
									throw new FormatException("At most 512 fish per bait are allowed.");
								}
								if (!baitDefinition.Fish.Add(text7))
								{
									throw new FormatException("Duplicate fish '" + text7 + "'.");
								}
							}
							else
							{
								int num3 = text6.IndexOf(':');
								if (num3 <= 0)
								{
									throw new FormatException("Expected - field: value.");
								}
								Field(baitDefinition, hashSet, text6.Substring(0, num3).Trim(), text6.Substring(num3 + 1).Trim());
							}
							break;
						}
						goto default;
					default:
						throw new FormatException("Use 2 spaces for sections and 4 spaces for dashed rows.");
					}
				}
				catch (FormatException ex)
				{
					throw new FormatException("baits.yaml:" + (i + 1) + ": " + ex.Message);
				}
			}
			return baitCatalog;
		}

		private static void Field(BaitDefinition bait, HashSet<string> seen, string key, string raw)
		{
			if (!seen.Add(key))
			{
				throw new FormatException("Duplicate field '" + key + "'.");
			}
			string text = Scalar(raw);
			switch (key)
			{
			case "biome":
				bait.Tier = Tier(text);
				break;
			case "recipe":
				bait.Recipe = text;
				if (text.Length != 0)
				{
					Materials.Parse(text);
				}
				break;
			case "levelUnlock":
				if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out bait.Level) || bait.Level < 0 || bait.Level > 100)
				{
					throw new FormatException("levelUnlock must be a whole number from 0 to 100.");
				}
				break;
			default:
				throw new FormatException("Unknown field '" + key + "'. Use biome, recipe, levelUnlock, or fish.");
			}
		}

		private static string Uncomment(string s)
		{
			char c = '\0';
			for (int i = 0; i < s.Length; i++)
			{
				char c2 = s[i];
				if (c != 0)
				{
					if (c2 == '\\' && c == '"')
					{
						i++;
					}
					else if (c2 == c)
					{
						if (c == '\'' && i + 1 < s.Length && s[i + 1] == '\'')
						{
							i++;
						}
						else
						{
							c = '\0';
						}
					}
					continue;
				}
				switch (c2)
				{
				case '"':
				case '\'':
					c = c2;
					break;
				case '#':
					if (i == 0 || char.IsWhiteSpace(s[i - 1]))
					{
						return s.Substring(0, i);
					}
					break;
				}
			}
			return s;
		}

		private static string Scalar(string s)
		{
			s = s.Trim();
			if (s == "null" || s == "~")
			{
				return "";
			}
			if (s.Length == 0)
			{
				return s;
			}
			if (s[0] == '\'' || s[0] == '"')
			{
				char c = s[0];
				if (s.Length < 2 || s[s.Length - 1] != c)
				{
					throw new FormatException("Unclosed quote.");
				}
				string text = s.Substring(1, s.Length - 2);
				if (c == '\'')
				{
					return text.Replace("''", "'");
				}
				StringBuilder stringBuilder = new StringBuilder();
				for (int i = 0; i < text.Length; i++)
				{
					char c2 = text[i];
					switch (c2)
					{
					case '\\':
						if (++i == text.Length)
						{
							throw new FormatException("Incomplete escape.");
						}
						c2 = text[i];
						if (c2 != '\\' && c2 != '"')
						{
							throw new FormatException("Only escaped quotes and backslashes are supported.");
						}
						break;
					case '"':
						throw new FormatException("Escape embedded quotes.");
					}
					stringBuilder.Append(c2);
				}
				return stringBuilder.ToString();
			}
			if ("[{&*!%@`".IndexOf(s[0]) >= 0 || s.Contains(": "))
			{
				throw new FormatException("Unsupported scalar; use plain or quoted values.");
			}
			return s;
		}
	}
	internal sealed class Settings
	{
		internal const string DefaultRodRecipe = "Wood[20],Resin[5]";

		internal const int MaxPacket = 262144;

		internal readonly bool Bait;

		internal readonly bool Lower;

		internal readonly bool Lock;

		internal readonly bool Rods;

		internal readonly bool Craft;

		internal readonly bool Treasure;

		internal readonly string RodRecipe;

		internal readonly string Yaml;

		internal readonly BaitCatalog Catalog;

		internal Settings(bool bait, bool lower, bool levelLock, bool rods, bool craft, string rodRecipe, bool treasure, string yaml = "")
		{
			if (rodRecipe == null || rodRecipe.Length > 16000)
			{
				throw new FormatException("Fishingrod Recipe is too long or missing.");
			}
			if (rods && craft)
			{
				Materials.Parse(rodRecipe);
			}
			Bait = bait;
			Lower = lower;
			Lock = levelLock;
			Rods = rods;
			Craft = craft;
			RodRecipe = rodRecipe;
			Treasure = treasure;
			Yaml = (bait ? (yaml ?? "") : "");
			Catalog = ((bait && Yaml.Length != 0) ? BaitCatalog.Parse(Yaml) : BaitCatalog.Empty);
		}

		internal Settings WithYaml(string yaml)
		{
			return new Settings(Bait, Lower, Lock, Rods, Craft, RodRecipe, Treasure, yaml);
		}

		internal bool Same(Settings s)
		{
			if (s != null && Bait == s.Bait && Lower == s.Lower && Lock == s.Lock && Rods == s.Rods && Craft == s.Craft && RodRecipe == s.RodRecipe && Treasure == s.Treasure)
			{
				return Yaml == s.Yaml;
			}
			return false;
		}

		internal bool SamePacket(Settings s)
		{
			if (s != null && Bait == s.Bait && (!Bait || (Lower == s.Lower && Lock == s.Lock && Yaml == s.Yaml)) && Rods == s.Rods && (!Rods || (Craft == s.Craft && (!Craft || RodRecipe == s.RodRecipe))))
			{
				return Treasure == s.Treasure;
			}
			return false;
		}

		internal void Write(ZPackage p)
		{
			p.Write(Bait);
			if (Bait)
			{
				p.Write(Lower);
				p.Write(Lock);
				p.Write(Yaml);
			}
			p.Write(Rods);
			if (Rods)
			{
				p.Write(Craft);
				if (Craft)
				{
					p.Write(RodRecipe);
				}
			}
			p.Write(Treasure);
		}

		internal static Settings Read(ZPackage p)
		{
			bool num = p.ReadBool();
			bool lower = true;
			bool levelLock = false;
			string yaml = "";
			if (num)
			{
				lower = p.ReadBool();
				levelLock = p.ReadBool();
				yaml = p.ReadString();
			}
			bool flag = p.ReadBool();
			bool flag2 = true;
			string rodRecipe = "Wood[20],Resin[5]";
			if (flag)
			{
				flag2 = p.ReadBool();
				if (flag2)
				{
					rodRecipe = p.ReadString();
				}
			}
			return new Settings(num, lower, levelLock, flag, flag2, rodRecipe, p.ReadBool(), yaml);
		}
	}
	internal sealed class ConfigurationManagerAttributes
	{
		public bool? ReadOnly;

		public int? Order;

		public bool? ShowRangeAsPercent;
	}
	internal sealed class LiveConfig : IDisposable
	{
		internal sealed class SaveScope
		{
			private LiveConfig owner;

			internal SaveScope(LiveConfig value)
			{
				owner = value;
			}

			internal void Finish()
			{
				if (owner == null)
				{
					return;
				}
				LiveConfig liveConfig = owner;
				owner = null;
				try
				{
					liveConfig.ApplyValues(liveConfig.Current);
				}
				finally
				{
					liveConfig.applying--;
				}
			}
		}

		internal sealed class ReloadScope
		{
			private LiveConfig owner;

			private readonly Settings before;

			internal ReloadScope(LiveConfig value, Settings previous)
			{
				owner = value;
				before = previous;
			}

			internal void Finish(Exception error)
			{
				if (owner == null)
				{
					return;
				}
				LiveConfig liveConfig = owner;
				owner = null;
				try
				{
					try
					{
						liveConfig.Local = ((error == null) ? liveConfig.Capture() : before);
					}
					catch (FormatException ex)
					{
						liveConfig.Local = before;
						Plugin.Warn("Config reload not applied: " + ex.Message);
					}
					liveConfig.ApplyValues(liveConfig.Current);
				}
				finally
				{
					liveConfig.applying--;
				}
				liveConfig.Refresh?.Invoke();
				if (!liveConfig.Client && !liveConfig.Local.Same(before))
				{
					liveConfig.AuthorityChanged?.Invoke();
				}
			}
		}

		internal static LiveConfig Instance;

		internal readonly ConfigFile File;

		internal readonly ConfigEntry<bool> BaitSection;

		internal readonly ConfigEntry<bool> LowerTiers;

		internal readonly ConfigEntry<bool> LevelLock;

		internal readonly ConfigEntry<bool> RodSection;

		internal readonly ConfigEntry<bool> CraftRod;

		internal readonly ConfigEntry<bool> RemoveTreasure;

		internal readonly ConfigEntry<string> RodRecipe;

		internal Settings Local;

		internal Settings Remote;

		private int applying;

		private readonly bool autoSave;

		private bool disposed;

		private readonly FileInfo configInfo;

		private DateTime observedWrite;

		private long observedLength;

		private float nextPoll;

		private float reloadAt;

		private bool filePending;

		private string fileError;

		private readonly List<ConfigurationManagerAttributes> tags = new List<ConfigurationManagerAttributes>();

		internal bool Client { get; private set; }

		internal bool Ready
		{
			get
			{
				if (Client)
				{
					return Remote != null;
				}
				return true;
			}
		}

		internal Settings Current
		{
			get
			{
				if (!Client || Remote == null)
				{
					return Local;
				}
				return Remote;
			}
		}

		internal event Action AuthorityChanged;

		internal event Action Refresh;

		private ConfigDescription Describe(string text, int order, AcceptableValueBase acceptable = null)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Expected O, but got Unknown
			ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes
			{
				Order = order,
				ShowRangeAsPercent = false
			};
			tags.Add(configurationManagerAttributes);
			return new ConfigDescription(text, acceptable, new object[1] { configurationManagerAttributes });
		}

		internal LiveConfig(ConfigFile file)
		{
			File = file;
			configInfo = new FileInfo(file.ConfigFilePath);
			autoSave = file.SaveOnConfigSet;
			file.SaveOnConfigSet = false;
			BaitSection = file.Bind<bool>("Bait", "Enable Section", true, Describe("Enable bait fish lists, recipes, progression and skill locks. False removes all bait gameplay hooks and stops YAML loading/polling/sync. Config and multiplayer authority essentials remain. Server-synced and locked in multiplayer.", 100));
			LowerTiers = file.Bind<bool>("Bait", "Lower Tier Progression", true, Describe("Higher tier bait can catch fish listed under every lower biome in baits.yaml. Order: Meadows, BlackForest, Swamp, Ocean, Mountain, Plains, Mistlands, Ashlands, DeepNorth. Server-synced and locked in multiplayer.", 90));
			LevelLock = file.Bind<bool>("Bait", "Level lock bait", false, Describe("Require the bait's levelUnlock Fishing skill to equip, begin charging and cast. Checks and lock messages are entirely local to the controlling client, before draw/cast animations and bait consumption. Effective skill includes equipment, buffs and temporary bonuses; there are no added fishing RPCs or idle skill polling. 0 is unrestricted. Server-synced and locked in multiplayer.", 80));
			RodSection = file.Bind<bool>("Fishing Rods", "Enable Section", true, Describe("Enable fishing rod recipes. False removes recipe registration hooks and restores previous recipes. Config and multiplayer authority essentials remain. Server-synced and locked in multiplayer.", 100));
			CraftRod = file.Bind<bool>("Fishing Rods", "Craftable Fishingrod", true, Describe("Craft FishingRod at a level 2 Workbench. Existing rod recipes are replaced while enabled and restored when disabled. Server-synced and locked in multiplayer.", 90));
			RodRecipe = file.Bind<string>("Fishing Rods", "Fishingrod Recipe", "Wood[20],Resin[5]", Describe("Materials using exact prefab names: Item1[amount],Item2[amount]. Positive whole amounts, at most 32 unique ingredients. Default: Wood[20],Resin[5]. Server-synced and locked in multiplayer.", 80));
			RemoveTreasure = file.Bind<bool>("Misc", "Remove default treasure", false, Describe("Remove the extra-drop tables on vanilla Fish1–Fish12 and Fish4_cave when caught by fishing, including amber, stone and other biome loot. The fish itself still drops. Custom fish are unaffected. Server-synced and locked in multiplayer.", 100));
			try
			{
				Local = Capture();
			}
			catch (FormatException ex)
			{
				Plugin.Warn("Invalid settings; using safe recipe defaults: " + ex.Message);
				Local = new Settings(BaitSection.Value, LowerTiers.Value, LevelLock.Value, RodSection.Value, craft: false, "Wood[20],Resin[5]", RemoveTreasure.Value);
				ApplyValues(Local);
			}
			file.Save();
			RememberFile();
			file.SettingChanged += Changed;
			Instance = this;
		}

		internal void RememberFile()
		{
			configInfo.Refresh();
			if (configInfo.Exists)
			{
				observedWrite = configInfo.LastWriteTimeUtc;
				observedLength = configInfo.Length;
				filePending = false;
			}
		}

		internal void PollFile(float now)
		{
			if (disposed || now < nextPoll)
			{
				return;
			}
			nextPoll = now + 1f;
			try
			{
				configInfo.Refresh();
				if (!configInfo.Exists)
				{
					return;
				}
				if (configInfo.LastWriteTimeUtc != observedWrite || configInfo.Length != observedLength)
				{
					observedWrite = configInfo.LastWriteTimeUtc;
					observedLength = configInfo.Length;
					filePending = true;
					reloadAt = now + 0.25f;
				}
				else
				{
					if (!filePending || now < reloadAt)
					{
						return;
					}
					ReloadScope reloadScope = BeforeReload();
					Exception error = null;
					try
					{
						File.Reload();
					}
					catch (Exception ex)
					{
						error = ex;
						throw;
					}
					finally
					{
						reloadScope?.Finish(error);
					}
					RememberFile();
					fileError = null;
				}
			}
			catch (Exception ex2)
			{
				if (fileError != ex2.Message)
				{
					Plugin.Warn("Config file reload not applied: " + ex2.Message);
				}
				fileError = ex2.Message;
				reloadAt = now + 1f;
			}
		}

		private Settings Capture()
		{
			return new Settings(BaitSection.Value, LowerTiers.Value, LevelLock.Value, RodSection.Value, CraftRod.Value, RodRecipe.Value, RemoveTreasure.Value, Local?.Yaml ?? "");
		}

		private void ApplyValues(Settings values)
		{
			BaitSection.Value = values.Bait;
			LowerTiers.Value = values.Lower;
			LevelLock.Value = values.Lock;
			RodSection.Value = values.Rods;
			CraftRod.Value = values.Craft;
			RodRecipe.Value = values.RodRecipe;
			RemoveTreasure.Value = values.Treasure;
		}

		internal void SetYaml(string text)
		{
			if (!Client && !(text == Local.Yaml))
			{
				Local = Local.WithYaml(text);
				this.Refresh?.Invoke();
				this.AuthorityChanged?.Invoke();
			}
		}

		private void Overlay()
		{
			applying++;
			try
			{
				ApplyValues(Current);
			}
			finally
			{
				applying--;
			}
			foreach (ConfigurationManagerAttributes tag in tags)
			{
				tag.ReadOnly = Client;
			}
			this.Refresh?.Invoke();
		}

		internal void SetClient(bool client)
		{
			if (Client != client)
			{
				Client = client;
				Remote = null;
				Overlay();
			}
		}

		internal void WaitForServer()
		{
			if (Client)
			{
				Remote = null;
				Overlay();
			}
		}

		internal void Receive(Settings values)
		{
			if (Client && values != null)
			{
				Remote = values;
				Overlay();
			}
		}

		private void Changed(object sender, SettingChangedEventArgs args)
		{
			if (applying != 0 || disposed)
			{
				return;
			}
			if (Client)
			{
				Overlay();
				return;
			}
			Settings local = Local;
			Settings local2;
			try
			{
				local2 = Capture();
			}
			catch (FormatException ex)
			{
				Plugin.Warn("Config edit not applied: " + ex.Message);
				Overlay();
				return;
			}
			Local = local2;
			Overlay();
			if (!Client && !Local.Same(local))
			{
				this.AuthorityChanged?.Invoke();
			}
			try
			{
				File.Save();
				RememberFile();
			}
			catch (Exception ex2)
			{
				Plugin.Warn("Could not save local config: " + ex2.Message);
			}
		}

		internal SaveScope BeforeSave()
		{
			if (applying != 0 || !Client || Remote == null)
			{
				return null;
			}
			applying++;
			try
			{
				ApplyValues(Local);
				return new SaveScope(this);
			}
			catch
			{
				applying--;
				throw;
			}
		}

		internal ReloadScope BeforeReload()
		{
			if (applying != 0)
			{
				return null;
			}
			applying++;
			try
			{
				ApplyValues(Local);
				return new ReloadScope(this, Local);
			}
			catch
			{
				applying--;
				throw;
			}
		}

		public void Dispose()
		{
			if (!disposed)
			{
				disposed = true;
				File.SettingChanged -= Changed;
				Client = false;
				Remote = null;
				Overlay();
				File.SaveOnConfigSet = autoSave;
				if (Instance == this)
				{
					Instance = null;
				}
			}
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Save")]
	internal static class SaveLocalPreferences
	{
		private static void Prefix(ConfigFile __instance, out LiveConfig.SaveScope __state)
		{
			LiveConfig instance = LiveConfig.Instance;
			__state = ((instance != null && instance.File == __instance) ? instance.BeforeSave() : null);
		}

		private static void Postfix(ConfigFile __instance, LiveConfig.SaveScope __state)
		{
			__state?.Finish();
			LiveConfig instance = LiveConfig.Instance;
			if (instance != null && instance.File == __instance)
			{
				instance.RememberFile();
			}
		}

		private static Exception Finalizer(Exception __exception, LiveConfig.SaveScope __state)
		{
			__state?.Finish();
			return __exception;
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Reload")]
	internal static class ReloadLocalPreferences
	{
		private static void Prefix(ConfigFile __instance, out LiveConfig.ReloadScope __state)
		{
			LiveConfig instance = LiveConfig.Instance;
			__state = ((instance != null && instance.File == __instance) ? instance.BeforeReload() : null);
		}

		private static void Postfix(LiveConfig.ReloadScope __state)
		{
			__state?.Finish(null);
		}

		private static Exception Finalizer(Exception __exception, LiveConfig.ReloadScope __state)
		{
			__state?.Finish(__exception);
			return __exception;
		}
	}
}