Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of FieldFilleting v1.0.1
BepInEx/plugins/dannanboi_FieldFilleting.dll
Decompiled 8 hours agousing 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; } } } } }