Decompiled source of FieldFilleting v1.0.1

BepInEx/plugins/dannanboi_FieldFilleting.dll

Decompiled 8 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Field Filleting")]
[assembly: AssemblyDescription("Hand filleting with live server-authoritative settings and optional species-specific fillets.")]
[assembly: AssemblyCompany("dannanboi")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyVersion("1.0.1.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 FishGrazingAsImGoing
{
	[BepInPlugin("dannanboi.fieldfilleting", "Field Filleting", "1.0.1")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "dannanboi.fieldfilleting";

		public const string PluginName = "Field Filleting";

		public const string PluginVersion = "1.0.1";

		internal static Plugin Instance;

		internal static bool RefreshPending;

		internal static bool LastKnifeState;

		internal static bool ForceRefresh;

		private Harmony harmony;

		private LiveSettings live;

		private SettingsNetwork sync;

		private FilletData filletData;

		private static MethodInfo refreshPanel;

		private void Awake()
		{
			//IL_0178: Unknown result type (might be due to invalid IL or missing references)
			//IL_0182: Expected O, but got Unknown
			try
			{
				if (LegacyFiles.CopyIfMissing(Path.Combine(Paths.ConfigPath, "dannanboi.FishGrazingAsImGoing.cfg"), ((BaseUnityPlugin)this).Config.ConfigFilePath))
				{
					((BaseUnityPlugin)this).Config.Reload();
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not import the previous config: " + ex.Message));
			}
			ModSettings.Bind(((BaseUnityPlugin)this).Config);
			refreshPanel = AccessTools.Method(typeof(InventoryGui), "UpdateCraftingPanel", new Type[1] { typeof(bool) }, (Type[])null);
			if (refreshPanel == null || AccessTools.Field(typeof(InventoryGui), "m_craftRecipe")?.FieldType != typeof(Recipe) || AccessTools.Field(typeof(InventoryGui), "m_multiCrafting")?.FieldType != typeof(bool) || AccessTools.Field(typeof(InventoryGui), "m_multiCraftAmount")?.FieldType != typeof(int) || AccessTools.Field(typeof(InventoryGui), "m_craftTimer")?.FieldType != typeof(float))
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Unsupported crafting UI. Field Filleting has not been enabled.");
				return;
			}
			Instance = this;
			harmony = new Harmony("dannanboi.fieldfilleting");
			try
			{
				UniqueFilletItems.ValidateApi();
				harmony.PatchAll(typeof(Plugin).Assembly);
				filletData = new FilletData(Path.Combine(Paths.ConfigPath, "FieldFilleting.Fillets.yaml"));
				live = new LiveSettings(((BaseUnityPlugin)this).Config);
				sync = new SettingsNetwork(live);
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Field Filleting 1.0.1 loaded. Settings are synchronized and locked on multiplayer clients.");
			}
			catch (Exception ex2)
			{
				sync?.Dispose();
				live?.Dispose();
				filletData?.Dispose();
				harmony.UnpatchSelf();
				Instance = null;
				((BaseUnityPlugin)this).Logger.LogError((object)("Could not patch the crafting UI; mod disabled. " + ex2));
			}
		}

		private void Update()
		{
			sync?.Tick(Time.realtimeSinceStartup);
			live?.Tick();
			filletData?.Tick();
			UniqueFilletItems.RetryLocalization(Time.realtimeSinceStartup);
		}

		private void LateUpdate()
		{
			if (!RefreshPending)
			{
				return;
			}
			RefreshPending = false;
			Player localPlayer = Player.m_localPlayer;
			if (!((Object)(object)localPlayer == (Object)null) && !((Object)(object)InventoryGui.instance == (Object)null) && InventoryGui.IsVisible())
			{
				bool flag = Filleting.HasKnife(localPlayer);
				bool forceRefresh = ForceRefresh;
				ForceRefresh = false;
				if (forceRefresh || flag != LastKnifeState)
				{
					LastKnifeState = flag;
					refreshPanel.Invoke(InventoryGui.instance, new object[1] { false });
				}
			}
		}

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

		private void OnDestroy()
		{
			sync?.Dispose();
			live?.Dispose();
			filletData?.Dispose();
			MovementGuard.Reset();
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			Filleting.Reset();
			UniqueFilletItems.Dispose();
			RefreshPending = (ForceRefresh = false);
			CraftPatch.Active = null;
			Instance = null;
		}
	}
	internal static class LegacyFiles
	{
		internal const string PluginGuid = "dannanboi.FishGrazingAsImGoing";

		internal const string FilletsFileName = "FishGrazingAsImGoing.Fillets.yaml";

		internal static bool CopyIfMissing(string source, string destination)
		{
			if (File.Exists(destination) || !File.Exists(source))
			{
				return false;
			}
			Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(destination)));
			File.Copy(source, destination, overwrite: false);
			return true;
		}
	}
	internal static class Filleting
	{
		private static ObjectDB database;

		private static Recipe source;

		internal static Recipe HandRecipe;

		private static bool warnedMissing;

		internal static bool HasKnife(Player player)
		{
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			Inventory inventory = ((Humanoid)player).GetInventory();
			if (inventory == null)
			{
				return false;
			}
			foreach (ItemData allItem in inventory.GetAllItems())
			{
				if (allItem != null && allItem.m_stack > 0 && IsEligibleKnife(allItem))
				{
					return true;
				}
			}
			return false;
		}

		internal static bool IsEligibleKnife(ItemData item)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Invalid comparison between Unknown and I4
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Invalid comparison between Unknown and I4
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Invalid comparison between Unknown and I4
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Invalid comparison between Unknown and I4
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Invalid comparison between Unknown and I4
			if (item == null || item.m_shared == null)
			{
				return false;
			}
			ItemType itemType = item.m_shared.m_itemType;
			if ((int)itemType == 3 || (int)itemType == 14 || (int)itemType == 22 || (int)itemType == 19)
			{
				if ((int)item.m_shared.m_skillType != 2)
				{
					if ((Object)(object)item.m_dropPrefab != (Object)null)
					{
						return ((Object)item.m_dropPrefab).name == "KnifeButcher";
					}
					return false;
				}
				return true;
			}
			return false;
		}

		internal static bool IsHandRecipe(Recipe recipe)
		{
			if ((Object)(object)recipe != (Object)null)
			{
				if (recipe != HandRecipe)
				{
					return UniqueFilletRecipes.IsRecipe(recipe);
				}
				return true;
			}
			return false;
		}

		internal static bool CanUse(Player player, Recipe recipe = null)
		{
			if (SettingsNetwork.CanFillet && (Object)(object)player != (Object)null && (Object)(object)player == (Object)(object)Player.m_localPlayer && (Object)(object)player.GetCurrentCraftingStation() == (Object)null && HasKnife(player) && ((Object)(object)recipe == (Object)null || UniqueFilletRecipes.IsRecipe(recipe) == ModSettings.UniqueFillets.Value) && (Object)(object)source != (Object)null)
			{
				return source.m_enabled;
			}
			return false;
		}

		internal static Recipe GetHandRecipe()
		{
			ObjectDB instance = ObjectDB.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return null;
			}
			if ((Object)(object)database != (Object)(object)instance)
			{
				Reset();
				database = instance;
			}
			if ((Object)(object)source == (Object)null)
			{
				foreach (Recipe recipe in instance.m_recipes)
				{
					if ((Object)(object)recipe != (Object)null && ((Object)recipe).name == "Recipe_Fish1")
					{
						source = recipe;
						break;
					}
				}
				if ((Object)(object)source == (Object)null || (Object)(object)source.m_item == (Object)null || source.m_resources == null)
				{
					source = null;
					if (!warnedMissing && instance.m_recipes.Count > 0)
					{
						warnedMissing = true;
						Plugin.Warn("Could not find Recipe_Fish1. Hand filleting is unavailable; preparation-table recipes are unchanged.");
					}
					return null;
				}
			}
			if (!source.m_enabled)
			{
				return null;
			}
			if ((Object)(object)HandRecipe == (Object)null)
			{
				HandRecipe = Object.Instantiate<Recipe>(source);
				((Object)HandRecipe).name = "FieldFilleting_HandFilleting";
				((Object)HandRecipe).hideFlags = (HideFlags)61;
				HandRecipe.m_craftingStation = null;
				HandRecipe.m_repairStation = null;
				HandRecipe.m_minStationLevel = 1;
			}
			HandRecipe.m_item = source.m_item;
			HandRecipe.m_amount = source.m_amount;
			HandRecipe.m_enabled = source.m_enabled;
			HandRecipe.m_resources = source.m_resources;
			HandRecipe.m_requireOnlyOneIngredient = source.m_requireOnlyOneIngredient;
			HandRecipe.m_qualityResultAmountMultiplier = source.m_qualityResultAmountMultiplier;
			HandRecipe.m_listSortWeight = source.m_listSortWeight;
			return HandRecipe;
		}

		internal static void AddHandRecipe(Player player, List<Recipe> recipes)
		{
			if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer || recipes == null)
			{
				return;
			}
			bool flag = (Plugin.LastKnifeState = HasKnife(player));
			for (int num = recipes.Count - 1; num >= 0; num--)
			{
				if (IsHandRecipe(recipes[num]))
				{
					recipes.RemoveAt(num);
				}
			}
			if (!SettingsNetwork.CanFillet || !flag || (Object)(object)player.GetCurrentCraftingStation() != (Object)null)
			{
				return;
			}
			Recipe handRecipe = GetHandRecipe();
			if (!((Object)(object)handRecipe == (Object)null))
			{
				if (ModSettings.UniqueFillets.Value)
				{
					UniqueFilletRecipes.Add(source, recipes);
				}
				else
				{
					recipes.Add(handRecipe);
				}
			}
		}

		internal static void Reset()
		{
			if ((Object)(object)HandRecipe != (Object)null)
			{
				Object.Destroy((Object)(object)HandRecipe);
			}
			HandRecipe = null;
			UniqueFilletRecipes.Reset();
			source = null;
			database = null;
			warnedMissing = false;
		}
	}
	[HarmonyPatch(typeof(Player), "GetAvailableRecipes")]
	internal static class AvailableRecipesPatch
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance, ref List<Recipe> __0)
		{
			Filleting.AddHandRecipe(__instance, __0);
		}
	}
	[HarmonyPatch(typeof(Player), "HaveRequirements", new Type[]
	{
		typeof(Recipe),
		typeof(bool),
		typeof(int),
		typeof(int)
	})]
	internal static class RequirementsPatch
	{
		[HarmonyPrefix]
		internal static bool Prefix(Player __instance, Recipe __0, ref bool __result)
		{
			if (!Filleting.IsHandRecipe(__0) || Filleting.CanUse(__instance, __0))
			{
				return true;
			}
			__result = false;
			return false;
		}
	}
	[HarmonyPatch(typeof(Player), "OnInventoryChanged")]
	internal static class InventoryChangedPatch
	{
		[HarmonyPostfix]
		internal static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				MovementGuard.InventoryChanged(__instance);
				Plugin.RefreshPending = true;
			}
		}
	}
	internal static class ModSettings
	{
		internal static ConfigEntry<float> FumbleChance;

		internal static ConfigEntry<float> BalancingMultiplier;

		internal static ConfigEntry<float> CookingBaseValue;

		internal static ConfigEntry<float> CookingMultiplier;

		internal static ConfigEntry<float> FishYieldReduction;

		internal static ConfigEntry<bool> SkillBalancing;

		internal static ConfigEntry<bool> CookingSkillGain;

		internal static ConfigEntry<bool> AddToTooltip;

		internal static ConfigEntry<bool> CancelOnMovement;

		internal static ConfigEntry<bool> UniqueFillets;

		internal static ConfigEntry<string> FumbleMessage;

		internal static ConfigEntry<string> TooltipText;

		internal static readonly ConfigurationManagerAttributes LockTag = new ConfigurationManagerAttributes();

		private static ConfigEntry<T> Synced<T>(ConfigFile config, string key, T value, string description)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			return Synced(config, key, value, new ConfigDescription(description, (AcceptableValueBase)null, Array.Empty<object>()));
		}

		private static ConfigEntry<T> Synced<T>(ConfigFile config, string key, T value, ConfigDescription description)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Expected O, but got Unknown
			return config.Bind<T>("Settings", key, value, new ConfigDescription(description.Description + " Server-controlled and locked on multiplayer clients.", description.AcceptableValues, new object[1] { LockTag }));
		}

		internal static void Bind(ConfigFile config)
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected O, but got Unknown
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Expected O, but got Unknown
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00da: Expected O, but got Unknown
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_010d: Expected O, but got Unknown
			//IL_0142: Unknown result type (might be due to invalid IL or missing references)
			//IL_014c: Expected O, but got Unknown
			FumbleChance = Synced(config, "Fumble chance", 0.1f, new ConfigDescription("Chance per completed hand-filleting action: 0.01 = 1%, 1 = 100%. 0 disables fumbles. A fumble consumes the fish and produces no fillets or XP. A multi-craft batch shares one roll.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			FumbleMessage = Synced(config, "Fumble message", "You fumbled the filleting and ruined the fish.", "Normal yellow center-screen message on a fumble. Empty or whitespace disables it.");
			SkillBalancing = Synced(config, "Skill balancing", value: true, "Reduce fumble chance using the player's current Cooking skill.");
			BalancingMultiplier = Synced(config, "Balancing multiplier", 1f, new ConfigDescription("Effective chance = Fumble chance * max(0, 1 - Cooking skill / 100 * multiplier). Recommended 1: half the chance at skill 50, no fumbles at 100. 0.5 leaves half the risk at 100; 2 removes risk at 50.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>()));
			CookingSkillGain = Synced(config, "Cooking skill gain", value: true, "Award Cooking XP only after successful hand filleting. Failed, cancelled and blocked attempts give none. Preparation-table XP is unchanged.");
			CookingBaseValue = Synced(config, "Cooking skill base value", 1f, new ConfigDescription("Base amount passed to Valheim's Cooking skill gain routine per successful recipe execution. 1 is one normal skill-gain unit, using the game's Cooking increase step and level progression curve; it is not one skill level. Status-effect/world XP modifiers still apply. Range: 0.1 to 40.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 40f), Array.Empty<object>()));
			CookingMultiplier = Synced(config, "Cooking skill multiplier", 1f, new ConfigDescription("Multiply Cooking skill base value. 1 = normal gain, 10 = ten times the gain. Batch gain scales with the number of successful recipe executions, not the number of fillets produced.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 10f), Array.Empty<object>()));
			CancelOnMovement = Synced(config, "Cancel on movement", value: true, "Cancel hand filleting when the player moves more than 5 cm from the start position. Turning in place does not cancel. Cancelled attempts consume no fish and grant no XP.");
			FishYieldReduction = Synced(config, "Fish yield reduction", 0f, new ConfigDescription("Reduce hand-filleting output: 0 disables, 0.01 = 1%, 0.5 = 50%, 0.99 = 99%. Round up per fish, minimum 1 per fish, including batches. Preparation-table yields are unchanged.", (AcceptableValueBase)(object)new YieldReductionRange(), Array.Empty<object>()));
			UniqueFillets = Synced(config, "Unique fillets", value: false, "Hand filleting produces a separate raw-fish item for each species. Prefab names: FGAGI_RawFish_<fish prefab name>, for example FGAGI_RawFish_Fish1. Items stay registered when disabled so existing fillets remain loadable. Vanilla recipes are not automatically changed to accept these ingredients; use other mods to add recipes. Preparation-table output is unchanged.");
			AddToTooltip = config.Bind<bool>("UI", "Add to tooltip", true, "Append Tooltip text to eligible knife and dagger tooltips. Local preference.");
			TooltipText = config.Bind<string>("UI", "Tooltip text", "Can be used to fillet fish", "Text appended to eligible knife/dagger tooltips when Add to tooltip is enabled. Empty or whitespace adds nothing. Local preference.");
		}

		internal static float FiniteRange(float value, float min, float max, float fallback)
		{
			if (float.IsNaN(value) || float.IsInfinity(value))
			{
				value = fallback;
			}
			return Math.Max(min, Math.Min(max, value));
		}

		internal static float EffectiveChance(float skillFactor)
		{
			float num = FiniteRange(FumbleChance.Value, 0f, 1f, 0.1f);
			if (num == 0f || !SkillBalancing.Value)
			{
				return num;
			}
			float num2 = FiniteRange(skillFactor, 0f, 1f, 0f);
			float num3 = FiniteRange(BalancingMultiplier.Value, 0f, 10f, 1f);
			return num * Math.Max(0f, 1f - num2 * num3);
		}

		internal static bool Fumbles(float chance, float roll)
		{
			if (!(chance >= 1f))
			{
				if (chance > 0f)
				{
					return roll < chance;
				}
				return false;
			}
			return true;
		}

		internal static float Experience(int attempts)
		{
			if (!CookingSkillGain.Value)
			{
				return 0f;
			}
			return FiniteRange(CookingBaseValue.Value, 0.1f, 40f, 1f) * FiniteRange(CookingMultiplier.Value, 1f, 10f, 1f) * (float)Math.Max(0, attempts);
		}
	}
	internal sealed class YieldReductionRange : AcceptableValueRange<float>
	{
		internal YieldReductionRange()
			: base(0f, 0.99f)
		{
		}

		public override object Clamp(object value)
		{
			float num = ModSettings.FiniteRange((float)value, 0f, 0.99f, 0f);
			return (num > 0f && num < 0.01f) ? 0.01f : num;
		}

		public override bool IsValid(object value)
		{
			float num = (float)value;
			if (num != 0f)
			{
				if (num >= 0.01f)
				{
					return num <= 0.99f;
				}
				return false;
			}
			return true;
		}

		public override string ToDescriptionString()
		{
			return "# Acceptable values: 0 (disabled), or 0.01 to 0.99";
		}
	}
	internal sealed class CraftState
	{
		internal Player Player;

		internal Inventory Inventory;

		internal string OutputName;

		internal int OutputBefore;

		internal int Attempts;

		internal CraftState Previous;
	}
	[HarmonyPatch(typeof(Recipe), "GetAmount")]
	internal static class FishYieldPatch
	{
		[HarmonyPostfix]
		internal static void Postfix(Recipe __instance, int __1, ref int __result)
		{
			if (Filleting.IsHandRecipe(__instance) && __result > 0)
			{
				float num = ModSettings.FiniteRange(ModSettings.FishYieldReduction.Value, 0f, 0.99f, 0f);
				if (num != 0f)
				{
					int num2 = Math.Max(1, __1);
					decimal num3 = (decimal)__result / (decimal)num2;
					decimal num4 = Math.Max(1m, Math.Ceiling(num3 * (1m - (decimal)num)));
					__result = (int)Math.Min(2147483647m, num4 * (decimal)num2);
				}
			}
		}
	}
	[HarmonyPatch(typeof(InventoryGui), "DoCrafting")]
	internal static class CraftPatch
	{
		[ThreadStatic]
		internal static CraftState Active;

		internal static int CountOutput(Inventory inventory, string name)
		{
			int num = 0;
			foreach (ItemData allItem in inventory.GetAllItems())
			{
				if (allItem != null && allItem.m_shared != null && allItem.m_shared.m_name == name)
				{
					num += allItem.m_stack;
				}
			}
			return num;
		}

		[HarmonyPrefix]
		internal static bool Prefix(InventoryGui __instance, Player __0, Recipe ___m_craftRecipe, bool ___m_multiCrafting, int ___m_multiCraftAmount, ref float ___m_craftTimer, out CraftState __state)
		{
			__state = null;
			if (!Filleting.IsHandRecipe(___m_craftRecipe))
			{
				return true;
			}
			if (!Filleting.CanUse(__0, ___m_craftRecipe))
			{
				___m_craftTimer = -1f;
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
				return false;
			}
			int num = ((!___m_multiCrafting) ? 1 : Math.Max(1, ___m_multiCraftAmount));
			Inventory inventory = ((Humanoid)__0).GetInventory();
			int num2 = default(int);
			ItemData val = default(ItemData);
			int amount = ___m_craftRecipe.GetAmount(1, ref num2, ref val, num);
			if (amount <= 0 || num2 <= 0 || val == null || val.m_stack < num2 || !inventory.GetAllItems().Contains(val) || !__0.HaveRequirements(___m_craftRecipe, false, 1, num) || !inventory.CanAddItem(___m_craftRecipe.m_item.m_itemData, amount))
			{
				___m_craftTimer = -1f;
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
				return false;
			}
			float num3 = ModSettings.EffectiveChance(((Character)__0).GetSkillFactor((SkillType)105));
			if (ModSettings.Fumbles(num3, (num3 > 0f) ? Random.value : 1f))
			{
				bool num4 = inventory.RemoveItem(val, num2);
				___m_craftTimer = -1f;
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
				string value = ModSettings.FumbleMessage.Value;
				if (num4 && !string.IsNullOrWhiteSpace(value))
				{
					MessageCompat.Show(__0, value);
				}
				return false;
			}
			__state = new CraftState
			{
				Player = __0,
				Inventory = inventory,
				OutputName = ___m_craftRecipe.m_item.m_itemData.m_shared.m_name,
				OutputBefore = CountOutput(inventory, ___m_craftRecipe.m_item.m_itemData.m_shared.m_name),
				Attempts = num,
				Previous = Active
			};
			Active = __state;
			return true;
		}

		[HarmonyPostfix]
		internal static void Postfix(CraftState __state)
		{
			if (__state == null)
			{
				return;
			}
			if (Active == __state)
			{
				Active = __state.Previous;
			}
			if (CountOutput(__state.Inventory, __state.OutputName) <= __state.OutputBefore)
			{
				return;
			}
			float num = ModSettings.Experience(1);
			if (num > 0f)
			{
				for (int i = 0; i < __state.Attempts; i++)
				{
					((Character)__state.Player).RaiseSkill((SkillType)105, num);
				}
			}
		}

		[HarmonyFinalizer]
		internal static Exception Finalizer(Exception __exception, CraftState __state, Recipe ___m_craftRecipe, ref float ___m_craftTimer)
		{
			if (__state != null && Active == __state)
			{
				Active = __state.Previous;
			}
			if (__exception != null && Filleting.IsHandRecipe(___m_craftRecipe))
			{
				___m_craftTimer = -1f;
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
			}
			return __exception;
		}
	}
	[HarmonyPatch(typeof(Player), "RaiseSkill")]
	internal static class NativeCraftExperiencePatch
	{
		[HarmonyPrefix]
		internal static bool Prefix(Player __instance, SkillType __0)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Invalid comparison between Unknown and I4
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Invalid comparison between Unknown and I4
			CraftState active = CraftPatch.Active;
			if (active != null && !((Object)(object)active.Player != (Object)(object)__instance))
			{
				if ((int)__0 != 105)
				{
					return (int)__0 != 107;
				}
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch]
	internal static class KnifeTooltipPatch
	{
		internal const string Hint = "Can be used to fillet fish";

		[HarmonyPrefix]
		private static void Prefix(ItemData __0)
		{
			UniqueFilletItems.RefreshData(__0);
		}

		private static MethodBase TargetMethod()
		{
			return AccessTools.Method(typeof(ItemData), "GetTooltip", new Type[6]
			{
				typeof(ItemData),
				typeof(int),
				typeof(bool),
				typeof(float),
				typeof(int),
				typeof(bool)
			}, (Type[])null) ?? AccessTools.Method(typeof(ItemData), "GetTooltip", new Type[5]
			{
				typeof(ItemData),
				typeof(int),
				typeof(bool),
				typeof(float),
				typeof(int)
			}, (Type[])null) ?? throw new MissingMethodException("No supported ItemData.GetTooltip overload was found.");
		}

		[HarmonyPostfix]
		internal static void Postfix(ItemData __0, ref string __result)
		{
			string value = ModSettings.TooltipText.Value;
			if (ModSettings.AddToTooltip.Value && !string.IsNullOrWhiteSpace(value) && Filleting.IsEligibleKnife(__0) && (__result == null || __result.IndexOf(value, StringComparison.Ordinal) < 0))
			{
				__result = (__result ?? "").TrimEnd() + "\n\n" + value;
			}
		}
	}
	internal static class MessageCompat
	{
		private static readonly MethodInfo PlayerMessage = Resolve(typeof(Player));

		private static bool warned;

		internal static MethodInfo Resolve(Type playerType)
		{
			return AccessTools.Method(playerType, "Message", new Type[5]
			{
				typeof(MessageType),
				typeof(string),
				typeof(int),
				typeof(Sprite),
				typeof(bool)
			}, (Type[])null) ?? AccessTools.Method(playerType, "Message", new Type[4]
			{
				typeof(MessageType),
				typeof(string),
				typeof(int),
				typeof(Sprite)
			}, (Type[])null);
		}

		internal static void Show(Player player, string message)
		{
			TryShow(player, PlayerMessage, message);
		}

		internal static bool TryShow(object player, MethodInfo method, string message)
		{
			if (player == null || string.IsNullOrWhiteSpace(message))
			{
				return false;
			}
			try
			{
				if (method == null)
				{
					throw new MissingMethodException("No supported Player.Message overload found.");
				}
				object[] parameters = ((method.GetParameters().Length != 5) ? new object[4]
				{
					(object)(MessageType)2,
					message,
					0,
					null
				} : new object[5]
				{
					(object)(MessageType)2,
					message,
					0,
					null,
					false
				});
				method.Invoke(player, parameters);
				return true;
			}
			catch (Exception ex)
			{
				if (!warned)
				{
					warned = true;
					Plugin.Warn("Could not show the fumble message. Crafting can continue. " + ex.GetBaseException().Message);
				}
				return false;
			}
		}
	}
	internal sealed class SettingsSnapshot
	{
		internal float Chance;

		internal float Balance;

		internal float BaseXP;

		internal float XPMultiplier;

		internal float YieldReduction;

		internal bool SkillBalance;

		internal bool GainXP;

		internal bool CancelMovement;

		internal bool UniqueFillets;

		internal string Message;

		internal static SettingsSnapshot Capture()
		{
			return new SettingsSnapshot
			{
				Chance = ModSettings.FumbleChance.Value,
				Message = (ModSettings.FumbleMessage.Value ?? ""),
				SkillBalance = ModSettings.SkillBalancing.Value,
				Balance = ModSettings.BalancingMultiplier.Value,
				GainXP = ModSettings.CookingSkillGain.Value,
				BaseXP = ModSettings.CookingBaseValue.Value,
				XPMultiplier = ModSettings.CookingMultiplier.Value,
				CancelMovement = ModSettings.CancelOnMovement.Value,
				YieldReduction = ModSettings.FishYieldReduction.Value,
				UniqueFillets = ModSettings.UniqueFillets.Value
			};
		}

		internal void Apply()
		{
			ModSettings.FumbleChance.Value = Chance;
			ModSettings.FumbleMessage.Value = Message;
			ModSettings.SkillBalancing.Value = SkillBalance;
			ModSettings.BalancingMultiplier.Value = Balance;
			ModSettings.CookingSkillGain.Value = GainXP;
			ModSettings.CookingBaseValue.Value = BaseXP;
			ModSettings.CookingMultiplier.Value = XPMultiplier;
			ModSettings.CancelOnMovement.Value = CancelMovement;
			ModSettings.FishYieldReduction.Value = YieldReduction;
			ModSettings.UniqueFillets.Value = UniqueFillets;
		}

		internal bool Same(SettingsSnapshot other)
		{
			if (other != null && Chance == other.Chance && Message == other.Message && SkillBalance == other.SkillBalance && Balance == other.Balance && GainXP == other.GainXP && BaseXP == other.BaseXP && XPMultiplier == other.XPMultiplier && CancelMovement == other.CancelMovement && YieldReduction == other.YieldReduction)
			{
				return UniqueFillets == other.UniqueFillets;
			}
			return false;
		}

		private static bool InRange(float n, float lo, float hi)
		{
			if (!float.IsNaN(n) && !float.IsInfinity(n) && n >= lo)
			{
				return n <= hi;
			}
			return false;
		}

		internal bool Valid()
		{
			if (InRange(Chance, 0f, 1f) && InRange(Balance, 0f, 10f) && InRange(BaseXP, 0.1f, 40f) && (YieldReduction == 0f || InRange(YieldReduction, 0.01f, 0.99f)) && InRange(XPMultiplier, 1f, 10f) && Message != null)
			{
				return Message.Length <= 8192;
			}
			return false;
		}

		internal void Write(ZPackage data)
		{
			data.Write(Chance);
			data.Write(Message);
			data.Write(SkillBalance);
			data.Write(Balance);
			data.Write(GainXP);
			data.Write(BaseXP);
			data.Write(XPMultiplier);
			data.Write(CancelMovement);
			data.Write(YieldReduction);
			data.Write(UniqueFillets);
		}

		internal static SettingsSnapshot Read(ZPackage data)
		{
			SettingsSnapshot obj = new SettingsSnapshot
			{
				Chance = data.ReadSingle(),
				Message = data.ReadString(),
				SkillBalance = data.ReadBool(),
				Balance = data.ReadSingle(),
				GainXP = data.ReadBool(),
				BaseXP = data.ReadSingle(),
				XPMultiplier = data.ReadSingle(),
				CancelMovement = data.ReadBool(),
				YieldReduction = data.ReadSingle(),
				UniqueFillets = data.ReadBool()
			};
			if (!obj.Valid())
			{
				throw new FormatException("Invalid server settings.");
			}
			return obj;
		}
	}
	internal sealed class ConfigurationManagerAttributes
	{
		public bool? ReadOnly = false;
	}
	internal sealed class LiveSettings : IDisposable
	{
		internal sealed class ReloadScope
		{
			private LiveSettings owner;

			private readonly SettingsSnapshot before;

			private readonly bool uiBefore;

			private readonly string tooltipBefore;

			internal ReloadScope(LiveSettings value, SettingsSnapshot previous, bool ui, string tooltip)
			{
				owner = value;
				before = previous;
				uiBefore = ui;
				tooltipBefore = tooltip;
			}

			internal void Finish(Exception error)
			{
				if (owner == null)
				{
					return;
				}
				LiveSettings liveSettings = owner;
				owner = null;
				try
				{
					SettingsSnapshot settingsSnapshot = SettingsSnapshot.Capture();
					if (error != null || !settingsSnapshot.Valid())
					{
						liveSettings.Local = before;
						ModSettings.AddToTooltip.Value = uiBefore;
						ModSettings.TooltipText.Value = tooltipBefore;
					}
					else
					{
						liveSettings.Local = settingsSnapshot;
					}
					(liveSettings.Locked ? (liveSettings.Remote ?? liveSettings.Local) : liveSettings.Local).Apply();
				}
				finally
				{
					liveSettings.applying--;
				}
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
				if (!liveSettings.Locked && !liveSettings.Local.Same(before))
				{
					liveSettings.AuthorityChanged?.Invoke();
				}
			}
		}

		internal sealed class SaveScope
		{
			private LiveSettings owner;

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

			internal void Finish()
			{
				if (owner == null)
				{
					return;
				}
				LiveSettings liveSettings = owner;
				owner = null;
				try
				{
					(liveSettings.Remote ?? liveSettings.Local).Apply();
				}
				finally
				{
					liveSettings.applying--;
				}
			}
		}

		internal static LiveSettings Instance;

		internal readonly ConfigFile Config;

		internal SettingsSnapshot Local;

		internal SettingsSnapshot Remote;

		private FileSystemWatcher watcher;

		private int applying;

		private long reloadAt;

		private bool disposed;

		private bool wasAutoSave;

		internal bool Locked { get; private set; }

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

		internal event Action AuthorityChanged;

		internal LiveSettings(ConfigFile config, bool watch = true)
		{
			Config = config;
			Local = SettingsSnapshot.Capture();
			wasAutoSave = config.SaveOnConfigSet;
			config.SaveOnConfigSet = false;
			config.SettingChanged += Changed;
			Instance = this;
			if (watch)
			{
				try
				{
					string fullPath = Path.GetFullPath(config.ConfigFilePath);
					watcher = new FileSystemWatcher(Path.GetDirectoryName(fullPath), Path.GetFileName(fullPath));
					watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
					watcher.Changed += FileChanged;
					watcher.Created += FileChanged;
					watcher.Renamed += FileRenamed;
					watcher.Error += WatchError;
					watcher.EnableRaisingEvents = true;
				}
				catch (Exception ex)
				{
					Plugin.Warn("Config file watching unavailable: " + ex.Message);
				}
			}
		}

		private void FileChanged(object sender, FileSystemEventArgs args)
		{
			QueueReload();
		}

		private void FileRenamed(object sender, RenamedEventArgs args)
		{
			QueueReload();
		}

		private void WatchError(object sender, ErrorEventArgs args)
		{
			QueueReload();
		}

		internal void QueueReload()
		{
			Interlocked.Exchange(ref reloadAt, DateTime.UtcNow.AddMilliseconds(750.0).Ticks);
		}

		internal void Tick()
		{
			long num = Interlocked.Read(in reloadAt);
			if (num != 0L && DateTime.UtcNow.Ticks >= num && Interlocked.CompareExchange(ref reloadAt, 0L, num) == num)
			{
				Reload();
			}
		}

		internal void Overlay(SettingsSnapshot values)
		{
			if (values != null)
			{
				applying++;
				try
				{
					values.Apply();
				}
				finally
				{
					applying--;
				}
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
			}
		}

		internal void SetClient(bool client)
		{
			if (Locked != client)
			{
				if (client)
				{
					Local = SettingsSnapshot.Capture();
				}
				Locked = client;
				Remote = null;
				ModSettings.LockTag.ReadOnly = client;
				if (!client)
				{
					Overlay(Local);
				}
				Plugin.RefreshPending = (Plugin.ForceRefresh = true);
			}
		}

		internal void WaitForServer()
		{
			if (Locked && Remote != null)
			{
				Remote = null;
				Overlay(Local);
			}
		}

		internal void Receive(SettingsSnapshot values)
		{
			if (Locked && values != null && values.Valid())
			{
				Remote = values;
				Overlay(Remote);
			}
		}

		private void Changed(object sender, SettingChangedEventArgs args)
		{
			if (applying != 0 || disposed)
			{
				return;
			}
			bool flag = args.ChangedSetting.Definition.Section == "Settings";
			if (flag && Locked)
			{
				Overlay(Remote ?? Local);
				return;
			}
			if (flag)
			{
				SettingsSnapshot settingsSnapshot = SettingsSnapshot.Capture();
				if (!settingsSnapshot.Valid())
				{
					Overlay(Local);
					Plugin.Warn("Invalid settings ignored.");
					return;
				}
				bool num = !settingsSnapshot.Same(Local);
				Local = settingsSnapshot;
				if (num)
				{
					this.AuthorityChanged?.Invoke();
				}
			}
			Plugin.RefreshPending = (Plugin.ForceRefresh = true);
			try
			{
				Config.Save();
			}
			catch (Exception ex)
			{
				Plugin.Warn("Could not save config: " + ex.Message);
			}
		}

		internal void Reload()
		{
			if (disposed)
			{
				return;
			}
			SettingsSnapshot local = Local;
			bool value = ModSettings.AddToTooltip.Value;
			string value2 = ModSettings.TooltipText.Value;
			applying++;
			try
			{
				Local.Apply();
				Config.Reload();
				SettingsSnapshot settingsSnapshot = SettingsSnapshot.Capture();
				if (!settingsSnapshot.Valid())
				{
					throw new FormatException("A Settings value is invalid.");
				}
				Local = settingsSnapshot;
			}
			catch (Exception ex)
			{
				Local = local;
				ModSettings.AddToTooltip.Value = value;
				ModSettings.TooltipText.Value = value2;
				Plugin.Warn("Config reload failed; keeping the last settings. " + ex.Message);
				if (ex is IOException)
				{
					QueueReload();
				}
			}
			finally
			{
				(Locked ? (Remote ?? Local) : Local).Apply();
				applying--;
			}
			Plugin.RefreshPending = (Plugin.ForceRefresh = true);
			if (!Locked && !Local.Same(local))
			{
				this.AuthorityChanged?.Invoke();
			}
		}

		internal ReloadScope BeforeExternalReload()
		{
			if (applying != 0)
			{
				return null;
			}
			ReloadScope result = new ReloadScope(this, Local, ModSettings.AddToTooltip.Value, ModSettings.TooltipText.Value);
			applying++;
			try
			{
				Local.Apply();
				return result;
			}
			catch
			{
				applying--;
				throw;
			}
		}

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

		public void Dispose()
		{
			if (!disposed)
			{
				watcher?.Dispose();
				watcher = null;
				Config.SettingChanged -= Changed;
				SetClient(client: false);
				disposed = true;
				Config.SaveOnConfigSet = wasAutoSave;
				if (Instance == this)
				{
					Instance = null;
				}
			}
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Reload")]
	internal static class PreserveReloadPatch
	{
		private static void Prefix(ConfigFile __instance, out LiveSettings.ReloadScope __state)
		{
			LiveSettings instance = LiveSettings.Instance;
			__state = ((instance != null && __instance == instance.Config) ? instance.BeforeExternalReload() : null);
		}

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

		private static Exception Finalizer(Exception __exception, LiveSettings.ReloadScope __state)
		{
			__state?.Finish(__exception);
			return __exception;
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Save")]
	internal static class PreserveLocalConfigPatch
	{
		private static void Prefix(ConfigFile __instance, out LiveSettings.SaveScope __state)
		{
			LiveSettings instance = LiveSettings.Instance;
			__state = ((instance != null && __instance == instance.Config) ? instance.BeforeSave() : null);
		}

		private static void Postfix(LiveSettings.SaveScope __state)
		{
			__state?.Finish();
		}

		private static Exception Finalizer(Exception __exception, LiveSettings.SaveScope __state)
		{
			__state?.Finish();
			return __exception;
		}
	}
	internal sealed class SettingsNetwork : IDisposable
	{
		private sealed class Peer
		{
			internal ZNetPeer Value;

			internal bool Compatible;

			internal bool SaidHello;

			internal bool Warned;

			internal float Started;

			internal SettingsSnapshot LastSettings;

			internal FilletCatalog LastFillets;

			internal float NextRequest;
		}

		internal const int Protocol = 5;

		internal const int SettingsPart = 1;

		internal const int FilletsPart = 2;

		internal const int Full = 3;

		private const float PublishInterval = 0.25f;

		internal const string HelloRpc = "dannanboi.fieldfilleting.ConfigHello";

		internal const string SettingsRpc = "dannanboi.fieldfilleting.ConfigSettings";

		internal const string RequestRpc = "dannanboi.fieldfilleting.ConfigRequest";

		internal static SettingsNetwork Instance;

		private readonly LiveSettings live;

		private readonly FilletData fillets;

		private ZNet network;

		private ZRpc server;

		private readonly Dictionary<ZRpc, Peer> peers = new Dictionary<ZRpc, Peer>();

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

		private bool dirty;

		private bool requestPending;

		private float nextCleanup;

		private float nextPublish;

		private float nextRequest;

		private SettingsSnapshot encodedSettings;

		private FilletCatalog encodedFillets;

		private readonly byte[][] packets = new byte[4][];

		internal static bool CanFillet
		{
			get
			{
				ZNet instance = ZNet.instance;
				if (!((Object)(object)instance == (Object)null) && !instance.IsServer())
				{
					if (Instance != null && Instance.network == instance && Instance.live.Locked && Instance.live.Ready && Instance.server != null)
					{
						return Instance.server.IsConnected();
					}
					return false;
				}
				return true;
			}
		}

		internal SettingsNetwork(LiveSettings settings)
		{
			live = settings;
			fillets = FilletData.Instance;
			Instance = this;
			live.AuthorityChanged += Changed;
			if (fillets != null)
			{
				fillets.AuthorityChanged += Changed;
			}
		}

		private void Changed()
		{
			dirty = true;
		}

		internal void Begin(ZNet value)
		{
			if (network != value)
			{
				live.SetClient(client: false);
				peers.Clear();
				network = value;
				dirty = false;
				server = null;
				requestPending = false;
				nextCleanup = (nextPublish = (nextRequest = 0f));
				encodedSettings = null;
				encodedFillets = null;
				Array.Clear(packets, 0, packets.Length);
				fillets?.SetClient(client: false);
				live.SetClient((Object)(object)value != (Object)null && !value.IsServer());
				fillets?.SetClient((Object)(object)value != (Object)null && !value.IsServer());
			}
		}

		internal void Register(ZNet net, ZNetPeer peer)
		{
			if ((Object)(object)net == (Object)null || peer == null || peer.m_rpc == null)
			{
				return;
			}
			Begin(net);
			if ((net.IsServer() || peer.m_server) && !peers.ContainsKey(peer.m_rpc))
			{
				if (!net.IsServer())
				{
					server = null;
					live.WaitForServer();
					fillets?.WaitForServer();
				}
				peers.Add(peer.m_rpc, new Peer
				{
					Value = peer,
					Started = Time.realtimeSinceStartup
				});
				peer.m_rpc.Register<string>("dannanboi.fieldfilleting.ConfigHello", (Action<ZRpc, string>)ReceiveHello);
				peer.m_rpc.Register<ZPackage>("dannanboi.fieldfilleting.ConfigSettings", (Action<ZRpc, ZPackage>)ReceiveSettings);
				if (net.IsServer())
				{
					peer.m_rpc.Register<int>("dannanboi.fieldfilleting.ConfigRequest", (Action<ZRpc, int>)ReceiveRequest);
				}
				peer.m_rpc.Invoke("dannanboi.fieldfilleting.ConfigHello", new object[1] { "1.0.1" });
			}
		}

		internal void ReceiveHello(ZRpc rpc, string version)
		{
			if (!((Object)(object)network == (Object)null) && peers.TryGetValue(rpc, out var value) && !value.SaidHello)
			{
				value.SaidHello = true;
				value.Compatible = string.Equals(version, "1.0.1", StringComparison.Ordinal);
				if (!value.Compatible)
				{
					Plugin.Warn("Field Filleting versions differ. Install 1.0.1 on server and clients for config sync.");
				}
				else if (network.IsServer())
				{
					Send(rpc, value);
				}
				else
				{
					server = rpc;
				}
			}
		}

		private byte[] Packet(int parts, SettingsSnapshot settings, FilletCatalog items)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Expected O, but got Unknown
			if (!settings.Same(encodedSettings))
			{
				encodedSettings = settings;
				packets[1] = (packets[3] = null);
			}
			if (!items.Same(encodedFillets))
			{
				encodedFillets = items;
				packets[2] = (packets[3] = null);
			}
			if (packets[parts] == null)
			{
				ZPackage val = new ZPackage();
				val.Write(5);
				val.Write(parts);
				if ((parts & 1) != 0)
				{
					settings.Write(val);
				}
				if ((parts & 2) != 0)
				{
					items.Write(val);
				}
				packets[parts] = val.GetArray();
			}
			return packets[parts];
		}

		private void Send(ZRpc rpc, Peer peer, bool full = false)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			SettingsSnapshot local = live.Local;
			FilletCatalog filletCatalog = fillets?.Local ?? FilletCatalog.Empty;
			int num = (full ? 3 : (((!local.Same(peer.LastSettings)) ? 1 : 0) | ((!filletCatalog.Same(peer.LastFillets)) ? 2 : 0)));
			if (num != 0 && rpc.IsConnected())
			{
				rpc.Invoke("dannanboi.fieldfilleting.ConfigSettings", new object[1] { (object)new ZPackage(Packet(num, local, filletCatalog)) });
				peer.LastSettings = local;
				peer.LastFillets = filletCatalog;
			}
		}

		internal void ReceiveRequest(ZRpc rpc, int protocol)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!((Object)(object)network == (Object)null) && network.IsServer() && protocol == 5 && peers.TryGetValue(rpc, out var value) && value.Compatible && !(realtimeSinceStartup < value.NextRequest))
			{
				value.NextRequest = realtimeSinceStartup + 2f;
				Send(rpc, value, full: true);
			}
		}

		internal void ReceiveSettings(ZRpc rpc, ZPackage data)
		{
			if ((Object)(object)network == (Object)null || network.IsServer() || !live.Locked || rpc != server || !peers.TryGetValue(rpc, out var value) || !value.Compatible || !rpc.IsConnected())
			{
				return;
			}
			try
			{
				if (data == null || data.Size() > 65536 || data.ReadInt() != 5)
				{
					throw new FormatException("Settings protocol mismatch or oversized packet.");
				}
				int num = data.ReadInt();
				if (num < 1 || num > 3)
				{
					throw new FormatException("Invalid settings sections.");
				}
				SettingsSnapshot settingsSnapshot = (((num & 1) != 0) ? SettingsSnapshot.Read(data) : live.Remote);
				FilletCatalog filletCatalog = (((num & 2) != 0) ? FilletCatalog.Read(data) : ((fillets == null) ? FilletCatalog.Empty : fillets.Remote));
				if (settingsSnapshot == null || filletCatalog == null)
				{
					throw new FormatException("A complete initial snapshot is required.");
				}
				if (!settingsSnapshot.Same(live.Remote))
				{
					live.Receive(settingsSnapshot);
				}
				if (fillets != null && !filletCatalog.Same(fillets.Remote))
				{
					fillets.Receive(filletCatalog);
				}
				requestPending = false;
			}
			catch (Exception ex)
			{
				live.WaitForServer();
				fillets?.WaitForServer();
				requestPending = true;
				Plugin.Warn("Server settings could not be applied. Hand filleting is disabled until settings arrive. " + ex.Message);
			}
		}

		internal void Tick(float now)
		{
			Begin(ZNet.instance);
			if ((Object)(object)network == (Object)null)
			{
				return;
			}
			if (!network.IsServer() && requestPending && server != null && server.IsConnected() && now >= nextRequest)
			{
				nextRequest = now + 2f;
				server.Invoke("dannanboi.fieldfilleting.ConfigRequest", new object[1] { 5 });
			}
			if (dirty && network.IsServer() && now >= nextPublish)
			{
				dirty = false;
				nextPublish = now + 0.25f;
				foreach (KeyValuePair<ZRpc, Peer> peer in peers)
				{
					if (peer.Value.Compatible)
					{
						Send(peer.Key, peer.Value);
					}
				}
			}
			if (now < nextCleanup)
			{
				return;
			}
			nextCleanup = now + 1f;
			stale.Clear();
			foreach (KeyValuePair<ZRpc, Peer> peer2 in peers)
			{
				if (!peer2.Key.IsConnected())
				{
					stale.Add(peer2.Key);
				}
				else if (!network.IsServer() && peer2.Value.Value.m_server && !live.Ready && !peer2.Value.Warned && now - peer2.Value.Started > 10f)
				{
					peer2.Value.Warned = true;
					Plugin.Warn("No synchronized settings received. Install Field Filleting 1.0.1 on the server. Hand filleting remains unavailable.");
				}
			}
			foreach (ZRpc item in stale)
			{
				if (item == server)
				{
					server = null;
					requestPending = false;
					live.WaitForServer();
					fillets?.WaitForServer();
				}
				peers.Remove(item);
			}
		}

		internal void End(ZNet value)
		{
			if (network == value)
			{
				Begin(null);
			}
		}

		public void Dispose()
		{
			live.AuthorityChanged -= Changed;
			Begin(null);
			if (fillets != null)
			{
				fillets.AuthorityChanged -= Changed;
			}
			if (Instance == this)
			{
				Instance = null;
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class SettingsConnectionPatch
	{
		private static void Prefix(ZNet __instance, ZNetPeer __0)
		{
			SettingsNetwork.Instance?.Register(__instance, __0);
		}
	}
	[HarmonyPatch(typeof(ZNet), "Shutdown")]
	internal static class SettingsShutdownPatch
	{
		private static void Postfix(ZNet __instance)
		{
			SettingsNetwork.Instance?.End(__instance);
			UniqueFilletItems.ResetRegistrationCache();
		}
	}
	internal static class MovementGuard
	{
		private static Player tracked;

		private static Vector3 origin;

		private static bool active;

		private static readonly FieldInfo craftRecipe = AccessTools.Field(typeof(InventoryGui), "m_craftRecipe");

		private static readonly FieldInfo craftTimer = AccessTools.Field(typeof(InventoryGui), "m_craftTimer");

		internal static void Reset()
		{
			active = false;
			tracked = null;
		}

		private static void Cancel(ref float timer)
		{
			timer = -1f;
			Reset();
			Plugin.RefreshPending = (Plugin.ForceRefresh = true);
		}

		internal static void InventoryChanged(Player player)
		{
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Expected O, but got Unknown
			InventoryGui instance = InventoryGui.instance;
			if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer) && !((Object)(object)instance == (Object)null) && Filleting.IsHandRecipe((Recipe)craftRecipe.GetValue(instance)) && !((float)craftTimer.GetValue(instance) < 0f) && !Filleting.HasKnife(player))
			{
				float timer = -1f;
				Cancel(ref timer);
				craftTimer.SetValue(instance, timer);
			}
		}

		internal static void Capture(Player player, Recipe recipe, float timer)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			Reset();
			if (!((Object)(object)player == (Object)null) && ModSettings.CancelOnMovement.Value && Filleting.IsHandRecipe(recipe) && !(timer < 0f))
			{
				tracked = player;
				origin = ((Component)player).transform.position;
				active = true;
			}
		}

		internal static void Check(Player player, Recipe recipe, ref float timer)
		{
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || !Filleting.IsHandRecipe(recipe) || timer < 0f)
			{
				Reset();
				return;
			}
			if (!Filleting.CanUse(player, recipe))
			{
				Cancel(ref timer);
				return;
			}
			if (!ModSettings.CancelOnMovement.Value)
			{
				Reset();
				return;
			}
			if (!active || (Object)(object)tracked != (Object)(object)player)
			{
				Capture(player, recipe, timer);
				return;
			}
			Vector3 val = ((Component)player).transform.position - origin;
			if (!(((Vector3)(ref val)).sqrMagnitude <= 0.0025f))
			{
				Cancel(ref timer);
			}
		}
	}
	[HarmonyPatch(typeof(InventoryGui), "OnCraftPressed")]
	internal static class CaptureMovementPatch
	{
		private static void Postfix(Recipe ___m_craftRecipe, float ___m_craftTimer)
		{
			MovementGuard.Capture(Player.m_localPlayer, ___m_craftRecipe, ___m_craftTimer);
		}
	}
	[HarmonyPatch(typeof(InventoryGui), "UpdateRecipe")]
	internal static class CancelMovementPatch
	{
		private static void Prefix(Player __0, Recipe ___m_craftRecipe, ref float ___m_craftTimer)
		{
			MovementGuard.Check(__0, ___m_craftRecipe, ref ___m_craftTimer);
		}
	}
	public static class FilletApi
	{
		public const string PrefabPrefix = "FGAGI_RawFish_";

		public static string GetPrefabName(string fishPrefabName)
		{
			if (string.IsNullOrEmpty(fishPrefabName))
			{
				throw new ArgumentException("A fish prefab name is required.", "fishPrefabName");
			}
			return "FGAGI_RawFish_" + fishPrefabName;
		}
	}
	internal static class UniqueFilletItems
	{
		private sealed class Details
		{
			internal string Prefab;

			internal string NameKey;

			internal string Title;

			internal string Description;

			internal string DescriptionKey;

			internal string DescriptionToken;

			internal float Weight;
		}

		private const string NamePrefix = "$fgagi_rawfish_";

		private static readonly Dictionary<string, GameObject> items = new Dictionary<string, GameObject>(StringComparer.Ordinal);

		private static readonly Dictionary<string, Details> details = new Dictionary<string, Details>(StringComparer.Ordinal);

		private static readonly HashSet<ItemDrop> drops = new HashSet<ItemDrop>();

		private static readonly HashSet<string> warnings = new HashSet<string>(StringComparer.Ordinal);

		private static readonly FieldInfo itemHashes = AccessTools.Field(typeof(ObjectDB), "m_itemByHash");

		private static readonly FieldInfo itemData = AccessTools.Field(typeof(ObjectDB), "m_itemByData");

		private static readonly FieldInfo sceneHashes = AccessTools.Field(typeof(ZNetScene), "m_namedPrefabs");

		private static readonly MethodInfo copy = AccessTools.Method(typeof(object), "MemberwiseClone", Type.EmptyTypes, (Type[])null);

		private static readonly FieldInfo localeCache = AccessTools.Field(typeof(Localization), "m_cache");

		private static readonly MethodInfo evictCache = ((localeCache == null) ? null : AccessTools.Method(localeCache.FieldType, "EvictAll", Type.EmptyTypes, (Type[])null));

		private static readonly MethodInfo totalWeight = AccessTools.Method(typeof(Inventory), "UpdateTotalWeight", Type.EmptyTypes, (Type[])null);

		private static GameObject root;

		private static ObjectDB lastDatabase;

		private static List<GameObject> lastItems;

		private static Dictionary<int, GameObject> lastItemHashes;

		private static Dictionary<SharedData, GameObject> lastItemData;

		private static int itemCount;

		private static int hashCount;

		private static int dataCount;

		private static ZNetScene lastScene;

		private static List<GameObject> lastPrefabs;

		private static Dictionary<int, GameObject> lastSceneHashes;

		private static int prefabCount;

		private static int sceneHashCount;

		private static int speciesCount;

		private static bool localizationPending;

		private static float nextLocalizationRetry;

		internal static void ValidateApi()
		{
			if (itemHashes?.FieldType != typeof(Dictionary<int, GameObject>) || itemData?.FieldType != typeof(Dictionary<SharedData, GameObject>) || sceneHashes?.FieldType != typeof(Dictionary<int, GameObject>) || copy == null || evictCache == null || totalWeight == null)
			{
				throw new MissingMemberException("Unsupported item/prefab registration API.");
			}
		}

		private static void WarnOnce(string key, string message)
		{
			if (warnings.Add(key))
			{
				Plugin.Warn(message);
			}
		}

		private static void WarnFailure(string operation, Exception ex)
		{
			if (ex is TargetInvocationException && ex.InnerException != null)
			{
				ex = ex.InnerException;
			}
			WarnOnce("failure:" + operation, "Field Filleting " + operation + " failed; Valheim loading continues. " + ex);
		}

		private static void Index(Dictionary<int, GameObject> index, GameObject prefab)
		{
			if (!((Object)(object)prefab == (Object)null))
			{
				int stableHashCode = StringExtensionMethods.GetStableHashCode(((Object)prefab).name);
				if (!index.TryGetValue(stableHashCode, out var value))
				{
					index.Add(stableHashCode, prefab);
				}
				else if ((Object)(object)value != (Object)(object)prefab)
				{
					index[stableHashCode] = null;
				}
			}
		}

		internal static void Ensure(ObjectDB db, bool force = false)
		{
			try
			{
				EnsureCore(db, force);
			}
			catch (Exception ex)
			{
				ResetRegistrationCache();
				WarnFailure("item registration", ex);
			}
		}

		private static void EnsureCore(ObjectDB db, bool force)
		{
			//IL_028b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0295: Expected O, but got Unknown
			//IL_01f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fe: Expected O, but got Unknown
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0129: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Invalid comparison between Unknown and I4
			if ((Object)(object)db == (Object)null || db.m_items == null)
			{
				return;
			}
			Dictionary<int, GameObject> dictionary = (Dictionary<int, GameObject>)itemHashes.GetValue(db);
			Dictionary<SharedData, GameObject> dictionary2 = (Dictionary<SharedData, GameObject>)itemData.GetValue(db);
			if (dictionary == null || dictionary2 == null || (!force && db == lastDatabase && db.m_items == lastItems && db.m_items.Count == itemCount && dictionary == lastItemHashes && dictionary.Count == hashCount && dictionary2 == lastItemData && dictionary2.Count == dataCount))
			{
				return;
			}
			GameObject val = null;
			List<GameObject> list = new List<GameObject>();
			Dictionary<int, GameObject> index = new Dictionary<int, GameObject>();
			foreach (GameObject item in db.m_items)
			{
				if (!((Object)(object)item == (Object)null))
				{
					Index(index, item);
					if (((Object)item).name == "FishRaw")
					{
						val = item;
					}
					ItemDrop component = item.GetComponent<ItemDrop>();
					if (component != null && (int)(component.m_itemData?.m_shared?.m_itemType).GetValueOrDefault() == 21)
					{
						list.Add(item);
					}
				}
			}
			if ((Object)(object)val == (Object)null || val.GetComponent<ItemDrop>()?.m_itemData?.m_shared == null)
			{
				return;
			}
			list.Sort((GameObject a, GameObject b) => string.Compare(((Object)a).name, ((Object)b).name, StringComparison.Ordinal));
			bool flag = false;
			foreach (GameObject item2 in list)
			{
				try
				{
					if (!items.TryGetValue(((Object)item2).name, out var value) || (Object)(object)value == (Object)null)
					{
						if ((Object)(object)root == (Object)null)
						{
							root = new GameObject("FieldFilleting_Prefabs");
							root.SetActive(false);
							((Object)root).hideFlags = (HideFlags)61;
							Object.DontDestroyOnLoad((Object)(object)root);
						}
						value = Object.Instantiate<GameObject>(val, root.transform, false);
						((Object)value).name = FilletApi.GetPrefabName(((Object)item2).name);
						((Object)value).hideFlags = (HideFlags)0;
						ItemDrop component2 = value.GetComponent<ItemDrop>();
						component2.m_itemData = val.GetComponent<ItemDrop>().m_itemData.Clone();
						component2.m_itemData.m_shared = (SharedData)copy.Invoke(component2.m_itemData.m_shared, null);
						string text = "fgagi_rawfish_" + ((Object)item2).name;
						string title = component2.m_itemData.m_shared.m_name + " (" + item2.GetComponent<ItemDrop>().m_itemData.m_shared.m_name + ")";
						component2.m_itemData.m_shared.m_name = "$" + text;
						string text2 = "fgagi_rawfish_desc_" + ((Object)item2).name;
						details["$" + text] = new Details
						{
							Prefab = ((Object)value).name,
							NameKey = text,
							Title = title,
							Description = (component2.m_itemData.m_shared.m_description ?? ""),
							DescriptionKey = text2,
							DescriptionToken = "$" + text2,
							Weight = component2.m_itemData.m_shared.m_weight
						};
						flag = true;
						RefreshData(component2.m_itemData);
						component2.m_itemData.m_dropPrefab = value;
						value.SetActive(true);
						items[((Object)item2).name] = value;
					}
					RegisterItem(db, value, dictionary, dictionary2, index);
				}
				catch (Exception ex)
				{
					WarnOnce(((Object)item2).name, "Could not register unique fillets for " + ((Object)item2).name + ": " + ex.Message);
				}
			}
			if (flag)
			{
				LocalizeCurrentNames();
			}
			lastDatabase = db;
			lastItems = db.m_items;
			itemCount = lastItems.Count;
			lastItemHashes = dictionary;
			hashCount = dictionary.Count;
			lastItemData = dictionary2;
			dataCount = dictionary2.Count;
		}

		private static void RegisterItem(ObjectDB db, GameObject prefab, Dictionary<int, GameObject> hashes, Dictionary<SharedData, GameObject> data, Dictionary<int, GameObject> index)
		{
			int stableHashCode = StringExtensionMethods.GetStableHashCode(((Object)prefab).name);
			if (hashes.TryGetValue(stableHashCode, out var value) && (Object)(object)value != (Object)(object)prefab)
			{
				WarnOnce(((Object)prefab).name, "Unique fillet prefab/hash collision for " + ((Object)prefab).name + "; that species is unavailable.");
				return;
			}
			bool flag = index.TryGetValue(stableHashCode, out value);
			if (flag && (Object)(object)value != (Object)(object)prefab)
			{
				WarnOnce(((Object)prefab).name, "Unique fillet prefab/hash collision for " + ((Object)prefab).name + "; that species is unavailable.");
				return;
			}
			if (!flag)
			{
				db.m_items.Add(prefab);
				index.Add(stableHashCode, prefab);
			}
			hashes[stableHashCode] = prefab;
			data[prefab.GetComponent<ItemDrop>().m_itemData.m_shared] = prefab;
		}

		internal static void RegisterScene(ZNetScene scene, bool beforeAwake, bool force = false)
		{
			try
			{
				RegisterSceneCore(scene, beforeAwake, force);
			}
			catch (Exception ex)
			{
				ResetRegistrationCache();
				WarnFailure("network prefab registration", ex);
			}
		}

		private static void RegisterSceneCore(ZNetScene scene, bool beforeAwake, bool force)
		{
			if ((Object)(object)scene == (Object)null || scene.m_prefabs == null)
			{
				return;
			}
			Ensure(ObjectDB.instance);
			Dictionary<int, GameObject> dictionary = (Dictionary<int, GameObject>)sceneHashes.GetValue(scene);
			if (!beforeAwake && !force && scene == lastScene && scene.m_prefabs == lastPrefabs && scene.m_prefabs.Count == prefabCount && dictionary == lastSceneHashes && (dictionary?.Count ?? 0) == sceneHashCount && items.Count == speciesCount)
			{
				return;
			}
			Dictionary<int, GameObject> dictionary2 = new Dictionary<int, GameObject>();
			foreach (GameObject prefab in scene.m_prefabs)
			{
				Index(dictionary2, prefab);
			}
			foreach (GameObject value2 in items.Values)
			{
				if ((Object)(object)value2 == (Object)null)
				{
					continue;
				}
				int stableHashCode = StringExtensionMethods.GetStableHashCode(((Object)value2).name);
				if (dictionary != null && dictionary.TryGetValue(stableHashCode, out var value) && (Object)(object)value != (Object)(object)value2)
				{
					WarnOnce(((Object)value2).name, "Unique fillet network prefab/hash collision for " + ((Object)value2).name + ".");
					continue;
				}
				bool flag = dictionary2.TryGetValue(stableHashCode, out value);
				if (flag && (Object)(object)value != (Object)(object)value2)
				{
					WarnOnce(((Object)value2).name, "Unique fillet network prefab/hash collision for " + ((Object)value2).name + ".");
					continue;
				}
				if (!flag)
				{
					scene.m_prefabs.Add(value2);
					dictionary2.Add(stableHashCode, value2);
				}
				if (!beforeAwake && dictionary != null)
				{
					dictionary[stableHashCode] = value2;
				}
			}
			if (!beforeAwake)
			{
				lastScene = scene;
				lastPrefabs = scene.m_prefabs;
				prefabCount = lastPrefabs.Count;
				lastSceneHashes = dictionary;
				sceneHashCount = dictionary?.Count ?? 0;
				speciesCount = items.Count;
			}
		}

		internal static ItemDrop Find(ObjectDB db, string species)
		{
			if ((Object)(object)db == (Object)null || !items.TryGetValue(species, out var value) || (Object)(object)value == (Object)null || (Object)(object)db.GetItemPrefab(((Object)value).name) != (Object)(object)value)
			{
				return null;
			}
			if ((Object)(object)ZNetScene.instance != (Object)null && (Object)(object)ZNetScene.instance.GetPrefab(((Object)value).name) != (Object)(object)value)
			{
				return null;
			}
			return value.GetComponent<ItemDrop>();
		}

		private static void AddName(Localization localization, string token, string name)
		{
			if (localization != null)
			{
				localization.AddWord(token, localization.Localize(name));
			}
		}

		internal static void LocalizeNames(Localization localization)
		{
			if (details.Count == 0)
			{
				localizationPending = false;
				return;
			}
			localizationPending = true;
			if (localization == null)
			{
				return;
			}
			try
			{
				object value = localeCache.GetValue(localization);
				if (value == null)
				{
					return;
				}
				foreach (Details value2 in details.Values)
				{
					FilletEntry filletEntry = FilletData.Instance?.Effective.Find(value2.Prefab);
					AddName(localization, value2.NameKey, string.IsNullOrWhiteSpace(filletEntry?.Title) ? value2.Title : filletEntry.Title);
					AddName(localization, value2.DescriptionKey, string.IsNullOrWhiteSpace(filletEntry?.Description) ? value2.Description : filletEntry.Description);
				}
				evictCache.Invoke(value, null);
				localizationPending = false;
			}
			catch (Exception ex)
			{
				WarnFailure("fillet localization", ex);
			}
		}

		private static void LocalizeCurrentNames()
		{
			localizationPending = true;
			try
			{
				LocalizeNames(Localization.instance);
			}
			catch (Exception ex)
			{
				WarnFailure("fillet localization", ex);
			}
		}

		internal static void RetryLocalization(float now)
		{
			if (localizationPending && !(now < nextLocalizationRetry))
			{
				nextLocalizationRetry = now + 1f;
				LocalizeCurrentNames();
			}
		}

		internal static void RefreshData(ItemData item)
		{
			SharedData val = item?.m_shared;
			if (val != null && val.m_name != null && val.m_name.StartsWith("$fgagi_rawfish_", StringComparison.Ordinal) && details.TryGetValue(val.m_name, out var value))
			{
				FilletEntry filletEntry = FilletData.Instance?.Effective.Find(value.Prefab);
				val.m_description = value.DescriptionToken;
				val.m_weight = filletEntry?.WeightPerUnit ?? value.Weight;
			}
		}

		internal static void TrackDrop(ItemDrop drop)
		{
			if (drop != null && drop.m_itemData?.m_shared?.m_name != null && details.ContainsKey(drop.m_itemData.m_shared.m_name))
			{
				drops.Add(drop);
				RefreshData(drop.m_itemData);
			}
		}

		internal static void ForgetDrop(ItemDrop drop)
		{
			if (drops.Count > 0)
			{
				drops.Remove(drop);
			}
		}

		internal static void RefreshData()
		{
			if (details.Count == 0)
			{
				return;
			}
			foreach (GameObject value in items.Values)
			{
				if ((Object)(object)value != (Object)null)
				{
					RefreshData(value.GetComponent<ItemDrop>().m_itemData);
				}
			}
			foreach (ItemDrop drop in drops)
			{
				if ((Object)(object)drop != (Object)null)
				{
					RefreshData(drop.m_itemData);
				}
			}
			LocalizeCurrentNames();
			Player localPlayer = Player.m_localPlayer;
			Inventory val = ((localPlayer != null) ? ((Humanoid)localPlayer).GetInventory() : null);
			if (val != null)
			{
				totalWeight?.Invoke(val, null);
			}
		}

		internal static void ResetRegistrationCache()
		{
			lastDatabase = null;
			lastItems = null;
			lastItemHashes = null;
			lastItemData = null;
			lastScene = null;
			lastPrefabs = null;
			lastSceneHashes = null;
		}

		internal static void Dispose()
		{
			items.Clear();
			details.Clear();
			drops.Clear();
			warnings.Clear();
			root = null;
			localizationPending = false;
			nextLocalizationRetry = 0f;
			ResetRegistrationCache();
		}
	}
	internal static class UniqueFilletRecipes
	{
		private static readonly Dictionary<string, Recipe> recipes = new Dictionary<string, Recipe>(StringComparer.Ordinal);

		private static readonly HashSet<Recipe> owned = new HashSet<Recipe>();

		internal static bool IsRecipe(Recipe recipe)
		{
			if ((Object)(object)recipe != (Object)null)
			{
				return owned.Contains(recipe);
			}
			return false;
		}

		internal static void Add(Recipe source, List<Recipe> output)
		{
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Invalid comparison between Unknown and I4
			ObjectDB instance = ObjectDB.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)source == (Object)null || !source.m_enabled || !source.m_requireOnlyOneIngredient)
			{
				return;
			}
			if ((Object)(object)ZNetScene.instance != (Object)null)
			{
				UniqueFilletItems.RegisterScene(ZNetScene.instance, beforeAwake: false);
			}
			else
			{
				UniqueFilletItems.Ensure(instance);
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			Requirement[] resources = source.m_resources;
			foreach (Requirement val in resources)
			{
				ItemDrop val2 = val?.m_resItem;
				if ((Object)(object)val2 == (Object)null)
				{
					continue;
				}
				ItemData itemData = val2.m_itemData;
				if (itemData == null || (int)(itemData.m_shared?.m_itemType).GetValueOrDefault() != 21)
				{
					continue;
				}
				string name = ((Object)((Component)val2).gameObject).name;
				if (!hashSet.Add(name))
				{
					continue;
				}
				ItemDrop val3 = UniqueFilletItems.Find(instance, name);
				if (!((Object)(object)val3 == (Object)null))
				{
					if (!recipes.TryGetValue(name, out var value))
					{
						value = Object.Instantiate<Recipe>(source);
						((Object)value).name = "FieldFilleting_HandFilleting_" + name;
						((Object)value).hideFlags = (HideFlags)61;
						value.m_craftingStation = (value.m_repairStation = null);
						value.m_minStationLevel = 1;
						recipes.Add(name, value);
						owned.Add(value);
					}
					value.m_item = val3;
					if (value.m_resources == null || value.m_resources.Length != 1 || value.m_resources[0] != val)
					{
						value.m_resources = (Requirement[])(object)new Requirement[1] { val };
					}
					value.m_requireOnlyOneIngredient = true;
					value.m_amount = source.m_amount;
					value.m_enabled = source.m_enabled;
					value.m_qualityResultAmountMultiplier = source.m_qualityResultAmountMultiplier;
					value.m_listSortWeight = source.m_listSortWeight;
					output.Add(value);
				}
			}
		}

		internal static void Reset()
		{
			foreach (Recipe item in owned)
			{
				if ((Object)(object)item != (Object)null)
				{
					Object.Destroy((Object)(object)item);
				}
			}
			recipes.Clear();
			owned.Clear();
		}
	}
	[HarmonyPatch(typeof(ObjectDB), "Awake")]
	internal static class FilletDatabasePatch
	{
		[HarmonyPriority(800)]
		private static void Postfix(ObjectDB __instance)
		{
			UniqueFilletItems.Ensure(__instance, force: true);
		}
	}
	[HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")]
	internal static class FilletDatabaseCopyPatch
	{
		[HarmonyPriority(800)]
		private static void Postfix(ObjectDB __instance)
		{
			UniqueFilletItems.Ensure(__instance, force: true);
		}
	}
	[HarmonyPatch(typeof(ObjectDB), "UpdateRegisters")]
	internal static class FilletDatabaseRegistersPatch
	{
		[HarmonyPriority(800)]
		private static void Postfix(ObjectDB __instance)
		{
			UniqueFilletItems.Ensure(__instance, force: true);
		}
	}
	[HarmonyPatch(typeof(ZNetScene), "Awake")]
	internal static class FilletScenePatch
	{
		[HarmonyPriority(800)]
		private static void Prefix(ZNetScene __instance)
		{
			UniqueFilletItems.RegisterScene(__instance, beforeAwake: true);
		}

		[HarmonyPriority(800)]
		private static void Postfix(ZNetScene __instance)
		{
			UniqueFilletItems.RegisterScene(__instance, beforeAwake: false, force: true);
		}
	}
	[HarmonyPatch(typeof(Localization), "SetupLanguage")]
	internal static class FilletLocalizationPatch
	{
		private static void Postfix(Localization __instance)
		{
			UniqueFilletItems.LocalizeNames(__instance);
		}
	}
	[HarmonyPatch(typeof(ItemData), "GetWeight", new Type[] { typeof(int) })]
	internal static class FilletItemWeightPatch
	{
		internal static void Prefix(ItemData __instance)
		{
			UniqueFilletItems.RefreshData(__instance);
		}
	}
	[HarmonyPatch(typeof(ItemDrop), "Awake")]
	internal static class FilletDropDataPatch
	{
		private static void Postfix(ItemDrop __instance)
		{
			UniqueFilletItems.TrackDrop(__instance);
		}
	}
	[HarmonyPatch(typeof(ItemDrop), "OnDestroy")]
	internal static class FilletDropCleanupPatch
	{
		private static void Prefix(ItemDrop __instance)
		{
			UniqueFilletItems.ForgetDrop(__instance);
		}
	}
	internal sealed class FilletEntry
	{
		internal string Prefabname = "";

		internal string Title = "";

		internal string Description = "";

		internal float? WeightPerUnit;
	}
	internal sealed class FilletCatalog
	{
		internal static readonly FilletCatalog Empty = new FilletCatalog(new List<FilletEntry>());

		internal readonly List<FilletEntry> Entries;

		private readonly Dictionary<string, FilletEntry> byName = new Dictionary<string, FilletEntry>(StringComparer.Ordinal);

		private readonly string signature;

		internal FilletCatalog(List<FilletEntry> entries)
		{
			if (entries.Count > 256)
			{
				throw new FormatException("At most 256 fillet entries are supported.");
			}
			entries.Sort((FilletEntry a, FilletEntry b) => string.Compare(a.Prefabname, b.Prefabname, StringComparison.Ordinal));
			StringBuilder stringBuilder = new StringBuilder();
			int num = 0;
			foreach (FilletEntry entry in entries)
			{
				if (entry.Prefabname == null || entry.Title == null || entry.Description == null || !entry.Prefabname.StartsWith("FGAGI_RawFish_", StringComparison.Ordinal) || entry.Prefabname.Length <= "FGAGI_RawFish_".Length || entry.Prefabname.Length > 256 || entry.Title.Length > 512 || entry.Description.Length > 8192 || (entry.WeightPerUnit.HasValue && (float.IsNaN(entry.WeightPerUnit.Value) || float.IsInfinity(entry.WeightPerUnit.Value) || entry.WeightPerUnit.Value < 0f)))
				{
					throw new FormatException("Invalid fillet entry or weight: " + entry.Prefabname);
				}
				if (byName.ContainsKey(entry.Prefabname))
				{
					throw new FormatException("Duplicate prefab: " + entry.Prefabname);
				}
				byName.Add(entry.Prefabname, entry);
				string[] array = new string[4]
				{
					entry.Prefabname,
					entry.Title,
					entry.Description,
					entry.WeightPerUnit?.ToString("R", CultureInfo.InvariantCulture) ?? ""
				};
				foreach (string text in array)
				{
					num += Encoding.UTF8.GetByteCount(text) + 8;
					stringBuilder.Append(text.Length).Append(':').Append(text);
				}
			}
			if (num > 32768)
			{
				throw new FormatException("Fillet item data exceeds the 32 KiB synchronization limit.");
			}
			Entries = entries;
			signature = stringBuilder.ToString();
		}

		internal FilletEntry Find(string prefab)
		{
			if (!byName.TryGetValue(prefab, out var value))
			{
				return null;
			}
			return value;
		}

		internal bool Same(FilletCatalog other)
		{
			if (other != null)
			{
				return signature == other.signature;
			}
			return false;
		}

		internal void Write(ZPackage data)
		{
			data.Write(Entries.Count);
			foreach (FilletEntry entry in Entries)
			{
				data.Write(entry.Prefabname);
				data.Write(entry.Title);
				data.Write(entry.Description);
				data.Write(entry.WeightPerUnit.HasValue);
				if (entry.WeightPerUnit.HasValue)
				{
					data.Write(entry.WeightPerUnit.Value);
				}
			}
		}

		internal static FilletCatalog Read(ZPackage data)
		{
			int num = data.ReadInt();
			if (num < 0 || num > 256)
			{
				throw new FormatException("Invalid fillet entry count.");
			}
			List<FilletEntry> list = new List<FilletEntry>();
			for (int i = 0; i < num; i++)
			{
				list.Add(new FilletEntry
				{
					Prefabname = data.ReadString(),
					Title = data.ReadString(),
					Description = data.ReadString(),
					WeightPerUnit = (data.ReadBool() ? new float?(data.ReadSingle()) : ((float?)null))
				});
			}
			return new FilletCatalog(list);
		}
	}
	internal static class FilletYaml
	{
		private static string Key(string value)
		{
			switch (Scalar(value).Replace(" ", "").Replace("_", "").Replace("-", "")
				.ToLowerInvariant())
			{
			case "prefab":
			case "prefabname":
				return "prefab";
			case "title":
			case "commonname":
				return "title";
			case "description":
			case "desc":
				return "description";
			case "weight":
			case "weightperunit":
				return "weight";
			default:
				throw new FormatException("Unknown field: " + value);
			}
		}

		private static string Uncomment(string line)
		{
			if (line.TrimStart().StartsWith("#", StringComparison.Ordinal))
			{
				return "";
			}
			int i;
			for (i = line.IndexOf(':') + 1; i < line.Length && char.IsWhiteSpace(line[i]); i++)
			{
			}
			char c = ((i < line.Length && (line[i] == '\'' || line[i] == '"')) ? line[i] : '\0');
			bool flag = c == '\0';
			for (int j = ((!flag) ? (i + 1) : 0); j < line.Length; j++)
			{
				char c2 = line[j];
				if (!flag)
				{
					if (c == '"' && c2 == '\\')
					{
						j++;
					}
					else if (c == '\'' && c2 == '\'' && j + 1 < line.Length && line[j + 1] == '\'')
					{
						j++;
					}
					else if (c2 == c)
					{
						flag = true;
					}
				}
				else if (c2 == '#' && (j == 0 || char.IsWhiteSpace(line[j - 1])))
				{
					return line.Substring(0, j);
				}
			}
			if (!flag)
			{
				throw new FormatException("Unclosed quote.");
			}
			return line;
		}

		private static string Scalar(string value)
		{
			value = value.Trim();
			if (value.Length == 0 || value == "~" || value.Equals("null", StringComparison.OrdinalIgnoreCase))
			{
				return "";
			}
			char c = value[0];
			if (c != '\'' && c != '"')
			{
				if ("[{&*!".IndexOf(c) >= 0)
				{
					throw new FormatException("Use a scalar value, not a YAML collection, anchor or tag.");
				}
				return value;
			}
			if (value.Length < 2 || value[value.Length - 1] != c)
			{
				throw new FormatException("Invalid quoted value.");
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 1; i < value.Length - 1; i++)
			{
				char c2 = value[i];
				if (c2 == c)
				{
					if (c == '\'' && i + 1 < value.Length - 1 && value[i + 1] == '\'')
					{
						stringBuilder.Append('\'');
						i++;
						continue;
					}
					throw new FormatException("Escape a quote inside a quoted value.");
				}
				if (c == '"' && c2 == '\\')
				{
					if (++i >= value.Length - 1)
					{
						throw new FormatException("Incomplete escape.");
					}
					c2 = value[i];
					int num;
					int num2;
					int result;
					switch (c2)
					{
					case 'n':
						c2 = '\n';
						break;
					case 'r':
						c2 = '\r';
						break;
					case 't':
						c2 = '\t';
						break;
					case 'b':
						c2 = '\b';
						break;
					case 'f':
						c2 = '\f';
						break;
					case '0':
						c2 = '\0';
						break;
					case 'x':
						num = 2;
						goto IL_01a0;
					case 'u':
						num = 4;
						goto IL_01a0;
					default:
						throw new FormatException("Unsupported escape. Use quoted \\n or a Description: | block for line breaks.");
					case '"':
					case '/':
					case '\\':
						break;
						IL_01a0:
						num2 = num;
						if (i + num2 >= value.Length - 1 || !int.TryParse(value.Substring(i + 1, num2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result))
						{
							throw new FormatException("Invalid Unicode escape.");
						}
						stringBuilder.Append((char)result);
						i += num2;
						continue;
					}
				}
				stringBuilder.Append(c2);
			}
			return stringBuilder.ToString();
		}

		private static int Indent(string line)
		{
			int i;
			for (i = 0; i < line.Length && line[i] == ' '; i++)
			{
			}
			if (i < line.Length && line[i] == '\t')
			{
				throw new FormatException("Use spaces for indentation, not tabs.");
			}
			return i;
		}

		private static string Block(string[] lines, ref int index, int fieldIndent, string style)
		{
			List<string> list = new List<string>();
			int num = -1;
			while (index + 1 < lines.Length)
			{
				string text = lines[index + 1];
				int num2 = Indent(text);
				if (text.Trim().Length != 0 && num2 <= fieldIndent)
				{
					break;
				}
				if (text.Trim().Length != 0)
				{
					if (num < 0)
					{
						num = num2;
					}
					if (num2 < num)
					{
						throw new FormatException("Inconsistent block-scalar indentation.");
					}
				}
				list.Add((text.Trim().Length == 0) ? "" : text.Substring(num));
				index++;
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < list.Count; i++)
			{
				if (i > 0)
				{
					if (style[0] == '|')
					{
						stringBuilder.Append('\n');
					}
					else if (list[i].Length == 0)
					{
						stringBuilder.Append('\n');
					}
					else if (list[i - 1].Length != 0)
					{
						stringBuilder.Append((list[i].StartsWith(" ") || list[i - 1].StartsWith(" ")) ? '\n' : ' ');
					}
				}
				stringBuilder.Append(list[i]);
			}
			string text2 = stringBuilder.ToString();
			if (style.EndsWith("-"))
			{
				return text2.TrimEnd('\n');
			}
			if (style.EndsWith("+"))
			{
				return text2 + ((list.Count > 0) ? "\n" : "");
			}
			return text2.TrimEnd('\n') + ((text2.Length > 0) ? "\n" : "");
		}

		internal static FilletCatalog Parse(string yaml)
		{
			if (yaml == null || Encoding.UTF8.GetByteCount(yaml) > 262144)
			{
				throw new FormatException("YAML file is missing or exceeds 256 KiB.");
			}
			string[] array = yaml.TrimStart('\ufeff').Replace("\r\n", "\n").Replace('\r', '\n')
				.Split('\n');
			if (array.Length != 0 && array[^1].Length == 0)
			{
				Array.Resize(ref array, array.Length - 1);
			}
			List<FilletEntry> list = new List<FilletEntry>();
			FilletEntry filletEntry = null;
			HashSet<string> hashSet = new HashSet<string>();
			for (int i = 0; i < array.Length; i++)
			{
				try
				{
					int num = Indent(array[i]);
					string text = Uncomment(array[i]).Trim();
					if (text.Length == 0)
					{
						continue;
					}
					switch (text)
					{
					case "...":
						goto end_IL_0081;
					case "[]":
						if (filletEntry != null || list.Count != 0)
						{
							break;
						}
						goto end_IL_0081;
					}
					if (num == 0 && (text == "-" || text.StartsWith("- ", StringComparison.Ordinal)))
					{
						if (filletEntry != null)
						{
							if (filletEntry.Prefabname.Length == 0)
							{
								throw new FormatException("Every entry needs a Prefabname.");
							}
							list.Add(filletEntry);
						}
						filletEntry = new FilletEntry();
						hashSet.Clear();
						text = text.Substring(1).Trim();
						num = 2;
						if (text.Length != 0)
						{
							goto IL_0166;
						}
						continue;
					}
					if (filletEntry == null || num != 2)
					{
						throw new FormatException("Use a list with '- Prefabname:' and fields indented by two spaces.");
					}
					goto IL_0166;
					IL_0166:
					int num2 = text.IndexOf(':');
					if (num2 <= 0)
					{
						throw new FormatException("Expected a field followed by ':'.");
					}
					string text2 = Key(text.Substring(0, num2));
					if (!hashSet.Add(text2))
					{
						throw new FormatException("Duplicate field: " + text2);
					}
					string text3 = text.Substring(num2 + 1).Trim();
					string text4;
					switch (text3)
					{
					case "|":
					case "|-":
					case "|+":
					case ">":
					case ">-":
					case ">+":
						text4 = Block(array, ref i, num, text3);
						break;
					default:
						text4 = Scalar(text3);
						break;
					}
					switch (text2)
					{
					case "prefab":
						filletEntry.Prefabname = text4;
						continue;
					case "title":
						filletEntry.Title = text4;
						continue;
					case "description":
						filletEntry.Description = text4;
						continue;
					}
					if (text4.Length > 0)
					{
						if (!float.TryParse(text4, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || float.IsNaN(result) || float.IsInfinity(result) || result < 0f)
						{
							throw new FormatException("WeightPerUnit must be a finite non-negative number; use a decimal point.");
						}
						filletEntry.WeightPerUnit = result;
					}
					end_IL_0081:;
				}
				catch (FormatException ex)
				{
					throw new FormatException("YAML line " + (i + 1) + ": " + ex.Message, ex);
				}
			}
			if (filletEntry != null)
			{
				if (filletEntry.Prefabname.Length == 0)
				{
					throw new FormatException("Every entry needs a Prefabname.");
				}
				list.Add(filletEntry);
			}
			return new FilletCatalog(list);
		}
	}
	internal sealed class FilletData : IDisposable
	{
		internal static FilletData Instance;

		internal const string FileName = "FieldFilleting.Fillets.yaml";

		internal readonly string FilePath;

		internal FilletCatalog Local = FilletCatalog.Empty;

		internal FilletCatalog Remote;

		private FileSystemWatcher watcher;

		private long reloadAt;

		private bool disposed;

		internal FilletCatalog Effective
		{
			get
			{
				if (!Locked)
				{
					return Local;
				}
				return Remote ?? FilletCatalog.Empty;
			}
		}

		internal bool Locked { get; private set; }

		internal event Action AuthorityChanged;

		internal FilletData(string path, bool watch = true)
		{
			FilePath = path;
			Instance = this;
			try
			{
				Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
				if (!File.Exists(path) && !LegacyFiles.CopyIfMissing(Path.Combine(Path.GetDirectoryName(Path.GetFullPath(path)), "FishGrazingAsImGoing.Fillets.yaml"), path))
				{
					using Stream stream = typeof(Plugin).Assembly.GetManifestResourceStream("FieldFilleting.FilletsDefaults");
					using StreamReader streamReader = new StreamReader(stream ?? throw new FileNotFoundException("Bundled fillet YAML is unavailable."));
					File.WriteAllText(path, streamReader.ReadToEnd(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
				}
				Reload();
				if (watch)
				{
					watcher = new FileSystemWatcher(Path.GetDirectoryName(Path.GetFullPath(path)), Path.GetFileName(path));
					watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
					watcher.Changed += FileChanged;
					watcher.Created += FileChanged;
					watcher.Deleted += FileChanged;
					watcher.Renamed += FileRenamed;
					watcher.Error += WatchError;
					watcher.EnableRaisingEvents = true;
				}
			}
			catch (Exception ex)
			{
				Plugin.Warn("Fillet YAML setup failed: " + ex.Message);
			}
		}

		private void FileChanged(object sender, FileSystemEventArgs args)
		{
			QueueReload();
		}

		private void FileRenamed(object sender, RenamedEventArgs args)
		{
			QueueReload();
		}

		private void WatchError(object sender, ErrorEventArgs args)
		{
			QueueReload();
		}

		internal void QueueReload()
		{
			Interlocked.Exchange(ref reloadAt, DateTime.UtcNow.AddMilliseconds(750.0).Ticks);
		}

		internal void Tick()
		{
			long num = Interlocked.Read(in reloadAt);
			if (num != 0L && DateTime.UtcNow.Ticks >= num && Interlocked.CompareExchange(ref reloadAt, 0L, num) == num)
			{
				Reload();
			}
		}

		internal void Reload()
		{
			if (disposed)
			{
				return;
			}
			try
			{
				if (new FileInfo(FilePath).Length > 262144)
				{
					throw new FormatException("YAML file exceeds 256 KiB.");
				}
				FilletCatalog filletCatalog = FilletYaml.Parse(File.ReadAllText(FilePath));
				if (!filletCatalog.Same(Local))
				{
					Local = filletCatalog;
					if (!Locked)
					{
						Apply();
						this.AuthorityChanged?.Invoke();
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Warn("Fillet YAML reload failed; keeping the last item values. " + ex.Message);
				if (ex is IOException && !(ex is FileNotFoundException))
				{
					QueueReload();
				}
			}
		}

		private static void Apply()
		{
			UniqueFilletItems.RefreshData();
			Plugin.RefreshPending = (Plugin.ForceRefresh = true);
		}

		internal void SetClient(bool client)
		{
			if (Locked != client)
			{
				Locked = client;
				Remote = null;
				Apply();
			}
		}

		internal void Receive(FilletCatalog data)
		{
			if (Locked && data != null)
			{
				Remote = data;
				Apply();
			}
		}

		internal void WaitForServer()
		{
			if (Locked && Remote != null)
			{
				Remote = null;
				Apply();
			}
		}

		public void Dispose()
		{
			if (!disposed)
			{
				watcher?.Dispose();
				watcher = null;
				SetClient(client: false);
				disposed = true;
				if (Instance == this)
				{
					Instance = null;
				}
			}
		}
	}
}