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 FineDining v1.0.0
FineDining.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Timers; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Jotunn.Configs; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; using UnityEngine.UI; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.ObjectPool; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("FineDining")] [assembly: AssemblyDescription("Integrated food spoilage, diet, cooking, and station guidance for Valheim.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("FineDining")] [assembly: AssemblyCopyright("Copyright © sighsorry 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("FDE6E067-900F-4380-9530-A1EF8D16E7B8")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 FineDining { internal static class AzuExtendedPlayerInventoryCompatibility { private readonly struct DirectMergeSite { internal int TargetLoadIndex { get; } internal int AmountLoadIndex { get; } internal int SourceLoadIndex { get; } internal int SourceLoadCount { get; } internal int InsertIndex { get; } internal DirectMergeSite(int targetLoadIndex, int amountLoadIndex, int sourceLoadIndex, int sourceLoadCount, int insertIndex) { TargetLoadIndex = targetLoadIndex; AmountLoadIndex = amountLoadIndex; SourceLoadIndex = sourceLoadIndex; SourceLoadCount = sourceLoadCount; InsertIndex = insertIndex; } } internal const string PluginGuid = "Azumatt.AzuExtendedPlayerInventory"; private const string RecoveryPatchTypeName = "AzuEPI.Game.Patches.InventoryPatches+Load_TrackAndFixHiddenItems_Patch"; private static readonly FieldInfo StackField = AccessTools.Field(typeof(ItemData), "m_stack"); private static readonly MethodInfo ComposeMethod = AccessTools.Method(typeof(AzuExtendedPlayerInventoryCompatibility), "ComposeDirectRecoveryMergeExpirySafe", (Type[])null, (Type[])null); private static int _matchedMergeSites; private static bool _runtimeFailureLogged; internal static void TryInstall(Harmony harmony) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Expected O, but got Unknown if (!Chainloader.PluginInfos.TryGetValue("Azumatt.AzuExtendedPlayerInventory", out var value)) { return; } MethodInfo methodInfo = null; bool flag = false; try { methodInfo = ResolveTargetMethod(value); if (methodInfo == null) { FineDiningPlugin.Log.LogWarning((object)"AzuExtendedPlayerInventory is installed, but its hidden-slot recovery method no longer has the expected signature. Direct recovery merges will not compose spoilage timers."); return; } Patches patchInfo = Harmony.GetPatchInfo((MethodBase)methodInfo); if (patchInfo == null || !patchInfo.Transpilers.Any((Patch patch) => string.Equals(patch.owner, harmony.Id, StringComparison.Ordinal) && object.Equals(patch.PatchMethod, AccessTools.Method(typeof(AzuExtendedPlayerInventoryCompatibility), "Transpiler", (Type[])null, (Type[])null)))) { _matchedMergeSites = 0; HarmonyMethod val = new HarmonyMethod(AccessTools.Method(typeof(AzuExtendedPlayerInventoryCompatibility), "Transpiler", (Type[])null, (Type[])null)); flag = true; harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null); if (_matchedMergeSites != 1) { harmony.Unpatch((MethodBase)methodInfo, (HarmonyPatchType)3, harmony.Id); FineDiningPlugin.Log.LogWarning((object)"AzuExtendedPlayerInventory is installed, but its direct recovery merge no longer matches the verified pattern. The compatibility patch was left disabled so inventory loading remains unchanged."); } else { FineDiningPlugin.Log.LogInfo((object)("Enabled AzuExtendedPlayerInventory " + value.Metadata.Version?.ToString() + " hidden-slot merge timer compatibility.")); } } } catch (Exception ex) { if (flag && methodInfo != null) { try { harmony.Unpatch((MethodBase)methodInfo, (HarmonyPatchType)3, harmony.Id); } catch { } } FineDiningPlugin.Log.LogWarning((object)("Could not enable AzuExtendedPlayerInventory merge timer compatibility. " + ex)); } } internal static void Shutdown() { _matchedMergeSites = 0; _runtimeFailureLogged = false; } private static MethodInfo? ResolveTargetMethod(PluginInfo pluginInfo) { MethodInfo methodInfo = ((((object)pluginInfo.Instance)?.GetType().Assembly)?.GetType("AzuEPI.Game.Patches.InventoryPatches+Load_TrackAndFixHiddenItems_Patch", throwOnError: false))?.GetMethod("Postfix", BindingFlags.Static | BindingFlags.NonPublic); if (methodInfo == null || methodInfo.ReturnType != typeof(void)) { return null; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length != 1 || !(parameters[0].ParameterType == typeof(Inventory))) { return null; } return methodInfo; } private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown List<CodeInstruction> list = instructions.ToList(); List<DirectMergeSite> list2 = new List<DirectMergeSite>(); for (int i = 5; i < list.Count; i++) { if (TryMatchDirectMerge(list, i, out var site)) { list2.Add(site); } } _matchedMergeSites = list2.Count; if (list2.Count != 1 || ComposeMethod == null) { if (ComposeMethod == null) { _matchedMergeSites = 0; } return list; } DirectMergeSite directMergeSite = list2[0]; List<CodeInstruction> list3 = new List<CodeInstruction> { CloneWithoutFlowMetadata(list[directMergeSite.TargetLoadIndex]) }; for (int j = 0; j < directMergeSite.SourceLoadCount; j++) { list3.Add(CloneWithoutFlowMetadata(list[directMergeSite.SourceLoadIndex + j])); } list3.Add(CloneWithoutFlowMetadata(list[directMergeSite.AmountLoadIndex])); list3.Add(new CodeInstruction(OpCodes.Call, (object)ComposeMethod)); list.InsertRange(directMergeSite.InsertIndex, list3); return list; } private static bool TryMatchDirectMerge(IReadOnlyList<CodeInstruction> code, int targetStoreIndex, out DirectMergeSite site) { site = default(DirectMergeSite); int num = targetStoreIndex - 5; if (!IsLocalLoad(code[num]) || code[num + 1].opcode != OpCodes.Dup || !IsFieldLoad(code[num + 2], StackField) || !IsLocalLoad(code[num + 3]) || code[num + 4].opcode != OpCodes.Add || !IsFieldStore(code[targetStoreIndex], StackField)) { return false; } int num2 = targetStoreIndex + 1; int sourceLoadCount; int num3; if (num2 + 5 < code.Count && IsLocalLoad(code[num2]) && code[num2 + 1].opcode == OpCodes.Dup) { sourceLoadCount = 1; num3 = num2 + 1; } else { if (num2 + 6 >= code.Count || !IsLocalLoad(code[num2]) || !IsItemDataFieldLoad(code[num2 + 1]) || !(code[num2 + 2].opcode == OpCodes.Dup)) { return false; } sourceLoadCount = 2; num3 = num2 + 2; } int index = num3 + 1; int index2 = num3 + 2; int index3 = num3 + 3; int num4 = num3 + 4; if (!IsFieldLoad(code[index], StackField) || !IsLocalLoad(code[index2]) || !AreSameLocalLoads(code[num + 3], code[index2]) || code[index3].opcode != OpCodes.Sub || !IsFieldStore(code[num4], StackField)) { return false; } site = new DirectMergeSite(num, num + 3, num2, sourceLoadCount, num4 + 1); return true; } private static void ComposeDirectRecoveryMergeExpirySafe(ItemData? target, ItemData? source, int movedAmount) { try { DecayRuntime.ComposeDirectRecoveryMergeExpiry(target, source, movedAmount); } catch (Exception ex) { if (!_runtimeFailureLogged) { _runtimeFailureLogged = true; FineDiningPlugin.Log.LogWarning((object)("AzuExtendedPlayerInventory merged a recovered stack, but FineDining could not compose its expiry timer. The original inventory merge will continue unchanged. " + ex)); } } } private static CodeInstruction CloneWithoutFlowMetadata(CodeInstruction instruction) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown CodeInstruction val = new CodeInstruction(instruction); val.labels.Clear(); val.blocks.Clear(); return val; } private static bool IsItemDataFieldLoad(CodeInstruction instruction) { if (instruction.opcode == OpCodes.Ldfld && instruction.operand is FieldInfo fieldInfo) { return fieldInfo.FieldType == typeof(ItemData); } return false; } private static bool IsFieldLoad(CodeInstruction instruction, FieldInfo field) { if (instruction.opcode == OpCodes.Ldfld) { return object.Equals(instruction.operand, field); } return false; } private static bool IsFieldStore(CodeInstruction instruction, FieldInfo field) { if (instruction.opcode == OpCodes.Stfld) { return object.Equals(instruction.operand, field); } return false; } private static bool IsLocalLoad(CodeInstruction instruction) { if (!(instruction.opcode == OpCodes.Ldloc) && !(instruction.opcode == OpCodes.Ldloc_S) && !(instruction.opcode == OpCodes.Ldloc_0) && !(instruction.opcode == OpCodes.Ldloc_1) && !(instruction.opcode == OpCodes.Ldloc_2)) { return instruction.opcode == OpCodes.Ldloc_3; } return true; } private static bool AreSameLocalLoads(CodeInstruction first, CodeInstruction second) { if (first.opcode != second.opcode) { return false; } if (!(first.opcode == OpCodes.Ldloc_0) && !(first.opcode == OpCodes.Ldloc_1) && !(first.opcode == OpCodes.Ldloc_2) && !(first.opcode == OpCodes.Ldloc_3)) { return object.Equals(first.operand, second.operand); } return true; } } internal static class ConfigPresentation { internal readonly struct SectionDefinition { internal string Name { get; } internal int CategoryOrder { get; } internal SectionDefinition(string name, int categoryOrder) { Name = name; CategoryOrder = categoryOrder; } } internal static readonly SectionDefinition General = new SectionDefinition("1 - General", 500); internal static readonly SectionDefinition ClientSection = new SectionDefinition("2 - Client", 400); internal static readonly SectionDefinition Diet = new SectionDefinition("3 - Diet", 300); internal static readonly SectionDefinition ChefChoice = new SectionDefinition("4 - Chef Choice", 200); internal static readonly SectionDefinition Spoilage = new SectionDefinition("5 - Spoilage", 100); internal static ConfigDescription Synced(string description, SectionDefinition section, int order, AcceptableValueBase? acceptableValues = null) { return Create(description, "[Synced with Server]", section, order, acceptableValues); } internal static ConfigDescription Client(string description, SectionDefinition section, int order, AcceptableValueBase? acceptableValues = null) { return Create(description, "[Client Only]", section, order, acceptableValues); } private static ConfigDescription Create(string description, string scope, SectionDefinition section, int order, AcceptableValueBase? acceptableValues) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown return new ConfigDescription(description + " " + scope, acceptableValues, new object[1] { new ConfigurationManagerAttributes { CategoryOrder = section.CategoryOrder, Order = order } }); } } internal sealed class ConfigurationManagerAttributes { public int? CategoryOrder { get; set; } public int? Order { get; set; } } internal enum FoodStatAxis { None, Health, Stamina, Eitr } internal static class FoodIdentity { internal static string GetCanonicalPrefabName(ItemData? item) { if (item == null) { return string.Empty; } if ((Object)(object)item.m_dropPrefab != (Object)null) { return NormalizePrefabName(((Object)item.m_dropPrefab).name); } ObjectDB instance = ObjectDB.instance; GameObject val = default(GameObject); if ((Object)(object)instance != (Object)null && item.m_shared != null && instance.TryGetItemPrefab(item.m_shared, ref val) && (Object)(object)val != (Object)null) { return NormalizePrefabName(((Object)val).name); } return string.Empty; } internal static string GetCanonicalPrefabName(Food? food) { if (food == null) { return string.Empty; } string text = NormalizePrefabName(food.m_name); if (text.Length <= 0) { return GetCanonicalPrefabName(food.m_item); } return text; } internal static bool IsDirectlyEdible(ItemData? item) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 SharedData val = item?.m_shared; if (val != null && (int)val.m_itemType == 2) { return HasChefFoodStats(val.m_food, val.m_foodStamina, val.m_foodEitr); } return false; } internal static bool IsDirectlyEdible(Food? food) { return IsDirectlyEdible(food?.m_item); } internal static bool TryGetFoodStatAxis(float health, float stamina, float eitr, out FoodStatAxis axis) { axis = FoodStatAxis.None; if (!HasChefFoodStats(health, stamina, eitr)) { return false; } if (eitr > 0f) { axis = FoodStatAxis.Eitr; return true; } axis = ((!(health <= stamina)) ? FoodStatAxis.Health : FoodStatAxis.Stamina); return true; } internal static bool TryGetFoodStatAxis(ItemData? item, out FoodStatAxis axis) { SharedData val = item?.m_shared; if (val == null || !IsDirectlyEdible(item)) { axis = FoodStatAxis.None; return false; } return TryGetFoodStatAxis(val.m_food, val.m_foodStamina, val.m_foodEitr, out axis); } internal static bool HasChefFoodStats(float health, float stamina, float eitr) { if (!(health > 0f) && !(stamina > 0f)) { return eitr > 0f; } return true; } internal static string NormalizePrefabName(string? name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } return Utils.GetPrefabName(name.Trim()).Trim(); } } [HarmonyPatch(typeof(ItemDrop), "SlowUpdate")] internal static class ItemDropSlowUpdateSpoilageSafetyPatch { private static void Postfix(ItemDrop __instance) { DecayRuntime.RefreshOwnedPlacedDrop(__instance); } } [HarmonyPatch(typeof(ItemDrop), "Load")] internal static class ItemDropLoadSpoilagePatch { private static void Postfix(ItemDrop __instance) { DecayRuntime.RegisterGroundDrop(__instance); } } [HarmonyPatch(typeof(ItemDrop), "LoadFromExternalZDO")] internal static class ItemDropExternalLoadSpoilagePatch { private static void Postfix(ItemDrop __instance) { DecayRuntime.RegisterGroundDrop(__instance); } } [HarmonyPatch(typeof(ItemDrop), "Save")] internal static class ItemDropSavedSpoilagePatch { private static void Postfix(ItemDrop __instance) { DecayRuntime.RegisterGroundDrop(__instance); } } [HarmonyPatch(typeof(ItemDrop), "OnDestroy")] internal static class ItemDropDestroyedSpoilagePatch { private static void Prefix(ItemDrop __instance) { DecayRuntime.UnregisterGroundDrop(__instance); } } [HarmonyPatch(typeof(Container), "Awake")] internal static class ContainerAwakeSpoilagePatch { private static void Prefix(Container __instance) { IceboxSubsystem.ApplyConfiguredStorageSize(__instance); } private static void Postfix(Container __instance) { IceboxSubsystem.ApplyConfiguredStorageSize(__instance); IceboxSubsystem.ApplyStoredRecipe(__instance); DecayRuntime.ContainerLoaded(__instance); } } [HarmonyPatch(typeof(Container), "Load")] internal static class ContainerLoadSpoilagePatch { private static void Prefix(Container __instance, out bool __state) { __state = IceboxSubsystem.PrepareStorageLoad(__instance); DecayRuntime.RegisterContainer(__instance); } private static void Postfix(Container __instance, bool __result, bool __state) { if (__state) { IceboxSubsystem.ApplyConfiguredStorageSize(__instance); } if (__result) { DecayRuntime.ContainerLoaded(__instance); } } } [HarmonyPatch(typeof(Inventory), "Changed")] internal static class InventoryChangedSpoilagePatch { private static void Postfix(Inventory __instance) { DecayRuntime.MarkDirty(__instance); IceboxSubsystem.HandleInventoryChanged(__instance); } } [HarmonyPatch(typeof(Player), "Save", new Type[] { typeof(ZPackage) })] internal static class PlayerSaveSpoilageClockPatch { private static void Prefix(Player __instance, out List<KeyValuePair<ItemData, string>> __state) { __state = DecayRuntime.BeginPlayerSaveClockSnapshot(__instance); } private static Exception? Finalizer(List<KeyValuePair<ItemData, string>> __state, Exception? __exception) { DecayRuntime.EndPlayerSaveClockSnapshot(__state); return __exception; } } [HarmonyPatch(typeof(ItemDrop), "AutoStackItems")] internal static class ItemDropAutoStackSpoilagePatch { private static readonly FieldInfo ItemDataField = AccessTools.Field(typeof(ItemDrop), "m_itemData"); private static readonly FieldInfo StackField = AccessTools.Field(typeof(ItemData), "m_stack"); private static readonly MethodInfo ComposeMethod = AccessTools.Method(typeof(DecayRuntime), "ComposeGroundStackExpiry", (Type[])null, (Type[])null); private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Expected O, but got Unknown //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Expected O, but got Unknown List<CodeInstruction> list = new List<CodeInstruction>(instructions); List<int> list2 = new List<int>(); for (int i = 8; i < list.Count; i++) { if (IsGroundStackIncrement(list, i)) { list2.Add(i - 8); } } if (list2.Count != 1 || ComposeMethod == null) { FineDiningPlugin.Log.LogWarning((object)("Could not safely patch ItemDrop.AutoStackItems expiry merge; expected one stack increment but found " + list2.Count + ". Ground-stack timers may not compose on this game version.")); return list; } int num = list2[0]; CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null); val.labels.AddRange(list[num].labels); list[num].labels.Clear(); val.blocks.AddRange(list[num].blocks); list[num].blocks.Clear(); CodeInstruction val2 = new CodeInstruction(list[num + 4]); val2.labels.Clear(); val2.blocks.Clear(); list.InsertRange(num, (IEnumerable<CodeInstruction>)(object)new CodeInstruction[3] { val, val2, new CodeInstruction(OpCodes.Call, (object)ComposeMethod) }); return list; } private static bool IsGroundStackIncrement(IReadOnlyList<CodeInstruction> code, int storeIndex) { int num = storeIndex - 8; if (code[storeIndex].opcode == OpCodes.Stfld && object.Equals(code[storeIndex].operand, StackField) && code[num].opcode == OpCodes.Ldarg_0 && IsFieldLoad(code[num + 1], ItemDataField) && code[num + 2].opcode == OpCodes.Dup && IsFieldLoad(code[num + 3], StackField) && IsLocalLoad(code[num + 4]) && IsFieldLoad(code[num + 5], ItemDataField) && IsFieldLoad(code[num + 6], StackField)) { return code[num + 7].opcode == OpCodes.Add; } return false; } private static bool IsFieldLoad(CodeInstruction instruction, FieldInfo field) { if (instruction.opcode == OpCodes.Ldfld) { return object.Equals(instruction.operand, field); } return false; } private static bool IsLocalLoad(CodeInstruction instruction) { if (!(instruction.opcode == OpCodes.Ldloc) && !(instruction.opcode == OpCodes.Ldloc_S) && !(instruction.opcode == OpCodes.Ldloc_0) && !(instruction.opcode == OpCodes.Ldloc_1) && !(instruction.opcode == OpCodes.Ldloc_2)) { return instruction.opcode == OpCodes.Ldloc_3; } return true; } } internal static class SpoilageContentLifecycle { internal static void Refresh() { GeneratedPrefabRegistry.RefreshConfiguredContent(); FoodClassifier.Invalidate(); DietModule.InvalidateChefTierCatalog(); DecayRuntime.InvalidateAll(); } } [HarmonyPatch(typeof(ObjectDB), "UpdateRegisters")] internal static class ObjectDbUpdateRegistersSpoilagePatch { [HarmonyPriority(0)] private static void Postfix(ObjectDB __instance) { SpoilageContentLifecycle.Refresh(); } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class ZNetDestroySpoilagePatch { private static void Postfix() { DecayRuntime.Reset(); FoodClassifier.Invalidate(); } } internal sealed class InventoryAddMergeState { internal readonly Dictionary<ItemData, int> PreviousStacks = new Dictionary<ItemData, int>(); internal readonly Dictionary<ItemData, AssignedLifetimeSnapshot> PreviousLifetimes = new Dictionary<ItemData, AssignedLifetimeSnapshot>(); internal bool SourceHadValidClock; internal bool SourceHadExpiryKey; internal string? OriginalSourceExpiryValue; internal bool SourceHadLifetimeKey; internal string? OriginalSourceLifetimeValue; internal long SourceClockValue; internal AssignedLifetimeSnapshot SourceLifetime; } internal static class InventoryAddMergeTracker { private static bool _loggedTemporarySourceRestoreFailure; internal static InventoryAddMergeState? Prefix(Inventory inventory, ItemData? source) { if (source == null || DecayRuntime.IsContainerLoading(inventory) || !DecayRuntime.IsAuthoritativeInventory(inventory)) { return null; } InventoryAddMergeState inventoryAddMergeState = new InventoryAddMergeState(); CaptureOriginalSourceMetadata(source, inventoryAddMergeState); DecayRuntime.PrepareItemForAdd(inventory, source); PieceRecoverySpoilageTracker.ApplyToInventoryItem(inventory, source); inventoryAddMergeState.SourceLifetime = FreshnessRuntime.CaptureAssignedLifetime(source); if (!DecayRuntime.TryGetExpiryTicks(source, out inventoryAddMergeState.SourceClockValue)) { return null; } foreach (ItemData item in inventory.m_inventory) { if (item != null && CanPotentiallyStack(item, source)) { inventoryAddMergeState.PreviousStacks[item] = item.m_stack; inventoryAddMergeState.PreviousLifetimes[item] = FreshnessRuntime.CaptureAssignedLifetime(item); } } return inventoryAddMergeState; } internal static void Postfix(Inventory inventory, ItemData? source, InventoryAddMergeState? state) { if (state == null || source == null) { return; } bool flag = false; try { if (state.SourceClockValue != 0L) { foreach (KeyValuePair<ItemData, int> previousStack in state.PreviousStacks) { ItemData key = previousStack.Key; if (key != source && key.m_stack > previousStack.Value) { flag |= DecayRuntime.ComposeInventoryStackMetadata(inventory, key, state.SourceClockValue, state.PreviousLifetimes[key], state.SourceLifetime); } } } } finally { RestoreTemporarySourceMetadataSafe(inventory, source, state); } if (flag) { inventory.Changed(); } } internal static void RestoreTemporarySourceMetadataSafe(Inventory inventory, ItemData? source, InventoryAddMergeState? state) { if (source == null || state == null) { return; } try { RestoreTemporarySourceMetadata(inventory, source, state); } catch (Exception ex) { if (!_loggedTemporarySourceRestoreFailure) { _loggedTemporarySourceRestoreFailure = true; FineDiningPlugin.Log.LogWarning((object)("Could not restore a temporarily activated source stack after Inventory.AddItem: " + ex)); } } } private static void CaptureOriginalSourceMetadata(ItemData source, InventoryAddMergeState state) { state.SourceHadValidClock = DecayRuntime.TryGetExpiryTicks(source, out var _); if (source.m_customData != null) { state.SourceHadExpiryKey = source.m_customData.TryGetValue("sighsorry.FineDining.ExpiryWorldTicks", out state.OriginalSourceExpiryValue); state.SourceHadLifetimeKey = source.m_customData.TryGetValue("sighsorry.FineDining.AssignedLifetimeTicks", out state.OriginalSourceLifetimeValue); } } private static void RestoreTemporarySourceMetadata(Inventory inventory, ItemData source, InventoryAddMergeState state) { if (!state.SourceHadValidClock && !inventory.m_inventory.Contains(source)) { if (source.m_customData == null) { source.m_customData = new Dictionary<string, string>(); } RestoreKey(source.m_customData, "sighsorry.FineDining.ExpiryWorldTicks", state.SourceHadExpiryKey, state.OriginalSourceExpiryValue); RestoreKey(source.m_customData, "sighsorry.FineDining.AssignedLifetimeTicks", state.SourceHadLifetimeKey, state.OriginalSourceLifetimeValue); } } private static void RestoreKey(Dictionary<string, string> customData, string key, bool hadKey, string? originalValue) { if (hadKey) { customData[key] = originalValue ?? string.Empty; } else { customData.Remove(key); } } private static bool CanPotentiallyStack(ItemData existing, ItemData source) { if (existing.m_shared == null || source.m_shared == null) { return false; } if (existing.m_shared.m_name == source.m_shared.m_name && existing.m_worldLevel == source.m_worldLevel) { if (existing.m_shared.m_maxQuality > 1) { return existing.m_quality == source.m_quality; } return true; } return false; } } [HarmonyPatch(typeof(Inventory), "AddItem", new Type[] { typeof(ItemData) })] internal static class InventoryAddItemSpoilagePatch { [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyBefore(new string[] { "sighsorry.InventorySlots" })] private static void Prefix(Inventory __instance, ItemData item, out InventoryAddMergeState? __state) { __state = InventoryAddMergeTracker.Prefix(__instance, item); } [HarmonyPostfix] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "sighsorry.InventorySlots" })] private static void Postfix(Inventory __instance, ItemData item, InventoryAddMergeState? __state) { InventoryAddMergeTracker.Postfix(__instance, item, __state); } private static Exception? Finalizer(Inventory __instance, ItemData item, InventoryAddMergeState? __state, Exception? __exception) { InventoryAddMergeTracker.RestoreTemporarySourceMetadataSafe(__instance, item, __state); return __exception; } } [HarmonyPatch(typeof(Inventory), "AddItem", new Type[] { typeof(ItemData), typeof(Vector2i) })] internal static class InventoryAddItemAtPositionSpoilagePatch { [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyBefore(new string[] { "sighsorry.InventorySlots" })] private static void Prefix(Inventory __instance, ItemData item, out InventoryAddMergeState? __state) { __state = InventoryAddMergeTracker.Prefix(__instance, item); } [HarmonyPostfix] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "sighsorry.InventorySlots" })] private static void Postfix(Inventory __instance, ItemData item, InventoryAddMergeState? __state) { InventoryAddMergeTracker.Postfix(__instance, item, __state); } private static Exception? Finalizer(Inventory __instance, ItemData item, InventoryAddMergeState? __state, Exception? __exception) { InventoryAddMergeTracker.RestoreTemporarySourceMetadataSafe(__instance, item, __state); return __exception; } } [HarmonyPatch(typeof(Inventory), "AddItem", new Type[] { typeof(ItemData), typeof(int), typeof(int), typeof(int) })] internal static class InventoryAddItemAmountAtPositionSpoilagePatch { [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyBefore(new string[] { "sighsorry.InventorySlots" })] private static void Prefix(Inventory __instance, ItemData item, out InventoryAddMergeState? __state) { __state = InventoryAddMergeTracker.Prefix(__instance, item); } [HarmonyPostfix] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "sighsorry.InventorySlots" })] private static void Postfix(Inventory __instance, ItemData item, InventoryAddMergeState? __state) { InventoryAddMergeTracker.Postfix(__instance, item, __state); } private static Exception? Finalizer(Inventory __instance, ItemData item, InventoryAddMergeState? __state, Exception? __exception) { InventoryAddMergeTracker.RestoreTemporarySourceMetadataSafe(__instance, item, __state); return __exception; } } internal static class DecayRuntime { private sealed class InventoryState { internal bool Dirty = true; internal bool Reconciling; internal bool SuppressDirty; internal bool PendingNotification; internal bool EnvironmentKnown; internal bool PausedByCold; internal bool BiomeSampleKnown; internal bool SampledBiomePaused; internal Vector3 SampledBiomePosition; internal float NextVisiblePreparationAt; internal long NextExpiryTicks = long.MaxValue; } private sealed class GroundEnvironmentSample { internal bool Paused; internal Vector3 Position; } private enum PreservationState { Unknown, Running, Paused } private enum ReplacementResolution { NotReady, Invalid, Ready } internal const string ExpiryDataKey = "sighsorry.FineDining.ExpiryWorldTicks"; internal const string PlacedAnchorDataKey = "sighsorry.FineDining.PlacedWorldTicks"; private static readonly Dictionary<Inventory, InventoryState> InventoryStates = new Dictionary<Inventory, InventoryState>(); private static readonly Dictionary<Inventory, WeakReference<Container>> ContainersByInventory = new Dictionary<Inventory, WeakReference<Container>>(); private static readonly Dictionary<int, WeakReference<ItemDrop>> GroundDropsByInstanceId = new Dictionary<int, WeakReference<ItemDrop>>(); private static readonly Dictionary<int, GroundEnvironmentSample> GroundEnvironmentSamples = new Dictionary<int, GroundEnvironmentSample>(); private static readonly List<KeyValuePair<int, WeakReference<ItemDrop>>> GroundDropSnapshot = new List<KeyValuePair<int, WeakReference<ItemDrop>>>(); private static readonly HashSet<string> LoggedReplacementWarnings = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static float _nextTickAt; private static bool _loggedFirstGroundLoadFailure; internal static void Tick() { if (Time.unscaledTime < _nextTickAt) { return; } _nextTickAt = Time.unscaledTime + 1f; if (!TryGetWorldTicks(out var ticks)) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { Inventory inventory = ((Humanoid)localPlayer).GetInventory(); if (inventory != null) { ProcessIfDue(inventory, ticks); } } PruneAndProcessContainers(ticks); PruneAndProcessGroundDrops(ticks); } internal static void RegisterContainer(Container? container) { if (!((Object)(object)container == (Object)null) && container.m_inventory != null && (!ContainersByInventory.TryGetValue(container.m_inventory, out WeakReference<Container> value) || !value.TryGetTarget(out var target) || target != container)) { ContainersByInventory[container.m_inventory] = new WeakReference<Container>(container); GetState(container.m_inventory).Dirty = true; } } internal static void ContainerLoaded(Container? container) { RegisterContainer(container); if (!((Object)(object)container == (Object)null) && IsOwnedContainer(container) && TryGetWorldTicks(out var ticks)) { GetState(container.m_inventory).Dirty = true; ProcessIfDue(container.m_inventory, ticks); } } internal static void MarkDirty(Inventory? inventory) { if (inventory != null && InventoryStates.TryGetValue(inventory, out InventoryState value) && !value.SuppressDirty) { value.Dirty = true; value.NextVisiblePreparationAt = 0f; } } internal static void InvalidateAll() { foreach (InventoryState value in InventoryStates.Values) { value.Dirty = true; value.EnvironmentKnown = false; value.BiomeSampleKnown = false; value.NextExpiryTicks = long.MinValue; value.NextVisiblePreparationAt = 0f; } GroundEnvironmentSamples.Clear(); if (ItemDrop.s_instances == null) { return; } ItemDrop[] array = ItemDrop.s_instances.ToArray(); foreach (ItemDrop val in array) { if ((Object)(object)val != (Object)null && IsPlacedGroundDrop(val)) { RegisterGroundDrop(val); } } } internal static void Reset() { InventoryStates.Clear(); ContainersByInventory.Clear(); GroundDropsByInstanceId.Clear(); GroundEnvironmentSamples.Clear(); GroundDropSnapshot.Clear(); LoggedReplacementWarnings.Clear(); _nextTickAt = 0f; _loggedFirstGroundLoadFailure = false; } internal static bool TryPrepareVisibleInventoryTimers(Inventory? inventory, out long nowTicks) { nowTicks = 0L; if (inventory == null || !TryGetWorldTicks(out nowTicks)) { return false; } if (IsAuthoritativeInventory(inventory)) { InventoryState state = GetState(inventory); if (Time.unscaledTime >= state.NextVisiblePreparationAt) { state.NextVisiblePreparationAt = Time.unscaledTime + 1f; if (IsPolicyReadyForInventory(inventory)) { ProcessIfDue(inventory, nowTicks); } } } return true; } internal static bool TryGetExpiryTicks(ItemData? item, out long clockValue) { clockValue = 0L; if (item?.m_customData != null && item.m_customData.TryGetValue("sighsorry.FineDining.ExpiryWorldTicks", out var value)) { return SpoilageClock.TryParseClockValue(value, out clockValue); } return false; } internal static bool TryGetSpoilageClock(ItemData? item, long nowTicks, out long remainingTicks, out bool paused) { remainingTicks = 0L; paused = false; if (TryGetExpiryTicks(item, out var clockValue)) { return SpoilageClock.TryDecodeClockValue(clockValue, nowTicks, out remainingTicks, out paused); } return false; } internal static string? ComposeStackClockValues(string? destinationValue, string? sourceValue) { if (!SpoilageClock.TryParseClockValue(sourceValue, out var clockValue)) { return destinationValue; } if (!SpoilageClock.TryParseClockValue(destinationValue, out var clockValue2)) { if (destinationValue != null) { return destinationValue; } return clockValue.ToString(CultureInfo.InvariantCulture); } long ticks; bool flag = TryGetWorldTicks(out ticks); if (!flag && clockValue2 < 0 != clockValue < 0) { return clockValue2.ToString(CultureInfo.InvariantCulture); } long nowTicks = (flag ? ticks : 0); return SpoilageClock.ComposeClockValues(clockValue2, clockValue, nowTicks, clockValue2 < 0).ToString(CultureInfo.InvariantCulture); } internal static bool CanMergeStackClockValues(string? destinationValue, string? sourceValue) { bool flag = destinationValue != null; bool flag2 = sourceValue != null; long clockValue = 0L; long clockValue2 = 0L; if (flag && !SpoilageClock.TryParseClockValue(destinationValue, out clockValue)) { return false; } if (flag2 && !SpoilageClock.TryParseClockValue(sourceValue, out clockValue2)) { return false; } if (!flag || !flag2 || clockValue < 0 == clockValue2 < 0) { return true; } long ticks; return TryGetWorldTicks(out ticks); } internal static bool PrepareItemForAdd(Inventory inventory, ItemData? item) { if (item == null || !IsAuthoritativeInventory(inventory)) { return false; } bool isPlayerInventory = IsLocalPlayerInventory(inventory); long clockValue; bool hasClock = TryGetExpiryTicks(item, out clockValue); if (!ShouldProcessInventorySpoilageClock(isPlayerInventory, hasClock) || !TryGetWorldTicks(out var ticks)) { return false; } ResolvedSpoilageRule rule = SpoilagePolicy.Resolve(item); bool paused; if (rule.State == SpoilageRuleState.Enabled) { return EnsureItemState(item, rule, ticks, ResolveInventoryPausedState(inventory), transitionExisting: false, out clockValue, out paused); } return false; } internal static bool ShouldProcessInventorySpoilageClock(bool isPlayerInventory, bool hasClock) { return isPlayerInventory || hasClock; } internal static bool PrepareInheritedItemForAdd(Inventory inventory, ItemData? item, long inheritedRemainingTicks) { if (item == null || !IsAuthoritativeInventory(inventory)) { return false; } bool flag = PrepareItemForAdd(inventory, item); if (inheritedRemainingTicks < 0 || SpoilagePolicy.Resolve(item).State != SpoilageRuleState.Enabled) { return flag; } bool destinationPaused = ResolveInventoryPausedState(inventory); return ApplyEarlierRemaining(item, inheritedRemainingTicks, destinationPaused) || flag; } internal static bool ApplyEarlierInventoryClock(Inventory inventory, ItemData? target, long sourceClockValue) { if (target == null || !SpoilageClock.IsValidClockValue(sourceClockValue) || !TryGetWorldTicks(out var ticks)) { return false; } return ApplyEarlierClock(target, sourceClockValue, ticks, ResolveInventoryPausedState(inventory)); } internal static bool ComposeInventoryStackMetadata(Inventory inventory, ItemData target, long sourceClockValue, AssignedLifetimeSnapshot targetLifetime, AssignedLifetimeSnapshot sourceLifetime) { if (inventory == null || target == null || !SpoilageClock.IsValidClockValue(sourceClockValue)) { return false; } return ApplyEarlierInventoryClock(inventory, target, sourceClockValue) | FreshnessRuntime.ComposeAssignedLifetime(target, targetLifetime, sourceLifetime); } internal static bool ComposeDirectRecoveryMergeExpiry(ItemData? target, ItemData? source, int movedAmount) { if (movedAmount <= 0 || target == null || source == null || target == source || target.m_customData == null || !TryGetExpiryTicks(source, out var clockValue)) { return false; } AssignedLifetimeSnapshot destination = FreshnessRuntime.CaptureAssignedLifetime(target); AssignedLifetimeSnapshot source2 = FreshnessRuntime.CaptureAssignedLifetime(source); long clockValue2; bool flag = TryGetExpiryTicks(target, out clockValue2); if (!TryGetWorldTicks(out var ticks)) { if (flag && clockValue2 < 0 != clockValue < 0) { return false; } ticks = 0L; } bool destinationPaused = (flag ? (clockValue2 < 0) : (clockValue < 0)); return ApplyEarlierClock(target, clockValue, ticks, destinationPaused) | FreshnessRuntime.ComposeAssignedLifetime(target, destination, source2); } internal static void ComposeGroundStackExpiry(ItemDrop? destination, ItemDrop? source) { if (!((Object)(object)destination == (Object)null) && !((Object)(object)source == (Object)null) && !((Object)(object)destination.m_nview == (Object)null) && !((Object)(object)source.m_nview == (Object)null) && destination.m_nview.IsValid() && source.m_nview.IsValid() && destination.m_nview.IsOwner() && source.m_nview.IsOwner() && TryGetExpiryTicks(source.m_itemData, out var clockValue) && TryGetWorldTicks(out var ticks)) { AssignedLifetimeSnapshot destination2 = FreshnessRuntime.CaptureAssignedLifetime(destination.m_itemData); AssignedLifetimeSnapshot source2 = FreshnessRuntime.CaptureAssignedLifetime(source.m_itemData); bool destinationPaused = ResolveWorldDropPausedState(destination); ApplyEarlierClock(destination.m_itemData, clockValue, ticks, destinationPaused); FreshnessRuntime.ComposeAssignedLifetime(destination.m_itemData, destination2, source2); RegisterGroundDrop(destination); } } internal static void RegisterGroundDrop(ItemDrop? drop) { if ((Object)(object)drop == (Object)null) { return; } int instanceID = ((Object)drop).GetInstanceID(); if (!ReconcileCreatorlessPlacedDrop(drop)) { WeakReference<ItemDrop> value; ItemDrop target; if (!HasValidGroundView(drop) || (!TryGetExpiryTicks(drop.m_itemData, out var _) && !IsPlacedGroundDrop(drop))) { GroundDropsByInstanceId.Remove(instanceID); GroundEnvironmentSamples.Remove(instanceID); } else if (!GroundDropsByInstanceId.TryGetValue(instanceID, out value) || !value.TryGetTarget(out target) || target != drop) { GroundDropsByInstanceId[instanceID] = new WeakReference<ItemDrop>(drop); } } } internal static void RefreshOwnedPlacedDrop(ItemDrop? drop) { if ((Object)(object)drop == (Object)null || !IsOwnedGroundDrop(drop) || !IsPlacedGroundDrop(drop)) { return; } try { drop.Load(); RegisterGroundDrop(drop); } catch (Exception ex) { if (!_loggedFirstGroundLoadFailure) { _loggedFirstGroundLoadFailure = true; FineDiningPlugin.Log.LogWarning((object)("Could not refresh an owned placed food item; FineDining will retry: " + ex)); } } } internal static void InitializePlacedDrop(ItemDrop? drop, long inheritedRemainingTicks, long placementTicks = 0L) { if ((Object)(object)drop == (Object)null || !HasValidGroundView(drop) || !IsPlacedGroundDrop(drop) || ReconcileCreatorlessPlacedDrop(drop)) { return; } if (IsOwnedGroundDrop(drop)) { long clockValue; bool hasPersistedExpiry = TryGetExpiryTicks(drop.m_itemData, out clockValue); bool hasPendingAnchor = drop.m_itemData?.m_customData.ContainsKey("sighsorry.FineDining.PlacedWorldTicks") ?? false; if (!ShouldInitializePlacedDeadline(placementTicks, hasPersistedExpiry, hasPendingAnchor)) { RegisterGroundDrop(drop); } else { InitializeOwnedWorldDrop(drop, inheritedRemainingTicks, placementTicks, keepPendingWhenNotReady: true); } } else { RegisterGroundDrop(drop); } } internal static bool ShouldInitializePlacedDeadline(long placementTicks, bool hasPersistedExpiry, bool hasPendingAnchor) { return placementTicks > 0 || !hasPersistedExpiry || hasPendingAnchor; } internal static void InitializeRecoveredDrop(ItemDrop? drop, long inheritedRemainingTicks) { if (!((Object)(object)drop == (Object)null) && inheritedRemainingTicks >= 0 && IsOwnedGroundDrop(drop)) { InitializeOwnedWorldDrop(drop, inheritedRemainingTicks, 0L, keepPendingWhenNotReady: false); } } internal static long CalculateEffectiveRemainingTicks(long nowTicks, long anchorTicks, long lifetimeTicks, long inheritedRemainingTicks, bool paused) { long num = Math.Max(10000000L, lifetimeTicks); long num2 = ((!paused && anchorTicks > 0 && nowTicks > anchorTicks) ? (nowTicks - anchorTicks) : 0); long num3 = ((num2 >= num) ? 0 : (num - num2)); if (inheritedRemainingTicks < 0) { return num3; } return Math.Min(inheritedRemainingTicks, num3); } internal static void UnregisterGroundDrop(ItemDrop? drop) { if ((Object)(object)drop != (Object)null) { int instanceID = ((Object)drop).GetInstanceID(); GroundDropsByInstanceId.Remove(instanceID); GroundEnvironmentSamples.Remove(instanceID); } } internal static bool IsAuthoritativeInventory(Inventory? inventory) { if (inventory == null) { return false; } if (IsLocalPlayerInventory(inventory)) { GetState(inventory); return true; } if (!ContainersByInventory.TryGetValue(inventory, out WeakReference<Container> value) || !value.TryGetTarget(out var target) || (Object)(object)target == (Object)null) { return false; } return IsOwnedContainer(target); } private static bool IsLocalPlayerInventory(Inventory? inventory) { Player localPlayer = Player.m_localPlayer; if (inventory != null && (Object)(object)localPlayer != (Object)null) { return ((Humanoid)localPlayer).GetInventory() == inventory; } return false; } internal static bool IsContainerLoading(Inventory? inventory) { if (TryGetContainer(inventory, out Container container)) { return container.m_loading; } return false; } internal static bool TryGetContainer(Inventory? inventory, out Container? container) { container = null; if (inventory == null || !ContainersByInventory.TryGetValue(inventory, out WeakReference<Container> value) || !value.TryGetTarget(out var target) || (Object)(object)target == (Object)null) { return false; } container = target; return true; } private static void ProcessIfDue(Inventory inventory, long nowTicks) { InventoryState state = GetState(inventory); PreservationState preservationState = ResolveInventoryPreservationState(inventory); if (preservationState != PreservationState.Unknown) { bool flag = preservationState == PreservationState.Paused; if (!state.EnvironmentKnown || state.PausedByCold != flag) { state.EnvironmentKnown = true; state.PausedByCold = flag; state.Dirty = true; } } if (state.Dirty || nowTicks >= state.NextExpiryTicks) { Reconcile(inventory, state, nowTicks); } } private static bool IsPolicyReadyForInventory(Inventory inventory) { foreach (ItemData item in inventory.m_inventory) { if (item != null && SpoilagePolicy.Resolve(item).State == SpoilageRuleState.NotReady) { return false; } } return true; } private static void PruneAndProcessContainers(long nowTicks) { List<Inventory> list = null; foreach (KeyValuePair<Inventory, WeakReference<Container>> item in ContainersByInventory) { if (!item.Value.TryGetTarget(out var target) || (Object)(object)target == (Object)null || target.m_inventory == null) { if (list == null) { list = new List<Inventory>(); } list.Add(item.Key); } else if (IsOwnedContainer(target)) { ProcessIfDue(item.Key, nowTicks); } } if (list == null) { return; } foreach (Inventory item2 in list) { ContainersByInventory.Remove(item2); InventoryStates.Remove(item2); } } private static void PruneAndProcessGroundDrops(long nowTicks) { GroundDropSnapshot.Clear(); foreach (KeyValuePair<int, WeakReference<ItemDrop>> item in GroundDropsByInstanceId) { GroundDropSnapshot.Add(item); } List<int> list = null; List<ItemDrop> list2 = null; foreach (KeyValuePair<int, WeakReference<ItemDrop>> item2 in GroundDropSnapshot) { if (!item2.Value.TryGetTarget(out var target) || (Object)(object)target == (Object)null || !HasValidGroundView(target)) { if (list == null) { list = new List<int>(); } list.Add(item2.Key); continue; } try { target.Load(); } catch (Exception ex) { if (!_loggedFirstGroundLoadFailure) { _loggedFirstGroundLoadFailure = true; FineDiningPlugin.Log.LogWarning((object)("Could not refresh a tracked world item; FineDining will retry: " + ex)); } continue; } bool flag = IsPlacedGroundDrop(target); if (flag && ReconcileCreatorlessPlacedDrop(target)) { if (list == null) { list = new List<int>(); } list.Add(item2.Key); continue; } if (flag && IsOwnedGroundDrop(target)) { ItemData itemData = target.m_itemData; if (itemData != null && itemData.m_customData.ContainsKey("sighsorry.FineDining.PlacedWorldTicks")) { long inheritedRemainingTicks = -1L; if (TryGetSpoilageClock(target.m_itemData, nowTicks, out var remainingTicks, out var _)) { inheritedRemainingTicks = remainingTicks; } InitializeOwnedWorldDrop(target, inheritedRemainingTicks, 0L, keepPendingWhenNotReady: true); } } if (!TryGetExpiryTicks(target.m_itemData, out var clockValue)) { if (!flag) { if (list == null) { list = new List<int>(); } list.Add(item2.Key); continue; } if (!IsOwnedGroundDrop(target)) { if (list == null) { list = new List<int>(); } list.Add(item2.Key); continue; } InitializeOwnedWorldDrop(target, -1L, 0L, keepPendingWhenNotReady: true); if (!TryGetExpiryTicks(target.m_itemData, out clockValue)) { continue; } } if (!IsOwnedGroundDrop(target)) { continue; } ResolvedSpoilageRule rule = SpoilagePolicy.Resolve(target.m_itemData); if (rule.State == SpoilageRuleState.NotReady) { continue; } bool expired; if (rule.State != SpoilageRuleState.Enabled) { ClearOwnedGroundExpiry(target); if (list == null) { list = new List<int>(); } list.Add(item2.Key); } else if (TryEvaluateGroundExpiry(target, rule, nowTicks, out expired) && expired) { if (list == null) { list = new List<int>(); } list.Add(item2.Key); if (list2 == null) { list2 = new List<ItemDrop>(); } list2.Add(target); } } GroundDropSnapshot.Clear(); if (list != null) { foreach (int item3 in list) { GroundDropsByInstanceId.Remove(item3); GroundEnvironmentSamples.Remove(item3); } } if (list2 == null) { return; } foreach (ItemDrop item4 in list2) { try { if (!IsOwnedGroundDrop(item4)) { RegisterGroundDrop(item4); continue; } ResolvedSpoilageRule rule2 = SpoilagePolicy.Resolve(item4.m_itemData); bool expired2; if (rule2.State == SpoilageRuleState.NotReady) { RegisterGroundDrop(item4); } else if (rule2.State != SpoilageRuleState.Enabled) { ClearOwnedGroundExpiry(item4); } else if (!TryEvaluateGroundExpiry(item4, rule2, nowTicks, out expired2) || !expired2) { RegisterGroundDrop(item4); } else { ExpireGroundStack(item4, rule2); } } catch (Exception ex2) { FineDiningPlugin.Log.LogError((object)("Failed to expire a tracked world item: " + ex2)); RegisterGroundDrop(item4); } } } private static bool TryEvaluateGroundExpiry(ItemDrop drop, ResolvedSpoilageRule rule, long nowTicks, out bool expired) { expired = false; if (ReconcileCreatorlessPlacedDrop(drop)) { return false; } ItemData itemData = drop.m_itemData; if (itemData == null) { return false; } if (EnsureItemState(itemData, rule, nowTicks, ResolveWorldDropPausedState(drop, rule.Group), transitionExisting: true, out var remainingTicks, out var paused)) { drop.Save(); } expired = !paused && remainingTicks <= 0; return true; } private static void ClearOwnedGroundExpiry(ItemDrop drop) { if (IsOwnedGroundDrop(drop)) { bool flag = false; if (drop.m_itemData?.m_customData != null) { flag |= drop.m_itemData.m_customData.Remove("sighsorry.FineDining.ExpiryWorldTicks"); flag |= drop.m_itemData.m_customData.Remove("sighsorry.FineDining.PlacedWorldTicks"); flag |= FreshnessRuntime.ClearTrackedMetadata(drop.m_itemData); } if (flag) { drop.Save(); } UnregisterGroundDrop(drop); } } private static void InitializeOwnedWorldDrop(ItemDrop drop, long inheritedRemainingTicks, long anchorTicks, bool keepPendingWhenNotReady) { if (!IsOwnedGroundDrop(drop) || ReconcileCreatorlessPlacedDrop(drop)) { return; } bool flag = false; long ticks = 0L; if (keepPendingWhenNotReady && !TryGetPositiveCustomTicks(drop.m_itemData, "sighsorry.FineDining.PlacedWorldTicks", out ticks)) { ticks = anchorTicks; if (ticks <= 0) { TryGetWorldTicks(out ticks); } if (ticks > 0) { drop.m_itemData.m_customData["sighsorry.FineDining.PlacedWorldTicks"] = ticks.ToString(CultureInfo.InvariantCulture); flag = true; } } ResolvedSpoilageRule resolvedSpoilageRule = SpoilagePolicy.Resolve(drop.m_itemData); if (resolvedSpoilageRule.State == SpoilageRuleState.NotReady) { if (flag | (inheritedRemainingTicks >= 0 && ApplyEarlierRemaining(drop.m_itemData, inheritedRemainingTicks, ResolveWorldDropPausedState(drop)))) { drop.Save(); } if (keepPendingWhenNotReady || inheritedRemainingTicks >= 0) { RegisterGroundDrop(drop); } return; } if (resolvedSpoilageRule.State != SpoilageRuleState.Enabled) { ClearOwnedGroundExpiry(drop); return; } if (!TryGetWorldTicks(out var ticks2)) { RegisterGroundDrop(drop); return; } bool flag2 = ResolveWorldDropPausedState(drop, resolvedSpoilageRule.Group); long num = CalculateEffectiveRemainingTicks(ticks2, ticks, resolvedSpoilageRule.LifetimeTicks, inheritedRemainingTicks, flag2); long clockValue = SpoilageClock.EncodeClockValue(ticks2, num, flag2 && num > 0); flag |= SetClockValue(drop.m_itemData, clockValue); flag |= FreshnessRuntime.EnsureTrackedMetadata(drop.m_itemData, resolvedSpoilageRule.LifetimeTicks); if (keepPendingWhenNotReady) { flag |= drop.m_itemData.m_customData.Remove("sighsorry.FineDining.PlacedWorldTicks"); } if (flag) { drop.Save(); } RegisterGroundDrop(drop); } private static bool HasValidGroundView(ItemDrop drop) { try { return (Object)(object)drop.m_nview != (Object)null && drop.m_nview.IsValid(); } catch { return false; } } private static bool IsOwnedGroundDrop(ItemDrop drop) { try { return HasValidGroundView(drop) && drop.m_nview.IsOwner(); } catch { return false; } } private static bool IsPlacedGroundDrop(ItemDrop drop) { try { if (!HasValidGroundView(drop)) { return false; } return drop.m_nview.GetZDO().GetBool(ZDOVars.s_piece, false) || drop.IsPiece(); } catch { return false; } } internal static bool IsCreatorlessPlacedDrop(ItemDrop? drop) { try { return (Object)(object)drop != (Object)null && IsPlacedGroundDrop(drop) && drop.m_nview.GetZDO().GetLong(ZDOVars.s_creator, 0L) == 0; } catch { return false; } } private static bool ReconcileCreatorlessPlacedDrop(ItemDrop? drop) { if (!IsCreatorlessPlacedDrop(drop)) { return false; } if (IsOwnedGroundDrop(drop)) { ClearOwnedGroundExpiry(drop); } else { UnregisterGroundDrop(drop); } return true; } private static bool TryGetPositiveCustomTicks(ItemData? item, string key, out long ticks) { ticks = 0L; if (item?.m_customData != null && item.m_customData.TryGetValue(key, out var value) && long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ticks) && ticks > 0) { return string.Equals(value, ticks.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); } return false; } private static PreservationState ResolveInventoryPreservationState(Inventory inventory) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) InventoryState state = GetState(inventory); Player localPlayer = Player.m_localPlayer; if (IsLocalPlayerInventory(inventory)) { return SampleInventoryBiome(state, ((Component)localPlayer).transform.position); } if (ContainersByInventory.TryGetValue(inventory, out WeakReference<Container> value) && value.TryGetTarget(out var target) && (Object)(object)target != (Object)null) { if (GeneratedPrefabRegistry.IsIcebox(target)) { return PreservationState.Paused; } return SampleInventoryBiome(state, ((Component)target).transform.position); } return PreservationState.Unknown; } private static bool ResolveInventoryPausedState(Inventory inventory) { PreservationState preservationState = ResolveInventoryPreservationState(inventory); if (preservationState != PreservationState.Unknown) { return preservationState == PreservationState.Paused; } InventoryState state = GetState(inventory); if (state.EnvironmentKnown) { return state.PausedByCold; } return false; } private static bool ResolveWorldDropPausedState(ItemDrop drop, SpoilageGroup? knownGroup = null) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) if (IsFishPreservedInWater(drop, knownGroup)) { return true; } int instanceID = ((Object)drop).GetInstanceID(); Vector3 position = ((Component)drop).transform.position; if (GroundEnvironmentSamples.TryGetValue(instanceID, out GroundEnvironmentSample value) && PositionsMatch(value.Position, position)) { return value.Paused; } if (TryIsNoSpoilBiome(position, out var preserved, out var cacheable)) { if (cacheable) { GroundEnvironmentSamples[instanceID] = new GroundEnvironmentSample { Position = position, Paused = preserved }; } return preserved; } if (TryGetExpiryTicks(drop.m_itemData, out var clockValue)) { return clockValue < 0; } return false; } private static bool IsFishPreservedInWater(ItemDrop? drop, SpoilageGroup? knownGroup) { //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)drop == (Object)null) { return false; } try { SpoilageGroup spoilageGroup; if (knownGroup.HasValue) { spoilageGroup = knownGroup.Value; } else { ResolvedSpoilageRule resolvedSpoilageRule = SpoilagePolicy.Resolve(drop.m_itemData); if (resolvedSpoilageRule.State != SpoilageRuleState.Enabled) { return false; } spoilageGroup = resolvedSpoilageRule.Group; } if (spoilageGroup != SpoilageGroup.Fish) { return false; } Fish val = ((Component)drop).GetComponent<Fish>() ?? ((Component)drop).GetComponentInParent<Fish>() ?? ((Component)drop).GetComponentInChildren<Fish>(true); if ((Object)(object)val != (Object)null) { return !val.IsOutOfWater(); } Floating val2 = drop.m_floating ?? ((Component)drop).GetComponent<Floating>(); if ((Object)(object)val2 == (Object)null || val2.m_waterLevel <= -10000f) { return false; } return (((Object)(object)val2.m_body != (Object)null) ? val2.m_body.worldCenterOfMass.y : ((Component)drop).transform.position.y) - val2.m_waterLevel - val2.m_waterLevelOffset <= 0.05f; } catch { return false; } } private static PreservationState SampleInventoryBiome(InventoryState state, Vector3 position) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_003b: 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) if (state.BiomeSampleKnown && PositionsMatch(state.SampledBiomePosition, position)) { if (!state.SampledBiomePaused) { return PreservationState.Running; } return PreservationState.Paused; } if (!TryIsNoSpoilBiome(position, out var preserved, out var cacheable)) { return PreservationState.Unknown; } if (cacheable) { state.BiomeSampleKnown = true; state.SampledBiomePosition = position; state.SampledBiomePaused = preserved; } if (!preserved) { return PreservationState.Running; } return PreservationState.Paused; } private static bool PositionsMatch(Vector3 left, Vector3 right) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) Vector3 val = left - right; return ((Vector3)(ref val)).sqrMagnitude <= 0.0001f; } private static bool TryIsNoSpoilBiome(Vector3 position, out bool preserved, out bool cacheable) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) preserved = false; cacheable = false; try { Biome val = Heightmap.FindBiome(position); cacheable = (int)val > 0; if ((int)val == 0 && WorldGenerator.instance != null) { val = WorldGenerator.instance.GetBiome(position.x, position.z, 0.02f, false); } if ((int)val == 0) { return false; } preserved = PreservationConfig.IsNoSpoilBiome(val); return true; } catch { cacheable = false; return false; } } internal static List<KeyValuePair<ItemData, string>> BeginPlayerSaveClockSnapshot(Player? player) { List<KeyValuePair<ItemData, string>> list = new List<KeyValuePair<ItemData, string>>(); Inventory val = ((player != null) ? ((Humanoid)player).GetInventory() : null); if (val == null || !TryGetWorldTicks(out var ticks)) { return list; } foreach (ItemData item in val.m_inventory) { if (TryGetSpoilageClock(item, ticks, out var remainingTicks, out var paused) && paused && item.m_customData.TryGetValue("sighsorry.FineDining.ExpiryWorldTicks", out var value)) { list.Add(new KeyValuePair<ItemData, string>(item, value)); item.m_customData["sighsorry.FineDining.ExpiryWorldTicks"] = SpoilageClock.EncodeClockValue(ticks, remainingTicks, paused: false).ToString(CultureInfo.InvariantCulture); } } return list; } internal static void EndPlayerSaveClockSnapshot(List<KeyValuePair<ItemData, string>>? pausedClocks) { if (pausedClocks == null) { return; } foreach (KeyValuePair<ItemData, string> pausedClock in pausedClocks) { if (pausedClock.Key?.m_customData != null) { pausedClock.Key.m_customData["sighsorry.FineDining.ExpiryWorldTicks"] = pausedClock.Value; } } } private static bool IsOwnedContainer(Container container) { try { return (Object)(object)container.m_nview != (Object)null && container.m_nview.IsValid() && container.m_nview.IsOwner(); } catch { return false; } } private static InventoryState GetState(Inventory inventory) { if (!InventoryStates.TryGetValue(inventory, out InventoryState value)) { value = new InventoryState(); InventoryStates.Add(inventory, value); } return value; } private static void Reconcile(Inventory inventory, InventoryState state, long nowTicks) { if (state.Reconciling) { return; } state.Reconciling = true; bool flag = state.PendingNotification; long num = long.MaxValue; bool shouldPause = state.EnvironmentKnown && state.PausedByCold; bool isPlayerInventory = IsLocalPlayerInventory(inventory); try { List<ItemData> inventory2 = inventory.m_inventory; for (int num2 = inventory2.Count - 1; num2 >= 0; num2--) { ItemData val = inventory2[num2]; if (val != null) { ResolvedSpoilageRule rule = SpoilagePolicy.Resolve(val); if (rule.State != SpoilageRuleState.NotReady) { if (rule.State != SpoilageRuleState.Enabled) { if (val.m_customData.Remove("sighsorry.FineDining.ExpiryWorldTicks")) { flag = true; } flag |= FreshnessRuntime.ClearTrackedMetadata(val); } else { long clockValue; bool hasClock = TryGetExpiryTicks(val, out clockValue); if (ShouldProcessInventorySpoilageClock(isPlayerInventory, hasClock)) { if (EnsureItemState(val, rule, nowTicks, shouldPause, state.EnvironmentKnown, out var remainingTicks, out var paused)) { flag = true; } if (!paused) { if (remainingTicks <= 0) { bool flag2 = ExpireItem(inventory, val, rule); flag = flag || flag2; if (!flag2) { num = Math.Min(num, nowTicks); } } else { long val2 = SpoilageClock.AddTicksSaturating(nowTicks, remainingTicks); num = Math.Min(num, val2); } } } } } } } if (flag) { state.PendingNotification = true; state.SuppressDirty = true; inventory.Changed(); state.PendingNotification = false; } state.Dirty = false; state.NextExpiryTicks = num; } catch (Exception ex) { state.Dirty = true; FineDiningPlugin.Log.LogError((object)("Failed to reconcile inventory spoilage: " + ex)); } finally { state.SuppressDirty = false; state.Reconciling = false; } } private static bool EnsureItemState(ItemData item, ResolvedSpoilageRule rule, long nowTicks, bool shouldPause, bool transitionExisting, out long remainingTicks, out bool paused) { remainingTicks = 0L; paused = false; if (rule.State != SpoilageRuleState.Enabled) { return false; } bool result = FreshnessRuntime.EnsureTrackedMetadata(item, rule.LifetimeTicks); if (TryGetSpoilageClock(item, nowTicks, out remainingTicks, out paused)) { if (!transitionExisting || paused == shouldPause) { return result; } if (!paused && remainingTicks <= 0) { return result; } paused = shouldPause; long num = SpoilageClock.EncodeClockValue(nowTicks, remainingTicks, paused); item.m_customData["sighsorry.FineDining.ExpiryWorldTicks"] = num.ToString(CultureInfo.InvariantCulture); return true; } long num2 = Math.Max(10000000L, rule.LifetimeTicks); remainingTicks = num2; paused = shouldPause; long num3 = SpoilageClock.EncodeClockValue(nowTicks, remainingTicks, paused); item.m_customData["sighsorry.FineDining.ExpiryWorldTicks"] = num3.ToString(CultureInfo.InvariantCulture); return true; } private static ReplacementResolution ResolveReplacement(ItemData sourceItem, ResolvedSpoilageRule rule, out string configuredPrefab, out ItemDrop replacementDrop) { configuredPrefab = FoodIdentity.NormalizePrefabName(rule.ReplacementPrefab); replacementDrop = null; ObjectDB instance = ObjectDB.instance; if (!IsObjectDatabaseReady(instance)) { return ReplacementResolution.NotReady; } bool flag = GeneratedPrefabRegistry.IsGeneratedReplacementPrefabName(configuredPrefab); if (flag && !GeneratedPrefabRegistry.EnsureGeneratedReplacementAvailable(configuredPrefab)) { return ReplacementResolution.NotReady; } GameObject val = ResolveItemPrefab(instance, configuredPrefab); if ((Object)(object)val == (Object)null && flag) { return ReplacementResolution.NotReady; } ItemDrop val2 = (((Object)(object)val != (Object)null) ? val.GetComponent<ItemDrop>() : null); string canonicalPrefabName = FoodIdentity.GetCanonicalPrefabName(sourceItem); if (val2?.m_itemData?.m_shared == null || string.Equals(canonicalPrefabName, configuredPrefab, StringComparison.OrdinalIgnoreCase)) { WarnReplacementOnce(configuredPrefab, "Spoiled prefab '" + configuredPrefab + "' is missing, invalid, or the same as the expired item. Removing expired stacks instead."); return ReplacementResolution.Invalid; } replacementDrop = val2; return ReplacementResolution.Ready; } private static bool ExpireItem(Inventory inventory, ItemData item, ResolvedSpoilageRule rule) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) string configuredPrefab; ItemDrop replacementDrop; switch (ResolveReplacement(item, rule, out configuredPrefab, out replacementDrop)) { case ReplacementResolution.NotReady: return false; case ReplacementResolution.Invalid: return inventory.m_inventory.Remove(item); default: { GameObject gameObject = ((Component)replacementDrop).gameObject; ItemData itemData = replacementDrop.m_itemData; int num = Math.Max(0, item.m_stack); Vector2i gridPos = item.m_gridPos; int worldLevel = item.m_worldLevel; if (!inventory.m_inventory.Remove(item)) { return false; } if (num <= 0) { return true; } int num2 = Math.Max(1, itemData.m_shared.m_maxStackSize); int num3 = CalculateReplacementAmount(num); ItemData val = CreateReplacement(itemData, gameObject, worldLevel); int amount = num3; num3 -= InsertReplacementStack(inventory, val, amount, gridPos); if (num3 > 0) { foreach (ItemData item2 in inventory.m_inventory) { if (num3 <= 0) { break; } if (CanStackReplacement(item2, val)) { int num4 = Math.Min(num3, Math.Max(0, num2 - item2.m_stack)); item2.m_stack += num4; num3 -= num4; } } } while (num3 > 0) { ItemData val2 = CreateReplacement(itemData, gameObject, worldLevel); Vector2i val3 = inventory.FindEmptySlot(inventory.TopFirst(val2)); if (val3.x < 0) { break; } int amount2 = Math.Min(num2, num3); int num5 = InsertReplacementStack(inventory, val2, amount2, val3); if (num5 <= 0) { break; } num3 -= num5; } if (num3 > 0) { WarnReplacementOnce("overflow:" + configuredPrefab, "Inventory had no room for " + num3 + " additional '" + configuredPrefab + "' items; overflow was discarded safely."); } return true; } } } private static void ExpireGroundStack(ItemDrop drop, ResolvedSpoilageRule rule) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) if (!IsOwnedGroundDrop(drop)) { RegisterGroundDrop(drop); } else { if (ReconcileCreatorlessPlacedDrop(drop)) { return; } ZDOID uid = drop.m_nview.GetZDO().m_uid; if (!TryGetExpiryTicks(drop.m_itemData, out var clockValue)) { UnregisterGroundDrop(drop); return; } if (drop.m_autoDestroy && drop.GetTimeSinceSpawned() >= 3600.0) { drop.TimedDestruction(); if (!IsOwnedGroundDrop(drop)) { return; } } ItemData itemData = drop.m_itemData; int num = Math.Max(0, itemData?.m_stack ?? 0); if (itemData == null || num <= 0) { drop.m_nview.Destroy(); return; } string configuredPrefab; ItemDrop replacementDrop; ReplacementResolution replacementResolution = ResolveReplacement(itemData, rule, out configuredPrefab, out replacementDrop); if (replacementResolution == ReplacementResolution.NotReady) { RegisterGroundDrop(drop); return; } string canonicalPrefabName = FoodIdentity.GetCanonicalPrefabName(itemData); if (replacementResolution == ReplacementResolution.Invalid) { drop.m_nview.Destroy(); return; } GameObject gameObject = ((Component)replacementDrop).gameObject; ItemData itemData2 = replacementDrop.m_itemData; Vector3 position = ((Component)drop).transform.position; Quaternion rotation = ((Component)drop).transform.rotation; long num2 = (ShouldInheritGroundSpawnTime(drop.m_autoDestroy, IsPlacedGroundDrop(drop)) ? GetGroundSpawnTimeTicks(drop) : 0); if (num2 > 0 && !replacementDrop.m_autoDestroy) { WarnReplacementOnce("cleanup:" + configuredPrefab, "Spoiled prefab '" + configuredPrefab + "' does not use vanilla ItemDrop auto-destruction; the original ground cleanup age cannot be enforced for that replacement."); } int worldLevel = itemData.m_worldLevel; int num3 = CalculateReplacementAmount(num); ItemData val = CreateReplacement(itemData2, gameObject, worldLevel); ItemDrop val2 = null; try { val2 = ItemDrop.DropItem(val, num3, position, rotation); if (!IsOwnedGroundDrop(val2)) { throw new InvalidOperationException("A newly spawned spoilage replacement was not locally owned."); } val2.OnPlayerDrop(); InheritGroundSpawnTime(val2, num2); } catch (Exception ex) { DestroySpawnedGroundReplacement(val2); FineDiningPlugin.Log.LogError((object)("Could not create a ground spoilage replacement; the original stack was kept: " + ex)); RegisterGroundDrop(drop); return; } if (!GroundSourceStillMatches(drop, uid, clockValue, canonicalPrefabName, num)) { DestroySpawnedGroundReplacement(val2); RegisterGroundDrop(drop); } else { drop.m_nview.Destroy(); } } } private static bool GroundSourceStillMatches(ItemDrop drop, ZDOID sourceZdoId, long sourceExpiryTicks, string sourcePrefab, int sourceAmount) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (!IsOwnedGroundDrop(drop) || IsCreatorlessPlacedDrop(drop) || drop.m_nview.GetZDO().m_uid != sourceZdoId || drop.m_itemData == null || drop.m_itemData.m_stack != sourceAmount || !TryGetExpiryTicks(drop.m_itemData, out var clockValue) || clockValue != sourceExpiryTicks) { return false; } return string.Equals(FoodIdentity.GetCanonicalPrefabName(drop.m_itemData), sourcePrefab, StringComparison.OrdinalIgnoreCase); } internal static bool ShouldInheritGroundSpawnTime(bool autoDestroy, bool isPiece) { if (autoDestroy) { return !isPiece; } return false; } private static long GetGroundSpawnTimeTicks(ItemDrop drop) { try { return HasValidGroundView(drop) ? drop.m_nview.GetZDO().GetLong(ZDOVars.s_spawnTime, 0L) : 0; } catch { return 0L; } } private static void InheritGroundSpawnTime(ItemDrop replacement, long sourceSpawnTimeTicks) { if (sourceSpawnTimeTicks > 0 && IsOwnedGroundDrop(replacement)) { replacement.m_nview.GetZDO().Set(ZDOVars.s_spawnTime, sourceSpawnTimeTicks); } } private static void DestroySpawnedGroundReplacement(ItemDrop? replacement) { if (!((Object)(object)replacement == (Object)null)) { if (IsOwnedGroundDrop(replacement)) { replacement.m_nview.Destroy(); } else if (HasValidGroundView(replacement)) { WarnReplacementOnce("ground-rollback-owner", "A spoilage replacement changed owner during rollback and could not be removed locally."); } } } internal static int CalculateReplacementAmount(int sourceAmount) { return Math.Max(0, sourceAmount); } private static int InsertReplacementStack(Inventory inventory, ItemData item, int amount, Vector2i requestedPosition) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (amount <= 0) { return 0; } item.m_stack = amount; int stack = item.m_stack; inventory.AddItem(item, amount, requestedPosition.x, requestedPosition.y); return Math.Max(0, stack - item.m_stack); } private static ItemData CreateReplacement(ItemData template, GameObject prefab, int sourceWorldLevel) { ItemData obj = template.Clone(); obj.m_dropPrefab = prefab; obj.m_worldLevel = sourceWorldLevel; obj.m_equipped = false; obj.m_customData.Remove("sighsorry.FineDining.ExpiryWorldTicks"); FreshnessRuntime.ClearTrackedMetadata(obj); return obj; } private static GameObject? ResolveItemPrefab(ObjectDB? objectDb, string prefabName) { if ((Object)(object)objectDb == (Object)null || string.IsNullOrWhiteSpace(prefabName)) { return null; } GameObject itemPrefab = objectDb.GetItemPrefab(prefabName); if ((Object)(object)itemPrefab != (Object)null && string.Equals(FoodIdentity.NormalizePrefabName(((Object)itemPrefab).name), prefabName, StringComparison.OrdinalIgnoreCase)) { return itemPrefab; } return objectDb.m_items?.FirstOrDefault((Func<GameObject, bool>)((GameObject prefab) => (Object)(object)prefab != (Object)null && string.Equals(FoodIdentity.NormalizePrefabName(((Object)prefab).name), prefabName, StringComparison.OrdinalIgnoreCase))); } private static bool IsObjectDatabaseReady(ObjectDB? objectDb) { if ((Object)(object)objectDb != (Object)null && objectDb.m_items != null) { return objectDb.m_items.Count > 0; } return false; } private static bool CanStackReplacement(ItemData candidate, ItemData template) { if (candidate.m_shared.m_name == template.m_shared.m_name && candidate.m_quality == template.m_quality && candidate.m_worldLevel == template.m_worldLevel && candidate.m_stack < candidate.m_shared.m_maxStackSize) { return CustomDataEqual(candidate.m_customData, template.m_customData); } return false; } private static bool CustomDataEqual(Dictionary<string, string> left, Dictionary<string, string> right) { int num = left.Count - CountStackMetadataKeys(left); int num2 = right.Count - CountStackMetadataKeys(right); string value; if (num == num2) { return left.All<KeyValuePair<string, string>>((KeyValuePair<string, string> pair) => IsStackMetadataKey(pair.Key) || (right.TryGetValue(pair.Key, out value) && value == pair.Value)); } return false; } private static int CountStackMetadataKeys(Dictionary<string, string> values) { return (values.ContainsKey("sighsorry.FineDining.ExpiryWorldTicks") ? 1 : 0) + (values.ContainsKey("sighsorry.FineDining.AssignedLifetimeTicks") ? 1 : 0); } private static bool IsStackMetadataKey(string key) { if (!(key == "sighsorry.FineDining.ExpiryWorldTicks")) { return key == "sighsorry.FineDining.AssignedLifetimeTicks"; } return true; } private static bool ApplyEarlierClock(ItemData target, long sourceClockValue, long nowTicks, bool destinationPaused) { if (!SpoilageClock.IsValidClockValue(sourceClockValue)) { return false; } long clockValue2; long clockValue = (TryGetExpiryTicks(target, out clockValue2) ? SpoilageClock.ComposeClockValues(clockValue2, sourceClockValue, nowTicks, destinationPaused) : ReencodeClockValue(sourceClockValue, nowTicks, destinationPaused)); return SetClockValue(target, clockValue); } private static bool ApplyEarlierRemaining(ItemData target, long sourceRemainingTicks, bool destinationPaused) { if (sourceRemainingTicks < 0) { return false; } long ticks; long nowTicks = (TryGetWorldTicks(out ticks) ? ticks : 0); long sourceClockValue = SpoilageClock.EncodeClockValue(nowTicks, sourceRemainingTicks, destinationPaused); return ApplyEarlierClock(target, sourceClockValue, nowTicks, destinationPaused); } private static long ReencodeClockValue(long clockValue, long nowTicks, bool paused) { if (!SpoilageClock.TryDecodeClockValue(clockValue, nowTicks, out var remainingTicks, out var _)) { return clockValue; } return SpoilageClock.EncodeClockValue(nowTicks, remainingTicks, paused && remainingTicks > 0); } private static bool SetClockValue(ItemData target, long clockValue) { if (!SpoilageClock.IsValidClockValue(clockValue)) { return false; } string text = clockValue.ToString(CultureInfo.InvariantCulture); if (target.m_customData.TryGetValue("sighsorry.FineDining.ExpiryWorldTicks", out var value) && value == text) { return false; } target.m_customData["sighsorry.FineDining.ExpiryWorldTicks"] = text; return true; } private static void WarnReplacementOnce(string key, string message) { if (LoggedReplacementWarnings.Add(key ?? "")) { FineDiningPlugin.Log.LogWarning((object)message); } } internal static bool TryGetWorldTicks(out long ticks) { ticks = 0L; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return false; } try { ticks = instance.GetTime().Ticks; return ticks > 0; } catch { return false; } } } internal static class ChefChoiceMath { internal const float MinimumAllowedMultiplier = 1f; internal const float MaximumTierSelectionStrength = 20f; internal static float ClampCookingFactor(float cookingFactor) { return ClampUnit(cookingFactor); } internal static float ClampNormalizedTier(float normalizedTier) { return ClampUnit(normalizedTier); } internal static float ClampPercentage(float percentage) { if (float.IsNaN(percentage) || percentage <= 0f) { return 0f; } if (!float.IsPositiveInfinity(percentage) && !(percentage >= 100f)) { return percentage; } return 100f; } internal static float BlendCategoryProbability(float baseProbability, float historyProbability, float percentage) { float num = ClampPercentage(percentage) / 100f; if (num <= 0f) { return baseProbability; } if (num >= 1f) { return historyProbability; } return baseProbability + (historyProbability - baseProbability) * num; } internal static float GetFoodSelectionWeight(float cookingFactor, float normalizedTier, bool tierResolved, float highTierSelectionStrength) { if (!tierResolved) { return 1f; } float num = ClampCookingFactor(cookingFactor); float num2 = ClampNormalizedTier(normalizedTier); return (float)Math.Exp(ClampTierSelectionStrength(highTierSelectionStrength) * num * num2); } internal static float GetInterpolatedMultiplierMode(float minimum, float maximum, float cookingLevelOneHundredMode, float cookingFactor) { minimum = ClampMultiplierMinimum(minimum); maximum = ClampMultiplierMaximum(maximum, minimum); float num = ClampMultiplierMode(cookingLevelOneHundredMode, minimum, maximum); return minimum + (num - minimum) * ClampCookingFactor(cookingFactor); } internal static float GetTriangularQuantile(float uniformSample, float normalizedMode) { double num = ClampUnit(uniformSample); double num2 = ClampUnit(normalizedMode); return ClampUnit((float)((num < num2) ? Math.Sqrt(num * num2) : (1.0 - Math.Sqrt((1.0 - num) * (1.0 - num2))))); } internal static float GetChefMultiplier(float minimum, float maximum, float uniformSample, float cookingFactor, float cookingLevelOneHundredMode) { minimum = ClampMultiplierMinimum(minimum); maximum = ClampMultiplierMaximum(maximum, minimum); if (minimum >= maximum) { return minimum; } double num = ((double)GetInterpolatedMultiplierMode(minimum, maximum, cookingLevelOneHundredMode, cookingFactor) - (double)minimum) / (double)(maximum - minimum); double num2 = GetTriangularQuantile(uniformSample, (float)num); return (float)((1.0 - num2) * (double)minimum + num2 * (double)maximum); } internal static int ChooseWeightedIndex(IReadOnlyList<float>? weights, float uniformSample) { if (weights == null || weights.Count == 0) { return -1; } double num = 0.0; int num2 = -1; for (int i = 0; i < weights.Count; i++) { float num3 = weights[i]; if (!float.IsNaN(num3) && !float.IsInfinity(num3) && !(num3 <= 0f)) { num += (double)num3; num2 = i; } } if (num2 < 0 || num <= 0.0 || double.IsInfinity(num)) { return -1; } double num4 = (double)ClampUnit(uniformSample) * num; double num5 = 0.0; for (int j = 0; j < weights.Count; j++) { float num6 = weights[j]; if (!float.IsNaN(num6) && !float.IsInfinity(num6) && !(num6 <= 0f)) { num5 += (double)num6; if (num4 < num5) { return j; } } } return num2; } private static float ClampUnit(float value) { if (float.IsNaN(value) || value <= 0f) { return 0f; } if (!(value >= 1f)) { return value; } return 1f; } internal static float ClampMultiplierMinimum(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(1f, value); } return 1f; } internal static float ClampMultiplierMaximum(float value, float minimum) { minimum = ClampMultiplierMinimum(minimum); if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(minimum, value); } return minimum; } internal static float ClampMultiplierMode(float value, float minimum, float maximum) { minimum = ClampMultiplierMinimum(minimum); maximum = ClampMultiplierMaximum(maximum, minimum); if (float.IsNaN(value) || value <= minimum) { return minimum; } if (!float.IsPositiveInfinity(value) && !(value >= maximum)) { return value; } return maximum; } private static float ClampTierSelectionStrength(float value) { if (float.IsNaN(value) || value <= 0f) { return 0f; } if (!float.IsPositiveInfinity(value) && !(value >= 20f)) { return value; } return 20f; } } internal static class ChefCollectionService { private sealed class ChefCandidate { internal string Key { get; } internal int AxisIndex { get; } private ChefFoodTierInfo? TierInfo { get; } internal ChefCandidate(string key, ChefFoodTierInfo? tierInfo, FoodStatAxis axis) { Key = key; TierInfo = tierInfo; AxisIndex = GetFoodStatAxisIndex(axis); } internal float GetSelectionWeight(float cookingFactor) { return ChefChoiceMath.GetFoodSelectionWeight(cookingFactor, TierInfo?.NormalizedTier ?? 0f, TierInfo?.IsResolved ?? false, DietConfig.GetChefHighTierSelectionStrength()); } } private readonly struct RecentFoodComposition { private readonly int _health; private readonly int _stamina; private readonly int _eitr; private readonly int _total; internal RecentFoodComposition(int health, int stamina, int eitr) { _health = Math.Max(0, health); _stamina = Math.Max(0, stamina); _eitr = Math.Max(0, eitr); _total = _health + _stamina + _eitr; } internal float GetShare(int axisIndex) { if (_total <= 0) { return 0f; } return (float)(axisIndex switch { 0 => _health, 1 => _stamina, 2 => _eitr, _ => 0, }) / (float)_total; } } private static readonly object RandomLock = new object(); private static readonly Random RandomSource = new Random(); private const int FoodStatAxisCount = 3; internal static bool EnsureChefCollection(Player? player, PlayerFoodStateData state) { return EnsureChefCollection(player, state, null); } private static bool EnsureChefCollection(Player? player, PlayerFoodStateData state, string? excludedRefillKey) { if ((Object)(object)player == (Object)null || (Object)(object)ObjectDB.instance == (Object)null) { return false; } ChefFoodTierCatalog.Tick(); if (!ChefFoodTierCatalog.IsReady) { return false; } Dictionary<string, FoodStatAxis> foodAxesByKey = new Dictionary<string, FoodStatAxis>(StringComparer.Ordinal); List<ChefCandidate> knownFoodCandidates = GetKnownFoodCandidates(player, foodAxesByKey); RecentFoodComposition recentFoodComposition = GetRecentFoodComposition(state, foodAxesByKey); HashSet<string> eligibleSet = new HashSet<string>(StringComparer.Ordinal); foreach (ChefCandidate item in knownFoodCandidates) { eligibleSet.Add(item.Key); } bool flag = false; int count = state.Chef.Count; state.Chef.RemoveAll((ChefEntryData entry) => entry == null || string.IsNullOrWhiteSpace(entry.Key) || !eligibleSet.Contains(entry.Key)); flag |= state.Chef.Count != count; HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int num = state.Chef.Count - 1; num >= 0; num--) { if (!hashSet.Add(state.Chef[num].Key)) { state.Chef.RemoveAt(num); flag = true; } } int num2 = Mathf.Min(DietConfig.GetChefCollectionSize(), knownFoodCandidates.Count); float cookingFactor = ChefChoiceMath.ClampCookingFactor(((Character)player).GetSkillFactor((SkillType)105)); ChefCandidate selected; while (state.Chef.Count < num2 && (TrySelectWeightedCandidate(knownFoodCandidates, hashSet, excludedRefillKey, cookingFactor, recentFoodComposition, out selected) || (!string.IsNullOrWhiteSpace(excludedRefillKey) && TrySelectWeightedCandidate(knownFoodCandidates, hashSet, null, cookingFactor, recentFoodComposition, out selected)))) { state.Chef.Add(new ChefEntryData { Key = selected.Key, Multiplier = RollChefMultiplier(cookingFactor) }); hashSet.Add(selected.Key); flag = true; } while (state.Chef.Count > num2) { state.Chef.RemoveAt(state.Chef.Count - 1); flag = true; } return flag; } internal static bool TryConsumeChefEntry(Player player, PlayerFoodStateData state, string key, out float multiplier) { EnsureChefCollection(player, state); for (int i = 0; i < state.Chef.Count; i++) { ChefEntryData chefEntryData = state.Chef[i]; if (!(chefEntryData.Key != key)) { multiplier = chefEntryData.Multiplier; state.Chef.RemoveAt(i); return true; } } multiplier = 1f; return false; } internal static bool RefillAfterConsumption(Player player, PlayerFoodStateData state, string consumedKey) { return EnsureChefCollection(player, state, consumedKey); } internal static ChefEntryData? GetEntry(PlayerFoodStateData state, string key) { if (SpoilagePolicy.IsChefChoiceBlacklisted(key)) { return null; } foreach (ChefEntryData item in state.Chef) { if (item.Key == key) { return item; } } return null; } internal static void RerollAll(Player? player) { if (!((Object)(object)player == (Object)null) && !((Object)(object)ObjectDB.instance == (Object)null)) { PlayerFoodStateData state = FoodStateStore.GetState(player); state.Chef.Clear(); EnsureChefCollection(player, state); FoodStateStore.SaveState(player, state); } } internal static void RotateOldest(Player? player, int count) { if ((Object)(object)player == (Object)null || (Object)(object)ObjectDB.instance == (Object)null || count <= 0) { return; } PlayerFoodStateData state = FoodStateStore.GetState(player); bool flag = EnsureChefCollection(player, state); for (int i = 0; i < count; i++) { if (state.Chef.Count <= 0) { break; } string key = state.Chef[0].Key; state.Chef.RemoveAt(0); flag = true; flag |= EnsureChefCollection(player, state, key); } if (flag) { FoodStateStore.SaveState(player, state); } } private static bool TrySelectWeightedCandidate(IReadOnlyList<ChefCandidate> eligible, ISet<string> currentKeys, string? excludedKey, float cookingFactor, RecentFoodComposition recentFoodComposition, out ChefCandidate selected) { List<ChefCandidate> list = new List<ChefCandidate>(); List<float> list2 = new List<float>(); float[] array = new float[3]; foreach (ChefCandidate item in eligible) { if (!currentKeys.Contains(item.Key) && !item.Key.Equals(excludedKey, StringComparison.Ordinal)) { list.Add(item); float selectionWeight = item.GetSelectionWeight(cookingFactor); list2.Add(selectionWeight); array[item.AxisIndex] += selectionWeight; } } if (list.Count == 0) { selected = null; return false; } float num = 0f; float num2 = 0f; for (int i = 0; i < 3; i++) { if (!(array[i] <= 0f)) { num += array[i]; num2 += recentFoodComposition.GetShare(i); } } List<float> list3 = new List<float>(list.Count); float percentage = ((num2 > 0f) ? DietConfig.GetChefRecentFoodPreferencePercent() : 0f); for (int j = 0; j < list.Count; j++) { ChefCandidate chefCandidate = list[j]; float num3 = array[chefCandidate.AxisIndex]; float num4 = ((num > 0f) ? (num3 / num) : 0f); float historyProbability = ((num2 > 0f) ? (recentFoodComposition.GetShare(chefCandidate.AxisIndex) / num2) : num4); float num5 = ChefChoiceMath.BlendCategoryProbability(num4, historyProbability, percentage); list3.Add((num3 > 0f) ? (num5 * list2[j] / num3) : 0f); } float uniformSample; lock (RandomLock) { uniformSample = (float)RandomSource.NextDouble(); } int num6 = ChefChoiceMath.ChooseWeightedIndex(list3, uniformSample); if (num6 < 0) { selected = null; return false; } selected = list[num6]; return true; } private static List<ChefCandidate> GetKnownFoodCandidates(Player player, IDictionary<string, FoodStatAxis> foodAxesByKey) { List<ChefCandidate> list = new List<ChefCandidate>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (ChefFoodTierInfo item in ChefFoodTierCatalog.GetSnapshot()) { string prefabName = item.PrefabName; if (!string.IsNullOrWhiteSpace(prefabName) && item.Axis != FoodStatAxis.None) { foodAxesByKey[prefabName] = item.Axis; if ((player.IsRecipeKnown(item.ItemNameToken) || player.IsKnownMaterial(item.ItemNameToken)) && !SpoilagePolicy.IsChefChoiceBlacklisted(prefabName) && hashSet.Add(prefabName)) { list.Add(new ChefCandidate(prefabName, item, item.Axis)); } } } list.Sort((ChefCandidate left, ChefCandidate right) => StringComparer.Ordinal.Compare(left.Key, right.Key)); return list; } private static RecentFoodComposition GetRecentFoodComposition(PlayerFoodStateData state, IReadOnlyDictionary<string, FoodStatAxis> foodAxesByKey) { int num = 0; int num2 = 0; int num3 = 0; foreach (HistoryEntryData item in state.Recent) { if (item != null && foodAxesByKey.TryGetValue(item.Key, out var value)) { switch (value) { case FoodStatAxis.Health: num++; break; case FoodStatAxis.Stamina: num2++; break; case FoodStatAxis.Eitr: num3++; break; } } } return new RecentFoodComposition(num, num2, num3); } private static float RollChefMultiplier(float cookingFactor) { float uniformSample; lock (RandomLock) { uniformSample = (float)RandomSource.NextDouble(); } return ChefChoiceMath.GetChefMultiplier(DietConfig.GetChefMultiplierMin(), DietConfig.GetChefMultiplierMax(), uniformSample, cookingFactor, DietConfig.GetChefMultiplierModeAtMaxCooking()); } private static int GetFoodStatAxisIndex(FoodStatAxis axis) { return axis switch { FoodStatAxis.Health => 0, FoodStatAxis.Stamina => 1, FoodStatAxis.Eitr => 2, _ => throw new ArgumentOutOfRangeException("axis", axis, "Chef food must have a classified food-stat axis."), }; } } internal sealed class ChefFoodTierInfo { internal string PrefabName { get; } internal int Tier { get; } internal int TierCount { get; } internal string TierName { get; } internal string FallbackReason { get; } internal IReadOnlyList<string> Sources { get; } internal string ItemNameToken { get; private set; } = ""; internal FoodStatAxis Axis { get; private set; } internal bool IsResolved => Tier >= 0; internal bool IsAllTier => !IsResolved; internal float NormalizedTier { get { if (IsResolved && TierCount > 1) { return Mathf.Clamp01((float)Tier / (float)(TierCount - 1)); } return 0f; } } internal ChefFoodTierInfo(string prefabName, int tier, int ti