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 FoodKnowledge v1.0.3
BepInEx/plugins/dannanboi_FoodKnowledge.dll
Decompiled a day 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.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using ModifiersFramework; using ModifiersFramework.Compatibility; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Food Knowledge")] [assembly: AssemblyDescription("YAML-defined Valheim foods, native recipe edits and temporary modifiers")] [assembly: AssemblyCompany("dannanboi")] [assembly: AssemblyProduct("Food Knowledge")] [assembly: AssemblyFileVersion("1.0.3")] [assembly: AssemblyInformationalVersion("1.0.3")] [assembly: AssemblyVersion("1.0.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FoodKnowledge { internal sealed class Ingredient { internal string Item; internal int Amount; } internal sealed class FoodDefinition { internal string Item; internal string UseModel; internal string DisplayName; internal string Description; internal string EffectInfo; internal string NamedBuff; internal BuffOptions Presentation; internal float Hp; internal float HpPerTick; internal float Stamina; internal float Eitr; internal float DurationMinutes; internal float WeightPerUnit; internal int Amount; internal int StackSize; internal int StationLevel; internal string[] Stations; internal Ingredient[] Ingredients; internal ModifierSet ModifierSet = ModifierSet.Empty; internal string Location; internal string NativeBaseline; internal bool Synchronized; internal bool UsesNamedBuff; internal readonly Dictionary<string, string> Fields = new Dictionary<string, string>(StringComparer.Ordinal); internal bool NameOnly => Fields.Count == 1; internal bool Has(string key) { return Fields.ContainsKey(key); } internal bool Overrides(string key) { string value; return Fields.TryGetValue(key, out value) && value != null; } } internal sealed class Catalog { internal const int MaxBytes = 1048576; internal const int MaxFoods = 512; internal readonly List<FoodDefinition> Foods = new List<FoodDefinition>(); internal string Canonical; internal const string FoodComments = "# Food Knowledge\n# See ModifiersFramework documentation on how to add modifiers and buffs.\n# The top-level key is the food's console name; do not add an itemName field.\n# A native food name with an empty value removes its crafting recipes; additional fields edit it.\n# Native edits inherit omitted values. New food names still require ingredients and use independent defaults.\n# Indent section headings by 2 spaces and their contents by 4.\n# In ingredients, use one '- Prefab[amount]' list row per ingredient.\n# Each food starts with PrefabName: (the colon is required), followed by basics, stats and ingredients.\n# Multiple .yaml/.yml files are supported. Server YAML controls multiplayer.\n# Leave optional values empty to use the mod's defaults.\n"; internal static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; private static readonly Regex PrefabName = new Regex("^[A-Za-z_][A-Za-z0-9_\\-]*$", RegexOptions.CultureInvariant); private static readonly Regex IngredientPattern = new Regex("^([A-Za-z_][A-Za-z0-9_\\-]*)\\s*\\[\\s*([0-9]+)\\s*\\]$", RegexOptions.CultureInvariant); private static readonly Regex NamedBuffStart = new Regex("^\\s*buff\\s*\\[", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex ModifierEntry = new Regex("^\\s*([A-Za-z_][A-Za-z0-9_\\-]*)\\s*\\[([^\\[\\]]*)\\]\\s*$", RegexOptions.CultureInvariant); private static readonly HashSet<string> Fields = new HashSet<string>(new string[17] { "itemName", "displayName", "description", "useModel", "craftingStation", "stationLevel", "hpGain", "hpPerTick", "staminaGain", "eitrGain", "modifierGain", "activeEffectText", "stackSize", "weightPerUnit", "durationMinutes", "amountProduced", "ingredientList" }, StringComparer.Ordinal); private static readonly HashSet<string> BasicFields = new HashSet<string>(new string[9] { "itemName", "displayName", "description", "useModel", "craftingStation", "stationLevel", "stackSize", "weightPerUnit", "amountProduced" }, StringComparer.Ordinal); internal static Catalog Parse(IEnumerable<KeyValuePair<string, string>> files) { return Parse(files, synchronized: false); } private static Catalog Parse(IEnumerable<KeyValuePair<string, string>> files, bool synchronized) { Catalog catalog = new Catalog(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); HashSet<int> hashSet2 = new HashSet<int>(); foreach (KeyValuePair<string, string> item in files.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> x) => x.Key, StringComparer.Ordinal)) { foreach (FlatYaml.Row item2 in FlatYaml.Read(item.Value, item.Key)) { FoodDefinition foodDefinition = ParseRow(item2, synchronized); if (!hashSet.Add(foodDefinition.Item)) { throw item2.Error("Duplicate item '" + foodDefinition.Item + "'."); } if (!hashSet2.Add(StableHash(foodDefinition.Item))) { throw item2.Error("Item prefab hash collision for '" + foodDefinition.Item + "'."); } catalog.Foods.Add(foodDefinition); if (catalog.Foods.Count > 512) { throw item2.Error("At most " + 512 + " foods are supported."); } } } catalog.Foods.Sort((FoodDefinition a, FoodDefinition b) => StringComparer.Ordinal.Compare(a.Item, b.Item)); catalog.Canonical = Serialize(catalog.Foods); if (Encoding.UTF8.GetByteCount(catalog.Canonical) > 1048576) { throw new FormatException("Food definitions exceed 1 MiB."); } return catalog; } internal static Catalog Parse(string yaml) { return Parse(new KeyValuePair<string, string>[1] { new KeyValuePair<string, string>("server YAML", yaml) }); } internal static Catalog ParseSynchronized(string yaml) { return Parse(new KeyValuePair<string, string>[1] { new KeyValuePair<string, string>("server YAML", yaml) }, synchronized: true); } internal static Catalog Empty() { return Parse("[]\n"); } internal static string WithFoodComments(string yaml) { Match match = Regex.Match(yaml, "\\A(?:[ \\t]*#[^\\r\\n]*(?:\\r\\n|\\r|\\n|$)|[ \\t]*(?:\\r\\n|\\r|\\n))*", RegexOptions.CultureInvariant); return "# Food Knowledge\n# See ModifiersFramework documentation on how to add modifiers and buffs.\n# The top-level key is the food's console name; do not add an itemName field.\n# A native food name with an empty value removes its crafting recipes; additional fields edit it.\n# Native edits inherit omitted values. New food names still require ingredients and use independent defaults.\n# Indent section headings by 2 spaces and their contents by 4.\n# In ingredients, use one '- Prefab[amount]' list row per ingredient.\n# Each food starts with PrefabName: (the colon is required), followed by basics, stats and ingredients.\n# Multiple .yaml/.yml files are supported. Server YAML controls multiplayer.\n# Leave optional values empty to use the mod's defaults.\n" + yaml.Substring(match.Length); } internal static string StarterYaml() { return "# Food Knowledge\n# See ModifiersFramework documentation on how to add modifiers and buffs.\n# The top-level key is the food's console name; do not add an itemName field.\n# A native food name with an empty value removes its crafting recipes; additional fields edit it.\n# Native edits inherit omitted values. New food names still require ingredients and use independent defaults.\n# Indent section headings by 2 spaces and their contents by 4.\n# In ingredients, use one '- Prefab[amount]' list row per ingredient.\n# Each food starts with PrefabName: (the colon is required), followed by basics, stats and ingredients.\n# Multiple .yaml/.yml files are supported. Server YAML controls multiplayer.\n# Leave optional values empty to use the mod's defaults.\n{}\n"; } internal static bool IsIdentity(string key) { return key == "itemName"; } internal static bool IsSection(string key) { return key == "basics" || key == "stats" || key == "ingredients"; } internal static bool FieldInSection(string section, string key) { return (section == "basics") ? BasicFields.Contains(key) : (section == "stats" && (key == "modifierValues" || (Fields.Contains(key) && key != "ingredientList" && !BasicFields.Contains(key)))); } internal static string NestedIngredient(FlatYaml.Row row, string item, string value) { CheckPrefab(row, item, "ingredient prefab"); if (!int.TryParse((value ?? "").Trim(), NumberStyles.None, Invariant, out var result) || result < 1 || result > 1000000) { throw row.Error("Ingredient '" + item + "' needs a whole-number amount between 1 and 1000000."); } return item + "[" + result.ToString(Invariant) + "]"; } internal static string BracketIngredient(FlatYaml.Row row, string value) { Match match = IngredientPattern.Match((value ?? "").Trim()); if (!match.Success) { throw row.Error("Invalid ingredient '" + value + "'. Use one 'Prefab[positive amount]' row, for example 'Fish1[5]'; a leading dash is optional."); } return NestedIngredient(row, match.Groups[1].Value, match.Groups[2].Value); } private static FoodDefinition ParseRow(FlatYaml.Row row, bool synchronized) { //IL_06fc: Unknown result type (might be due to invalid IL or missing references) //IL_0706: Expected O, but got Unknown //IL_067b: Unknown result type (might be due to invalid IL or missing references) //IL_0768: Unknown result type (might be due to invalid IL or missing references) //IL_0772: Expected O, but got Unknown foreach (string key in row.Values.Keys) { if (!Fields.Contains(key) && (!synchronized || (!(key == "_namedBuff") && !(key == "_nativeBaseline")))) { throw row.Error("Unknown field '" + key + "'. Field names are case-sensitive."); } } FoodDefinition foodDefinition = new FoodDefinition { Item = Required(row, "itemName"), UseModel = Value(row, "useModel"), DisplayName = Value(row, "displayName"), Description = Value(row, "description"), EffectInfo = Value(row, "activeEffectText"), Hp = Number(row, "hpGain", 0f, 100000f).GetValueOrDefault(), Stamina = Number(row, "staminaGain", 0f, 100000f).GetValueOrDefault(), Eitr = Number(row, "eitrGain", 0f, 100000f).GetValueOrDefault(), DurationMinutes = (Number(row, "durationMinutes", 0.0001f, 100000f) ?? 20f), WeightPerUnit = (Number(row, "weightPerUnit", 0f, 100000f) ?? 0.1f), StackSize = (Integer(row, "stackSize", required: false) ?? 20), Amount = (Integer(row, "amountProduced", required: false) ?? 1), StationLevel = (string.IsNullOrWhiteSpace(Value(row, "stationLevel")) ? 1 : Integer(row, "stationLevel", required: true).Value), Stations = Split(Value(row, "craftingStation")), Location = row.Location }; foodDefinition.Synchronized = synchronized; foodDefinition.NativeBaseline = Value(row, "_nativeBaseline"); if (foodDefinition.NativeBaseline != null && (foodDefinition.NativeBaseline.Length != 44 || !Regex.IsMatch(foodDefinition.NativeBaseline, "^[A-Za-z0-9+/]{43}=$", RegexOptions.CultureInvariant))) { throw row.Error("Invalid native food baseline fingerprint."); } foreach (KeyValuePair<string, string> value2 in row.Values) { if (value2.Key != "_namedBuff" && value2.Key != "_nativeBaseline") { foodDefinition.Fields.Add(value2.Key, value2.Value); } } foodDefinition.HpPerTick = Number(row, "hpPerTick", 0f, 100000f) ?? ((foodDefinition.Hp > 0f) ? 2f : 0f); if (string.IsNullOrWhiteSpace(foodDefinition.EffectInfo)) { foodDefinition.EffectInfo = null; } CheckPrefab(row, foodDefinition.Item, "itemName"); if (string.IsNullOrWhiteSpace(foodDefinition.UseModel) || !PrefabName.IsMatch(foodDefinition.UseModel) || foodDefinition.Item == foodDefinition.UseModel) { foodDefinition.UseModel = "RawMeat"; } if (foodDefinition.StationLevel > 1000) { throw row.Error("stationLevel must be between 1 and 1000."); } if (foodDefinition.Stations.Length > 32) { throw row.Error("At most 32 crafting stations can be listed per food."); } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string[] stations = foodDefinition.Stations; foreach (string text in stations) { CheckPrefab(row, text, "craftingStation"); if (!hashSet.Add(text)) { throw row.Error("Duplicate crafting station '" + text + "'."); } } if (foodDefinition.DisplayName != null && foodDefinition.DisplayName.Length > 512) { throw row.Error("displayName exceeds 512 characters."); } if (foodDefinition.Description != null && foodDefinition.Description.Length > 16384) { throw row.Error("description exceeds 16384 characters."); } string value = Value(row, "ingredientList"); if (!string.IsNullOrWhiteSpace(value)) { List<Ingredient> list = new List<Ingredient>(); HashSet<string> hashSet2 = new HashSet<string>(StringComparer.Ordinal); string[] array = Split(value); foreach (string text2 in array) { Match match = IngredientPattern.Match(text2); if (!match.Success || !int.TryParse(match.Groups[2].Value, NumberStyles.None, Invariant, out var result) || result < 1 || result > 1000000) { throw row.Error("Invalid ingredient '" + text2 + "'. Use Prefab[positive amount], for example Fish1[5]."); } if (!hashSet2.Add(match.Groups[1].Value)) { throw row.Error("Duplicate ingredient '" + match.Groups[1].Value + "'."); } list.Add(new Ingredient { Item = match.Groups[1].Value, Amount = result }); } if (list.Count == 0 || list.Count > 32) { throw row.Error("ingredientList needs between 1 and 32 ingredients."); } foodDefinition.Ingredients = list.ToArray(); } try { ModifierSet val = FoodModifiers(row, Value(row, "modifierGain"), out foodDefinition.UsesNamedBuff); foodDefinition.NamedBuff = val.CustomBuffName; foodDefinition.ModifierSet = (ModifierSet)((val.CustomBuffName == null) ? ((object)val) : ((object)new ModifierSet((IEnumerable<SkillBonus>)val.Skills, (IEnumerable<EffectBonus>)val.Effects, (IEnumerable<AddedDamageBonus>)val.AddedDamage))); if (synchronized) { if (foodDefinition.NamedBuff != null) { throw row.Error("Server catalog must contain expanded named effects."); } foodDefinition.NamedBuff = Value(row, "_namedBuff"); } if (foodDefinition.NamedBuff != null) { foodDefinition.EffectInfo = null; try { foodDefinition.Presentation = FrameworkApi.GetCustomBuff(foodDefinition.NamedBuff, true, true, true); } catch (FormatException ex) { foodDefinition.Presentation = new BuffOptions(foodDefinition.NamedBuff, (Sprite)null, (string)null, true, true, true); Plugin.Warn(row.Location + ": Named buff presentation is unavailable; using its calling name. " + ex.Message); } } else { if (Encoding.UTF8.GetByteCount(foodDefinition.EffectInfo ?? "") > 4096) { throw row.Error("activeEffectText exceeds 4096 UTF-8 bytes."); } foodDefinition.Presentation = new BuffOptions((string)null, (Sprite)null, foodDefinition.EffectInfo, true, true, true); } } catch (FormatException ex2) { throw row.Error(ex2.Message); } return foodDefinition; } private static ModifierSet FoodModifiers(FlatYaml.Row row, string value, out bool usesNamedBuff) { usesNamedBuff = false; if (Encoding.UTF8.GetByteCount(value ?? "") > 16384) { return FrameworkNames.Parse(value); } if (value != null && value.IndexOf("buff", StringComparison.OrdinalIgnoreCase) >= 0) { ModifierSet val = null; string[] array = value.Split(','); foreach (string text in array) { if (!NamedBuffStart.IsMatch(text)) { continue; } usesNamedBuff = true; ModifierSet val2; try { val2 = FrameworkNames.Parse(text.Trim()); } catch (FormatException ex) { if (!ModifierEntry.IsMatch(text)) { throw; } Plugin.Warn(row.Location + ": Ignoring unavailable named buff '" + text.Trim() + "'. " + ex.Message); continue; } if (val != null) { throw new FormatException("Use one buff[CallingName] per food; other modifierGain entries are ignored."); } val = val2; } if (usesNamedBuff) { return val ?? ModifierSet.Empty; } } try { return FrameworkNames.Parse(value); } catch (FormatException) { StringBuilder stringBuilder = new StringBuilder(); string[] array2 = value.Split(','); string text3 = default(string); foreach (string text2 in array2) { Match match = ModifierEntry.Match(text2); if (!match.Success) { throw; } if (!FrameworkApi.TryGetModifierFullName(match.Groups[1].Value, ref text3)) { Plugin.Warn(row.Location + ": Ignoring unknown modifier '" + match.Groups[1].Value + "'."); continue; } if (stringBuilder.Length > 0) { stringBuilder.Append(','); } stringBuilder.Append(text2); } return FrameworkNames.Parse(stringBuilder.ToString()); } } private static string Value(FlatYaml.Row row, string key) { string value; return row.Values.TryGetValue(key, out value) ? value : null; } private static string Required(FlatYaml.Row row, string key) { string text = Value(row, key); if (string.IsNullOrWhiteSpace(text)) { throw row.Error("Required field '" + key + "' is missing or empty."); } return text.Trim(); } private static void CheckPrefab(FlatYaml.Row row, string value, string field) { if (value.Length > 128 || !PrefabName.IsMatch(value)) { throw row.Error("Invalid prefab name in " + field + ": '" + value + "'."); } } private static int? Integer(FlatYaml.Row row, string key, bool required) { string text = Value(row, key); if (text == null && !required) { return null; } if (text == null || !int.TryParse(text, NumberStyles.Integer, Invariant, out var result) || result < 1 || result > 1000000) { throw row.Error(key + " must be a positive whole number."); } return result; } private static float? Number(FlatYaml.Row row, string key, float min, float max) { string text = Value(row, key); return (text == null) ? ((float?)null) : new float?(ParseNumber(text, min, max, row, key)); } private static float ParseNumber(string text, float min, float max, FlatYaml.Row row, string key) { if (!float.TryParse(text, NumberStyles.Float, Invariant, out var result) || float.IsNaN(result) || float.IsInfinity(result) || result < min || result > max) { throw row.Error(key + " must be between " + min.ToString(Invariant) + " and " + max.ToString(Invariant) + ", using a decimal point."); } return result; } private static string[] Split(string value) { if (string.IsNullOrWhiteSpace(value)) { return Array.Empty<string>(); } string[] array = (from x in value.Split(',') select x.Trim()).ToArray(); if (array.Any((string x) => x.Length == 0)) { throw new FormatException("Comma-separated lists cannot contain empty entries."); } return array; } internal static int StableHash(string value) { int num = 5381; int num2 = 5381; for (int i = 0; i < value.Length && value[i] != 0; i += 2) { num = ((num << 5) + num) ^ value[i]; if (i == value.Length - 1 || value[i + 1] == '\0') { break; } num2 = ((num2 << 5) + num2) ^ value[i + 1]; } return num + num2 * 1566083941; } internal static string Quote(string value) { StringBuilder stringBuilder = new StringBuilder("\""); foreach (char c in value) { if (c == '"' || c == '\\') { stringBuilder.Append('\\').Append(c); continue; } switch (c) { case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (char.IsControl(c)) { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4", Invariant)); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } internal static string SerializeSynchronized(IEnumerable<FoodDefinition> definitions, Dictionary<string, string> baselines) { string text = Serialize(definitions, baselines); if (Encoding.UTF8.GetByteCount(text) > 1048576) { throw new FormatException("Synchronized food definitions exceed 1 MiB."); } return text; } private static string Serialize(IEnumerable<FoodDefinition> definitions, Dictionary<string, string> baselines = null) { StringBuilder stringBuilder = new StringBuilder(); foreach (FoodDefinition definition in definitions) { stringBuilder.Append("- itemName: ").Append(Quote(definition.Item)).Append('\n'); string text = definition.NativeBaseline; if (baselines != null && baselines.TryGetValue(definition.Item, out var value)) { text = value; } if (text != null) { Write(stringBuilder, "_nativeBaseline", text); } WriteField(stringBuilder, definition, "displayName", definition.DisplayName); WriteField(stringBuilder, definition, "description", definition.Description); WriteField(stringBuilder, definition, "useModel", definition.Fields.TryGetValue("useModel", out var value2) ? value2 : null); WriteField(stringBuilder, definition, "craftingStation", string.Join(",", definition.Stations)); WriteField(stringBuilder, definition, "stationLevel", definition.StationLevel); WriteField(stringBuilder, definition, "hpGain", definition.Hp); WriteField(stringBuilder, definition, "hpPerTick", definition.HpPerTick); WriteField(stringBuilder, definition, "staminaGain", definition.Stamina); WriteField(stringBuilder, definition, "eitrGain", definition.Eitr); WriteField(stringBuilder, definition, "modifierGain", definition.ModifierSet.Canonical); if (definition.NamedBuff != null) { Write(stringBuilder, "_namedBuff", definition.NamedBuff); } if (definition.NamedBuff == null) { WriteField(stringBuilder, definition, "activeEffectText", definition.Fields.TryGetValue("activeEffectText", out var value3) ? value3 : null); } WriteField(stringBuilder, definition, "stackSize", definition.StackSize); WriteField(stringBuilder, definition, "weightPerUnit", definition.WeightPerUnit); WriteField(stringBuilder, definition, "durationMinutes", definition.DurationMinutes); WriteField(stringBuilder, definition, "amountProduced", definition.Amount); if (definition.Ingredients != null) { WritePlain(stringBuilder, "ingredientList", string.Join(",", definition.Ingredients.Select((Ingredient x) => x.Item + "[" + x.Amount.ToString(Invariant) + "]"))); } else if (definition.Has("ingredientList")) { stringBuilder.Append(" ingredientList:\n"); } } return (stringBuilder.Length == 0) ? "[]\n" : stringBuilder.ToString(); } private static void WriteField(StringBuilder b, FoodDefinition f, string key, object value) { if (!f.Has(key)) { return; } if (!f.Overrides(key)) { b.Append(" ").Append(key).Append(":\n"); return; } string text = ((value is float num) ? num.ToString("R", Invariant) : Convert.ToString(value, Invariant)); if (value is float || value is int || key == "craftingStation" || key == "modifierGain") { WritePlain(b, key, text); } else { Write(b, key, text ?? ""); } } private static void Write(StringBuilder b, string field, string value) { if (value != null) { b.Append(" ").Append(field).Append(": ") .Append(Quote(value)) .Append('\n'); } } private static void WritePlain(StringBuilder b, string field, string value) { b.Append(" ").Append(field).Append(':'); if (!string.IsNullOrEmpty(value)) { b.Append(' ').Append(value); } b.Append('\n'); } private static void Write(StringBuilder b, string field, float? value) { if (value.HasValue) { b.Append(" ").Append(field).Append(": ") .Append(value.Value.ToString("R", Invariant)) .Append('\n'); } } } internal static class FlatYaml { internal sealed class Row { internal string Location; internal readonly Dictionary<string, string> Values = new Dictionary<string, string>(StringComparer.Ordinal); internal readonly HashSet<string> Sections = new HashSet<string>(StringComparer.Ordinal); internal List<string> Ingredients; internal bool BracketIngredients; internal bool ItemFirst; internal bool InlineValue; internal readonly HashSet<string> RootFields = new HashSet<string>(StringComparer.Ordinal); internal FormatException Error(string message) { return new FormatException(Location + ": " + message); } } internal static IEnumerable<Row> Read(string text, string filename) { if (text == null || Encoding.UTF8.GetByteCount(text) > 1048576) { throw new FormatException(filename + ": YAML exceeds 1 MiB."); } string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').TrimStart('\ufeff') .Split('\n'); List<Row> list = new List<Row>(); Row row = null; int num = -1; bool flag = false; bool flag2 = false; bool? flag3 = null; string text2 = null; int num2 = -1; bool? flag4 = null; for (int i = 0; i < array.Length; i++) { string text3 = array[i]; int num3 = Indent(text3, filename, i); string text4 = text3.Trim(); if (text4.Length == 0 || text4.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text4 == "---" && !flag2 && num3 == 0) { flag2 = true; continue; } if (text4 == "..." && num3 == 0) { flag = true; continue; } if (flag) { throw new FormatException(filename + ":" + (i + 1) + ": Multiple YAML documents are not supported."); } if ((text4 == "[]" || text4 == "{}") && list.Count == 0 && num3 == 0) { flag = true; continue; } bool flag5 = num3 == 0 && text3.StartsWith("- ", StringComparison.Ordinal); if (num3 == 0 && !flag5) { if (flag3 == false) { throw new FormatException(filename + ":" + (i + 1) + ": Do not mix food-name mappings and the earlier itemName list in one file."); } row = new Row { Location = filename + ":" + (i + 1), ItemFirst = true }; int num4 = text3.IndexOf(':'); if (num4 <= 0 || (num4 + 1 < text3.Length && !char.IsWhiteSpace(text3[num4 + 1]))) { throw row.Error("Start each food with 'PrefabName:'; the colon is required."); } string value = Scalar(text3.Substring(0, num4).Trim(), row); string text5 = Uncomment(text3.Substring(num4 + 1)).Trim(); if (text5.Length != 0 && text5 != "{}" && Scalar(text5, row) != null) { throw row.Error("A food-name key must contain indented fields or an empty value, not an inline scalar."); } row.Values.Add("itemName", value); row.RootFields.Add("itemName"); row.InlineValue = text5.Length != 0; list.Add(row); flag3 = true; num = 2; text2 = null; num2 = -1; flag4 = null; flag2 = true; continue; } bool flag6 = false; if (flag5) { if (flag3 == true) { throw new FormatException(filename + ":" + (i + 1) + ": Do not mix food-name mappings and the earlier itemName list in one file."); } flag3 = false; row = new Row { Location = filename + ":" + (i + 1) }; list.Add(row); text3 = text3.Substring(2); num = -1; text2 = null; num2 = -1; flag4 = null; } else { if (row == null || num3 == 0) { throw new FormatException(filename + ":" + (i + 1) + ": Start each food with 'PrefabName:'. Earlier '- itemName: PrefabName' lists remain supported."); } if (row.InlineValue) { throw row.Error("An inline empty value cannot also have indented fields. Leave the food-name value empty before its sections."); } if (num < 0) { num = num3; } if (text2 != null && num3 > num) { if (num2 < 0) { num2 = num3; } if (num3 != num2) { throw row.Error("Inconsistent section indentation at line " + (i + 1) + "."); } flag6 = true; } else { if (num3 != num && (!row.ItemFirst || text2 != null || num != 4 || num3 != 2 || !IsSectionHeading(text3, num3))) { throw row.Error("Inconsistent field indentation at line " + (i + 1) + "."); } text2 = null; num2 = -1; flag4 = null; } text3 = text3.Substring(num3); } string text6 = ((flag6 && text2 == "ingredients") ? Uncomment(text3).Trim() : null); if (text6 != null && (text6.StartsWith("- ", StringComparison.Ordinal) || text6.IndexOf(':') < 0)) { if (row.Ingredients.Count != 0 && !row.BracketIngredients) { throw row.Error("Use either Prefab[amount] rows or prefab/amount mappings in ingredients; do not mix them."); } if (row.Ingredients.Count >= 32) { throw row.Error("At most 32 ingredients can be listed per food."); } row.BracketIngredients = true; string text7 = (text6.StartsWith("- ", StringComparison.Ordinal) ? text6.Substring(2) : text6); row.Ingredients.Add(Catalog.BracketIngredient(row, Scalar(text7.Trim(), row))); flag2 = true; continue; } int num5 = (flag5 ? 2 : num3); if (flag6 && text2 != "ingredients") { bool flag7 = text3.StartsWith("- ", StringComparison.Ordinal); if (flag4.HasValue && flag4.Value != flag7) { throw row.Error("Use either dashed fields or mapping fields within '" + text2 + "'; do not mix them."); } flag4 = flag7; if (flag7) { text3 = text3.Substring(2); num5 += 2; } } int num6 = text3.IndexOf(':'); if (num6 <= 0 || (num6 + 1 < text3.Length && !char.IsWhiteSpace(text3[num6 + 1]))) { throw row.Error(((flag6 && text2 == "ingredients") ? "Expected an ingredient row 'Prefab[amount]'" : "Expected field: value") + " at line " + (i + 1) + "."); } string text8 = text3.Substring(0, num6).Trim(); if (!flag6 || !(text2 == "ingredients")) { text8 = YamlMigration.Key(text8, row); } string text9 = Uncomment(text3.Substring(num6 + 1)).Trim(); if (flag5) { row.ItemFirst = Catalog.IsIdentity(text8); } if (!flag6 && Catalog.IsSection(text8)) { string text10 = text8; if (row.RootFields.Count != 0) { foreach (string rootField in row.RootFields) { if (text10 == "basics" && !Catalog.IsIdentity(rootField)) { throw row.Error("Keep item and crafting fields inside basics when using that section."); } if (!Catalog.FieldInSection("basics", rootField)) { throw row.Error("Put food stats inside stats when using sections; do not mix flat stats with sections."); } } } if (text9.Length != 0) { throw row.Error("Section '" + text8 + "' must contain indented rows, not an inline value."); } if (!row.Sections.Add(text10)) { throw row.Error("Duplicate section '" + text10 + "' (including earlier section names)."); } text2 = text10; num2 = -1; flag4 = null; num = 2; flag2 = true; if (text8 == "ingredients") { row.Ingredients = new List<string>(); } continue; } if (!flag6) { if (row.Sections.Count != 0 && (row.Sections.Contains("basics") ? (!Catalog.IsIdentity(text8)) : (!Catalog.FieldInSection("basics", text8)))) { throw row.Error(row.Sections.Contains("basics") ? "Put item and crafting fields inside basics when using grouped sections." : "Only item characteristics belong at the root when using stats and ingredients sections."); } row.RootFields.Add(text8); } else if (text2 != "ingredients" && !Catalog.FieldInSection(text2, text8)) { throw row.Error("Field '" + text8 + "' does not belong in '" + text2 + "'."); } string value2; switch (text9) { default: if (!(text9 == ">+")) { value2 = Scalar(text9, row); break; } goto case "|"; case "|": case "|-": case "|+": case ">": case ">-": { List<string> list2 = new List<string>(); int num7 = -1; int num8 = num5; while (i + 1 < array.Length) { string text11 = array[i + 1]; int num9 = Indent(text11, filename, i + 1); if (text11.Trim().Length != 0 && num9 <= num8) { break; } i++; if (text11.Trim().Length == 0) { list2.Add(""); continue; } if (num7 < 0) { num7 = num9; } if (num9 < num7) { throw row.Error("Invalid block indentation at line " + (i + 1) + "."); } list2.Add(text11.Substring(num7)); } StringBuilder stringBuilder = new StringBuilder(); for (int j = 0; j < list2.Count; j++) { if (j > 0) { stringBuilder.Append((text9[0] == '>' && list2[j].Length != 0 && list2[j - 1].Length != 0) ? ' ' : '\n'); } stringBuilder.Append(list2[j]); } value2 = stringBuilder.ToString(); value2 = ((!text9.EndsWith("-", StringComparison.Ordinal)) ? (value2.TrimEnd('\n') + "\n") : value2.TrimEnd('\n')); break; } } if (flag6 && text2 == "ingredients") { if (row.BracketIngredients) { throw row.Error("Use either Prefab[amount] rows or prefab/amount mappings in ingredients; do not mix them."); } if (row.Ingredients.Count >= 32) { throw row.Error("At most 32 ingredients can be listed per food."); } row.Ingredients.Add(Catalog.NestedIngredient(row, text8, value2)); flag2 = true; } else { if (row.Values.ContainsKey(text8)) { throw row.Error("Duplicate field '" + text8 + "'."); } row.Values.Add(text8, value2); flag2 = true; } } foreach (Row item in list) { if (item.Ingredients != null) { item.Values.Add("ingredientList", string.Join(",", item.Ingredients)); } YamlMigration.Finish(item); } return list; } private static int Indent(string line, string filename, int index) { int i; for (i = 0; i < line.Length && (line[i] == ' ' || line[i] == '\t'); i++) { if (line[i] == '\t') { throw new FormatException(filename + ":" + (index + 1) + ": Use spaces, not tabs, for YAML indentation."); } } return i; } private static bool IsSectionHeading(string line, int indent) { int num = line.IndexOf(':'); return num > indent && (Catalog.IsSection(line.Substring(indent, num - indent).Trim()) || line.Substring(indent, num - indent).Trim() == "itemCharacteristics"); } private static string Uncomment(string text) { bool flag = false; char c = '\0'; int i; for (i = 0; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } if (i < text.Length && (text[i] == '\'' || text[i] == '"')) { flag = true; c = text[i]; } for (int j = (flag ? (i + 1) : 0); j < text.Length; j++) { if (flag) { if (c == '"' && text[j] == '\\') { j++; } else if (c == '\'' && text[j] == '\'' && j + 1 < text.Length && text[j + 1] == '\'') { j++; } else if (text[j] == c) { flag = false; } } else if (text[j] == '#' && (j == 0 || char.IsWhiteSpace(text[j - 1]))) { return text.Substring(0, j); } } return text; } private static string Scalar(string text, Row row) { if (text.Length != 0) { switch (text) { default: { if (text == "NULL") { break; } char c = text[0]; if (c != '\'' && c != '"') { if (text.IndexOf(": ", StringComparison.Ordinal) >= 0) { throw row.Error("Quote text containing a colon followed by a space."); } if ("[{&*!%@`".IndexOf(c) >= 0) { throw row.Error("Use scalar values; YAML collections, tags and anchors are not supported here."); } return text; } if (text.Length < 2 || text[text.Length - 1] != c) { throw row.Error("Unclosed YAML quote."); } StringBuilder stringBuilder = new StringBuilder(); for (int i = 1; i < text.Length - 1; i++) { char c2 = text[i]; if (c2 == c) { if (c == '\'' && i + 1 < text.Length - 1 && text[i + 1] == '\'') { stringBuilder.Append('\''); i++; continue; } throw row.Error("Escape quotes inside quoted text."); } if (c2 == '\\' && c == '"') { if (++i >= text.Length - 1) { throw row.Error("Incomplete YAML escape."); } c2 = text[i]; if (c2 == 'n') { c2 = '\n'; } else if (c2 == 'r') { c2 = '\r'; } else if (c2 == 't') { c2 = '\t'; } else if (c2 == '0') { c2 = '\0'; } else if (c2 == 'b') { c2 = '\b'; } else if (c2 == 'f') { c2 = '\f'; } else { if (c2 == 'u' || c2 == 'x' || c2 == 'U') { int num = c2 switch { 'x' => 2, 'u' => 4, _ => 8, }; if (i + num >= text.Length - 1 || !int.TryParse(text.Substring(i + 1, num), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result) || result < 0 || result > 1114111) { throw row.Error("Invalid Unicode escape."); } if (result <= 65535) { stringBuilder.Append((char)result); } else { stringBuilder.Append(char.ConvertFromUtf32(result)); } i += num; continue; } if (c2 != '"' && c2 != '\\' && c2 != '/') { throw row.Error("Unsupported YAML escape."); } } } stringBuilder.Append(c2); } return stringBuilder.ToString(); } case "~": case "null": case "Null": break; } } return null; } } internal static class FoodEffects { private static string Gain(float value, string token, string fallback) { string text = ((Localization.instance == null) ? token : Localization.instance.Localize(token)); if (text == token || text.StartsWith("[", StringComparison.Ordinal)) { text = fallback; } return "+" + value.ToString("R", CultureInfo.InvariantCulture) + " " + text; } internal static BuffOptions ResolveDisplay(BuffOptions display) { if (((display != null) ? display.CustomBuffName : null) == null) { return display; } try { return FrameworkApi.GetCustomBuff(display.CustomBuffName, true, true, true); } catch (FormatException) { return display; } } internal static string Eating(FoodStatSnapshot food, ModifierSet modifiers = null, BuffOptions presentation = null) { List<string> list = new List<string>(); if (food.Hp > 0f) { list.Add(Gain(food.Hp, "$item_food_health", "Health")); } if (food.Stamina > 0f) { list.Add(Gain(food.Stamina, "$item_food_stamina", "Stamina")); } if (food.Eitr > 0f) { list.Add(Gain(food.Eitr, "$item_food_eitr", "Eitr")); } string text2; if (((presentation != null) ? presentation.CustomBuffName : null) != null) { BuffOptions val = ResolveDisplay(presentation); string text = val.DisplayName ?? val.CustomBuffName; text2 = ((Localization.instance == null) ? text : Localization.instance.Localize(text)); } else { text2 = FrameworkApi.FormatGains(modifiers ?? ModifierSet.Empty); } if (text2.Length > 0) { list.Add(text2); } if (list.Count == 0 && food.Regen > 0f) { list.Add(Gain(food.Regen, "$item_food_health", "Health") + " / 10s"); } return string.Join(" and ", list); } } internal static class FoodMessages { internal sealed class Notice { internal MessageHud Hud; internal MethodInfo Method; internal object[] Arguments; } internal sealed class Scope { internal Scope Previous; internal Player Player; internal RegisteredFood Food; internal BuffOptions Presentation; internal readonly List<Notice> Notices = new List<Notice>(); internal bool Managed; internal bool Finished; } [HarmonyPatch(typeof(Player), "EatFood")] private static class EatingHook { [HarmonyPrepare] private static bool Prepare() { return !Application.isBatchMode && show != null; } private static void Prefix(Player __instance, ItemData __0, out Scope __state) { __state = Begin(__instance, __0); } private static Exception Finalizer(Scope __state, bool __result, Exception __exception) { Plugin.Guard("eating notification", delegate { End(__state, __result, __exception); }); return __exception; } } [HarmonyPatch] private static class HudHook { [HarmonyPrepare] private static bool Prepare() { return PrepareHud(hudTargets, Application.isBatchMode); } private static IEnumerable<MethodBase> TargetMethods() { return hudTargets; } private static bool Prefix(MessageHud __instance, MethodBase __originalMethod, object[] __args) { return Capture(__instance, (MethodInfo)__originalMethod, __args); } } [ThreadStatic] private static Scope active; [ThreadStatic] private static int replaying; private static readonly MethodInfo[] hudTargets = FindHudTargets(typeof(MessageHud)); private static readonly MethodInfo show = hudTargets.OrderByDescending((MethodInfo m) => m.GetParameters().Length).FirstOrDefault(); private static readonly FieldInfo hudInstance = AccessTools.Field(typeof(MessageHud), "m_instance"); internal static MethodInfo[] FindHudTargets(Type hudType) { return hudType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(IsShowMessage).ToArray(); } internal static bool IsShowMessage(MethodInfo method) { ParameterInfo[] parameters = method.GetParameters(); return method.Name == "ShowMessage" && method.ReturnType == typeof(void) && !method.IsStatic && parameters.Length >= 4 && parameters[0].ParameterType == typeof(MessageType) && parameters[1].ParameterType == typeof(string) && parameters[2].ParameterType == typeof(int) && parameters[3].ParameterType == typeof(Sprite) && parameters.Skip(4).All((ParameterInfo parameter) => parameter.ParameterType == typeof(bool)); } internal static bool PrepareHud(IEnumerable<MethodInfo> methods, bool headless) { if (headless) { return false; } if (methods.Any()) { return true; } Plugin.Warn("No compatible MessageHud.ShowMessage method was found. Native eating notices are retained; recipes and food bonuses remain enabled."); return false; } private static object[] MessageArguments(MethodInfo method, Notice route) { if (route != null) { return (object[])route.Arguments.Clone(); } return (from p in method.GetParameters() select (p.HasDefaultValue && p.DefaultValue != DBNull.Value && p.DefaultValue != Missing.Value) ? p.DefaultValue : (p.ParameterType.IsValueType ? Activator.CreateInstance(p.ParameterType) : null)).ToArray(); } internal static Scope Begin(Player player, ItemData item) { RegisteredFood registeredFood = Plugin.Instance?.Registry.Find(item); bool flag = show != null && !Application.isBatchMode && Object.op_Implicit((Object)(object)player) && player == Player.m_localPlayer && registeredFood != null; if (!flag && active == null) { return null; } return active = new Scope { Previous = active, Player = player, Food = registeredFood, Presentation = registeredFood?.Presentation, Managed = flag }; } internal static bool Capture(MessageHud hud, MethodInfo method, object[] arguments) { if (replaying != 0 || active == null || !active.Managed) { return true; } active.Notices.Add(new Notice { Hud = hud, Method = method, Arguments = (object[])arguments.Clone() }); return false; } internal static void End(Scope scope, bool success, Exception error) { //IL_01be: Unknown result type (might be due to invalid IL or missing references) if (scope == null || scope.Finished) { return; } scope.Finished = true; active = scope.Previous; if (!scope.Managed) { return; } replaying++; try { if (!success || error != null) { foreach (Notice notice2 in scope.Notices) { notice2.Method.Invoke(notice2.Hud, notice2.Arguments); } return; } Food val = ((IEnumerable<Food>)scope.Player.GetFoods()).FirstOrDefault((Func<Food, bool>)((Food f) => Plugin.Instance.Registry.Find(f.m_item) == scope.Food)); FoodStatSnapshot foodStatSnapshot = ((val == null) ? null : FoodStats.FindSnapshot(val.m_item)); SourceSnapshot val2 = ((val == null) ? null : FrameworkApi.GetFoodSnapshot(scope.Player, val.m_item)); string text = FoodEffects.Eating(foodStatSnapshot ?? scope.Food.Snapshot(), ((val2 != null) ? val2.Modifiers : null) ?? scope.Food.ModifierSet, scope.Presentation); if (text.Length != 0) { Notice notice = scope.Notices.FirstOrDefault(); MessageHud val3 = (MessageHud)(((object)notice?.Hud) ?? ((object)(MessageHud)(hudInstance?.GetValue(null)))); MethodInfo methodInfo = notice?.Method ?? show; if (Object.op_Implicit((Object)(object)val3) && !(methodInfo == null)) { object[] array = MessageArguments(methodInfo, notice); array[0] = (object)(MessageType)2; array[1] = text; array[2] = 0; array[3] = scope.Food.Icons[0]; methodInfo.Invoke(val3, array); } } } finally { replaying--; } } } internal static class FoodEligibility { [HarmonyPatch] private static class EligibilityHooks { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(Player), "CanEat", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Player), "EatFood", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(ItemDrop), "CanEat", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Humanoid), "UseItem", (Type[])null, (Type[])null); yield return AccessTools.DeclaredMethod(typeof(Player), "CanConsumeItem", (Type[])null, (Type[])null); yield return AccessTools.DeclaredMethod(typeof(Player), "ConsumeItem", (Type[])null, (Type[])null); yield return AccessTools.DeclaredMethod(typeof(Humanoid), "CanConsumeItem", (Type[])null, (Type[])null); yield return AccessTools.DeclaredMethod(typeof(Humanoid), "ConsumeItem", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(InventoryGui), "UpdateRecipe", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(InventoryGui), "DoCrafting", (Type[])null, (Type[])null); } private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod) { return Rewrite(instructions, __originalMethod); } } private static readonly FieldInfo hp = AccessTools.Field(typeof(SharedData), "m_food"); private static readonly MethodInfo predicate = AccessTools.Method(typeof(FoodEligibility), "Eligibility", (Type[])null, (Type[])null); internal static float Eligibility(SharedData shared) { if (shared.m_food > 0f || (Object)(object)Plugin.Instance == (Object)null) { return shared.m_food; } RegisteredFood registeredFood = Plugin.Instance.Registry.Find(shared); return (registeredFood == null) ? shared.m_food : 1f; } internal static IEnumerable<CodeInstruction> Rewrite(IEnumerable<CodeInstruction> instructions, MethodBase original) { List<CodeInstruction> list = instructions.ToList(); int num = 0; for (int i = 0; i + 2 < list.Count; i++) { if (list[i].opcode != OpCodes.Ldfld || !object.Equals(list[i].operand, hp)) { continue; } int j; for (j = i + 1; j < list.Count && list[j].opcode == OpCodes.Nop; j++) { } if (j + 1 < list.Count && !(list[j].opcode != OpCodes.Ldc_R4) && list[j].operand is float && (float)list[j].operand == 0f) { OpCode opcode = list[j + 1].opcode; if (!(opcode != OpCodes.Ble_Un) || !(opcode != OpCodes.Ble_Un_S) || !(opcode != OpCodes.Ble) || !(opcode != OpCodes.Ble_S) || !(opcode != OpCodes.Bgt) || !(opcode != OpCodes.Bgt_S) || !(opcode != OpCodes.Bgt_Un) || !(opcode != OpCodes.Bgt_Un_S) || !(opcode != OpCodes.Bge) || !(opcode != OpCodes.Bge_S) || !(opcode != OpCodes.Bge_Un) || !(opcode != OpCodes.Bge_Un_S) || !(opcode != OpCodes.Blt) || !(opcode != OpCodes.Blt_S) || !(opcode != OpCodes.Blt_Un) || !(opcode != OpCodes.Blt_Un_S) || !(opcode != OpCodes.Cgt) || !(opcode != OpCodes.Cgt_Un) || !(opcode != OpCodes.Clt) || !(opcode != OpCodes.Clt_Un) || !(opcode != OpCodes.Beq) || !(opcode != OpCodes.Beq_S) || !(opcode != OpCodes.Bne_Un) || !(opcode != OpCodes.Bne_Un_S) || !(opcode != OpCodes.Ceq)) { list[i].opcode = OpCodes.Call; list[i].operand = predicate; num++; } } } if (num == 0 && original.DeclaringType != typeof(InventoryGui) && original.Name != "EatFood" && list.Any((CodeInstruction x) => x.opcode == OpCodes.Ldfld && object.Equals(x.operand, hp))) { throw new NotSupportedException("Unrecognized zero-HP food eligibility check in " + original.DeclaringType.Name + "." + original.Name + "."); } return list; } } internal static class FoodForks { [HarmonyPatch(typeof(InventoryGrid), "UpdateGui")] private static class GridHook { [HarmonyPrepare] private static bool Prepare() { if (Application.isBatchMode) { return false; } if (Compatible) { return true; } if (!reported) { reported = true; Plugin.Warn("Food fork layout is unavailable; native inventory display is retained."); } return false; } [HarmonyPriority(0)] private static void Postfix(InventoryGrid __instance) { try { Refresh(__instance); } catch (Exception ex) { if (!reported) { reported = true; Plugin.Warn("Could not update the food fork: " + ex.Message); } } } } private static readonly FieldInfo elements = AccessTools.Field(typeof(InventoryGrid), "m_elements"); private static readonly Type elementType; private static readonly FieldInfo position; private static readonly MethodInfo positionGetter; private static readonly FieldInfo fork; private static readonly Func<object, Vector2i> readPosition; private static readonly Func<InventoryGrid, Color> healthColor; private static readonly Func<InventoryGrid, Color> staminaColor; private static readonly Func<InventoryGrid, Color> eitrColor; private static readonly bool compatible; private static bool reported; internal static bool Compatible => compatible; private static Func<InventoryGrid, Color> ColorReader(string name) { FieldInfo fieldInfo = AccessTools.Field(typeof(InventoryGrid), name); if (fieldInfo?.FieldType != typeof(Color)) { return null; } try { DynamicMethod dynamicMethod = new DynamicMethod("FoodKnowledge_" + name, typeof(Color), new Type[1] { typeof(InventoryGrid) }, typeof(FoodForks), skipVisibility: true); ILGenerator iLGenerator = dynamicMethod.GetILGenerator(); iLGenerator.Emit(OpCodes.Ldarg_0); iLGenerator.Emit(OpCodes.Ldfld, fieldInfo); iLGenerator.Emit(OpCodes.Ret); return (Func<InventoryGrid, Color>)dynamicMethod.CreateDelegate(typeof(Func<InventoryGrid, Color>)); } catch { return null; } } private static Func<object, Vector2i> PositionReader() { if (positionGetter?.ReturnType == typeof(Vector2i)) { try { return (Func<object, Vector2i>)AccessTools.Method(typeof(FoodForks), "CreatePositionReader", (Type[])null, (Type[])null).MakeGenericMethod(elementType).Invoke(null, new object[1] { positionGetter }); } catch { return (object cell) => (Vector2i)positionGetter.Invoke(cell, null); } } return (position?.FieldType == typeof(Vector2i)) ? ((Func<object, Vector2i>)((object cell) => (Vector2i)position.GetValue(cell))) : null; } private static Func<object, Vector2i> CreatePositionReader<T>(MethodInfo getter) { Func<T, Vector2i> read = (Func<T, Vector2i>)getter.CreateDelegate(typeof(Func<T, Vector2i>)); return (object cell) => read((T)cell); } internal static bool ModifierOnly(ItemData item, FoodRegistry registry) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 SharedData val = item?.m_shared; if (val == null || (int)val.m_itemType != 2 || val.m_food != 0f || val.m_foodStamina != 0f || val.m_foodEitr != 0f) { return false; } RegisteredFood registeredFood = registry?.Find(item); return registeredFood != null && registeredFood.ModifierSet.HasAny; } internal static void Refresh(InventoryGrid grid) { //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Invalid comparison between Unknown and I4 //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0140: 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) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_0274: Unknown result type (might be due to invalid IL or missing references) //IL_0281: Unknown result type (might be due to invalid IL or missing references) //IL_0267: Unknown result type (might be due to invalid IL or missing references) //IL_025a: Unknown result type (might be due to invalid IL or missing references) //IL_0286: Unknown result type (might be due to invalid IL or missing references) //IL_028a: Unknown result type (might be due to invalid IL or missing references) //IL_028f: Unknown result type (might be due to invalid IL or missing references) //IL_0293: Unknown result type (might be due to invalid IL or missing references) //IL_02a5: Unknown result type (might be due to invalid IL or missing references) if (Application.isBatchMode || !Object.op_Implicit((Object)(object)grid) || !Compatible) { return; } FoodRegistry foodRegistry = Plugin.Instance?.Registry; Inventory val = ((foodRegistry == null) ? null : grid.GetInventory()); if (val == null) { return; } IList list = null; int num = 0; foreach (ItemData allItem in val.GetAllItems()) { SharedData val2 = allItem?.m_shared; if (val2 == null || (int)val2.m_itemType != 2 || (val2.m_food <= 0f && val2.m_foodStamina <= 0f && val2.m_foodEitr <= 0f && !ModifierOnly(allItem, foodRegistry))) { continue; } if (list == null) { list = elements.GetValue(grid) as IList; num = val.GetWidth(); } if (list == null || num <= 0) { break; } Vector2i gridPos = allItem.m_gridPos; if (gridPos.x < 0 || gridPos.x >= num || gridPos.y < 0) { continue; } long num2 = (long)gridPos.y * (long)num + gridPos.x; if (num2 >= list.Count) { continue; } object obj = list[(int)num2]; if (obj == null) { continue; } Vector2i val3 = readPosition(obj); if (val3.x != gridPos.x || val3.y != gridPos.y) { continue; } object? value = fork.GetValue(obj); Image val4 = (Image)((value is Image) ? value : null); if (Object.op_Implicit((Object)(object)val4) && Object.op_Implicit((Object)(object)((Component)val4).gameObject)) { Color val5 = ((val2.m_foodEitr > val2.m_food && val2.m_foodEitr > val2.m_foodStamina) ? eitrColor(grid) : ((val2.m_food > val2.m_foodStamina) ? healthColor(grid) : ((val2.m_foodStamina > val2.m_food) ? staminaColor(grid) : Color.white))); Color color = ((Graphic)val4).color; if (!((Color)(ref color)).Equals(val5)) { ((Graphic)val4).color = val5; } if (!((Behaviour)val4).enabled) { ((Behaviour)val4).enabled = true; } ((Component)val4).gameObject.SetActive(true); } } } static FoodForks() { FieldInfo fieldInfo = elements; elementType = (((object)fieldInfo != null && fieldInfo.FieldType.IsGenericType) ? elements.FieldType.GetGenericArguments()[0] : null); position = ((elementType == null) ? null : AccessTools.Field(elementType, "m_pos")); positionGetter = ((elementType == null) ? null : AccessTools.PropertyGetter(elementType, "Position")); fork = ((elementType == null) ? null : AccessTools.Field(elementType, "m_food")); readPosition = PositionReader(); healthColor = ColorReader("m_foodHealthColor"); staminaColor = ColorReader("m_foodStaminaColor"); eitrColor = ColorReader("m_foodEitrColor"); compatible = elements != null && typeof(IList).IsAssignableFrom(elements.FieldType) && readPosition != null && fork?.FieldType == typeof(Image) && healthColor != null && staminaColor != null && eitrColor != null; } } internal sealed class FoodIcons : IDisposable { private sealed class Entry { internal IconFile File; internal Sprite Sprite; internal Texture2D Texture; internal long Pixels; } internal const int MaxBytes = 8388608; internal const int MaxDimension = 2048; private const long MaxPixels = 33554432L; internal readonly IconFiles Files; private readonly Action<string> warn; private readonly Dictionary<string, Entry> cache = new Dictionary<string, Entry>(StringComparer.Ordinal); private readonly Dictionary<string, string> errors = new Dictionary<string, string>(StringComparer.Ordinal); private long pixels; private Dictionary<string, IconFile> serverFiles; internal bool ServerLocked => serverFiles != null; internal bool NeedsRetry { get; private set; } internal FoodIcons(string directory, Action<string> warn) : this(new IconFiles(directory), warn) { } internal FoodIcons(IconFiles files, Action<string> warn) { Files = files; this.warn = warn; } internal void UseServer(Dictionary<string, IconFile> files) { if (serverFiles == null) { Dispose(); } serverFiles = files ?? new Dictionary<string, IconFile>(StringComparer.Ordinal); } internal void UseLocal() { Dispose(); serverFiles = null; } internal bool Refresh(IEnumerable<RegisteredFood> registered, Sprite fallback) { //IL_02ec: Unknown result type (might be due to invalid IL or missing references) //IL_02f1: Unknown result type (might be due to invalid IL or missing references) //IL_0309: Unknown result type (might be due to invalid IL or missing references) //IL_0311: Unknown result type (might be due to invalid IL or missing references) //IL_031b: Expected O, but got Unknown //IL_037d: Unknown result type (might be due to invalid IL or missing references) //IL_038c: Unknown result type (might be due to invalid IL or missing references) NeedsRetry = false; if (Application.isBatchMode) { return false; } RegisteredFood[] array = registered.ToArray(); Dictionary<string, string[]> dictionary; try { dictionary = ((serverFiles == null) ? ((IEnumerable<string>)Files.Paths()) : ((IEnumerable<string>)serverFiles.Keys.ToArray())).GroupBy<string, string>(Path.GetFileNameWithoutExtension, StringComparer.Ordinal).ToDictionary<IGrouping<string, string>, string, string[]>((IGrouping<string, string> g) => g.Key, (IGrouping<string, string> g) => g.OrderBy<string, string>((string p) => p, StringComparer.Ordinal).ToArray(), StringComparer.Ordinal); errors.Remove("folder"); } catch (Exception ex) { NeedsRetry = ex is IOException; Warn("folder", "Cannot read the icons folder: " + ex.Message); return false; } HashSet<string> names = new HashSet<string>(array.Select((RegisteredFood f) => f.Item), StringComparer.Ordinal); string[] array2 = cache.Keys.Where((string k) => !names.Contains(k)).ToArray(); foreach (string item in array2) { Remove(item); } bool flag = false; RegisteredFood[] array3 = array; foreach (RegisteredFood registeredFood in array3) { Sprite val = (registeredFood.Created ? fallback : registeredFood.NativeIcon); cache.TryGetValue(registeredFood.Item, out var value); if (!dictionary.TryGetValue(registeredFood.Item, out var value2)) { flag |= Set(registeredFood, val); Remove(registeredFood.Item); errors.Remove(registeredFood.Item); continue; } Texture2D val2 = null; Sprite val3 = null; try { if (value2.Length != 1) { throw new FormatException("More than one PNG matches this item. Keep one file with this exact item name."); } IconFile iconFile = ((serverFiles == null) ? Files.Read(value2[0]) : serverFiles[value2[0]]); int width = iconFile.Width; int height = iconFile.Height; if (value == null || value.File.Hash != iconFile.Hash) { long num3 = (long)width * (long)height; if (pixels - (value?.Pixels ?? 0) + num3 > 33554432) { throw new FormatException("Combined loaded icons exceed 32 million pixels. Use smaller icons."); } val2 = new Texture2D(2, 2, (TextureFormat)4, false) { name = "FoodKnowledge_Icon_" + registeredFood.Item, wrapMode = (TextureWrapMode)1, filterMode = (FilterMode)1 }; if (!ImageConversion.LoadImage(val2, iconFile.Bytes, true)) { throw new FormatException("Unity could not decode the PNG."); } if (((Texture)val2).width != width || ((Texture)val2).height != height) { throw new FormatException("Decoded PNG dimensions do not match its header."); } val3 = Sprite.Create(val2, new Rect(0f, 0f, (float)width, (float)height), new Vector2(0.5f, 0.5f), 100f, 0u, (SpriteMeshType)0); if (!Object.op_Implicit((Object)(object)val3)) { throw new FormatException("Unity could not create the icon sprite."); } ((Object)val3).name = "FoodKnowledge_Icon_" + registeredFood.Item; flag |= Set(registeredFood, val3); Remove(registeredFood.Item); value = new Entry { File = iconFile, Sprite = val3, Texture = val2, Pixels = num3 }; cache[registeredFood.Item] = value; pixels += num3; val2 = null; val3 = null; } flag |= Set(registeredFood, value.Sprite); errors.Remove(registeredFood.Item); } catch (Exception ex2) { if (Object.op_Implicit((Object)(object)val3)) { Object.Destroy((Object)(object)val3); } if (Object.op_Implicit((Object)(object)val2)) { Object.Destroy((Object)(object)val2); } NeedsRetry |= ex2 is IOException; Warn(registeredFood.Item, "Icon for '" + registeredFood.Item + "' was not changed: " + ex2.Message); flag |= Set(registeredFood, Object.op_Implicit((Object)(object)value?.Sprite) ? value.Sprite : val); } } return flag; } private static bool Set(RegisteredFood food, Sprite sprite) { if (food.Icons[0] == sprite) { return false; } food.Icons[0] = sprite; return true; } private void Warn(string key, string message) { if (!errors.TryGetValue(key, out var value) || !(value == message)) { errors[key] = message; warn?.Invoke(message); } } private void Remove(string item) { if (cache.TryGetValue(item, out var value)) { pixels -= value.Pixels; cache.Remove(item); if (Object.op_Implicit((Object)(object)value.Sprite)) { Object.Destroy((Object)(object)value.Sprite); } if (Object.op_Implicit((Object)(object)value.Texture)) { Object.Destroy((Object)(object)value.Texture); } } } internal static byte[] Read(string path) { FileInfo fileInfo = new FileInfo(path); long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; if (length < 33 || length > 8388608) { throw new FormatException("PNG must be at most 8 MiB and contain a valid image header."); } byte[] array = new byte[(int)length]; using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { int i; int num; for (i = 0; i < array.Length; i += num) { if ((num = fileStream.Read(array, i, array.Length - i)) == 0) { break; } } if (i != array.Length || fileStream.ReadByte() != -1) { throw new IOException("PNG changed while being read; waiting for the save to finish."); } } FileInfo fileInfo2 = new FileInfo(path); if (!fileInfo2.Exists || fileInfo2.Length != length || fileInfo2.LastWriteTimeUtc != lastWriteTimeUtc) { throw new IOException("PNG changed while being read; waiting for the save to finish."); } return array; } internal static void Dimensions(byte[] bytes, out int width, out int height) { byte[] second = new byte[8] { 137, 80, 78, 71, 13, 10, 26, 10 }; if (bytes == null || bytes.Length < 33 || !bytes.Take(8).SequenceEqual(second) || Number(bytes, 8) != 13 || bytes[12] != 73 || bytes[13] != 72 || bytes[14] != 68 || bytes[15] != 82) { throw new FormatException("File is not a PNG with a valid IHDR header."); } uint num = Number(bytes, 16); uint num2 = Number(bytes, 20); if (num == 0 || num2 == 0 || num > 2048 || num2 > 2048) { throw new FormatException("PNG dimensions must be between 1 and 2048 pixels per side."); } width = (int)num; height = (int)num2; } private static uint Number(byte[] data, int offset) { return (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); } public void Dispose() { string[] array = cache.Keys.ToArray(); foreach (string item in array) { Remove(item); } errors.Clear(); serverFiles = null; NeedsRetry = false; } } internal sealed class RegisteredFood { internal string Item; internal string UseModel; internal string Title; internal string Description; internal string EffectInfo; internal GameObject Prefab; internal ItemDrop Drop; internal ModifierSet ModifierSet = ModifierSet.Empty; internal BuffOptions Presentation; internal Recipe[] Recipes = Array.Empty<Recipe>(); internal Sprite[] Icons = (Sprite[])(object)new Sprite[1]; internal bool Active; internal bool Created = true; internal bool Deleted; internal bool ChangedRecipe; internal SharedData Baseline; internal Recipe[] OriginalRecipes = Array.Empty<Recipe>(); internal Sprite NativeIcon => (Baseline?.m_icons != null && Baseline.m_icons.Length != 0) ? Baseline.m_icons[0] : null; internal FoodStatSnapshot Snapshot() { SharedData shared = Drop.m_itemData.m_shared; return new FoodStatSnapshot { Item = Item, Hp = shared.m_food, Stamina = shared.m_foodStamina, Eitr = shared.m_foodEitr, Seconds = shared.m_foodBurnTime, Regen = shared.m_foodRegen }; } } internal sealed class FoodRegistry { private sealed class References<T> : IEqualityComparer<T> where T : class { internal static readonly References<T> Instance = new References<T>(); public bool Equals(T x, T y) { return x == y; } public int GetHashCode(T value) { return RuntimeHelpers.GetHashCode(value); } } private sealed class Plan { internal FoodDefinition Definition; internal ItemDrop Template; internal ItemDrop BaseFood; internal SharedData Values; internal Ingredient[] Ingredients; internal CraftingStation[] Stations; internal int Amount; internal string Title; internal string Description; internal RegisteredFood Existing; internal bool Created; internal bool Delete; internal bool ChangeRecipe; internal Recipe[] Originals = Array.Empty<Recipe>(); } private sealed class InventoryRevision { internal int Value = -1; } [HarmonyPatch(typeof(Inventory), "Changed")] private static class InventoryChangeHook { private static void Prefix(Inventory __instance) { if (!Application.isBatchMode) { Plugin.Instance?.Registry?.RefreshInventory(__instance, force: true, notify: false); } } } [HarmonyPatch(typeof(InventoryGrid), "UpdateGui")] private static class InventoryDisplayHook { private static void Prefix(InventoryGrid __instance) { if (!Application.isBatchMode && Object.op_Implicit((Object)(object)__instance)) { Plugin.Instance?.Registry?.RefreshInventory(__instance.GetInventory()); } } } internal ObjectDB Database; internal ZNetScene Scene; internal bool SceneAwaking; internal FoodIcons Icons; internal Action<string> Warning; private GameObject root; private readonly Dictionary<string, RegisteredFood> foods = new Dictionary<string, RegisteredFood>(StringComparer.Ordinal); private readonly Dictionary<string, RegisteredFood> tokens = new Dictionary<string, RegisteredFood>(StringComparer.Ordinal); private readonly HashSet<Recipe> ownedRecipes = new HashSet<Recipe>(References<Recipe>.Instance); private readonly HashSet<List<GameObject>> itemLists = new HashSet<List<GameObject>>(); private readonly HashSet<List<Recipe>> recipeLists = new HashSet<List<Recipe>>(); private readonly HashSet<Dictionary<int, GameObject>> itemHashes = new HashSet<Dictionary<int, GameObject>>(); private readonly HashSet<Dictionary<SharedData, GameObject>> itemData = new HashSet<Dictionary<SharedData, GameObject>>(); private readonly HashSet<List<GameObject>> sceneLists = new HashSet<List<GameObject>>(); private readonly HashSet<Dictionary<int, GameObject>> sceneHashes = new HashSet<Dictionary<int, GameObject>>(); private readonly Dictionary<Localization, Dictionary<string, string>> nativeWords = new Dictionary<Localization, Dictionary<string, string>>(); private readonly HashSet<string> nativeFoods = new HashSet<string>(StringComparer.Ordinal); private int itemRevision; private bool inventoryWarning; private readonly ConditionalWeakTable<Inventory, InventoryRevision> refreshedInventories = new ConditionalWeakTable<Inventory, InventoryRevision>(); internal bool DatabaseReady => Object.op_Implicit((Object)(object)Database) && Database.m_items != null && Database.m_items.Count > 0 && Object.op_Implicit((Object)(object)Database.GetItemPrefab("Wood")); internal bool HasActiveFoods => ownedRecipes.Count != 0; internal IEnumerable<RegisteredFood> ActiveEntries => foods.Values.Where((RegisteredFood food) => food.Active); internal RegisteredFood Find(string item) { RegisteredFood value; return (item != null && foods.TryGetValue(item, out value)) ? value : null; } internal RegisteredFood Find(ItemData data) { if (data == null || data.m_shared == null) { return null; } RegisteredFood value; return (Object.op_Implicit((Object)(object)data.m_dropPrefab) && foods.TryGetValue(((Object)data.m_dropPrefab).name, out value)) ? value : (tokens.TryGetValue(data.m_shared.m_name ?? "", out value) ? value : null); } internal RegisteredFood Find(SharedData shared) { RegisteredFood value; return (shared != null && tokens.TryGetValue(shared.m_name ?? "", out value)) ? value : null; } internal bool IsOwned(Recipe recipe) { return Object.op_Implicit((Object)(object)recipe) && ownedRecipes.Contains(recipe); } internal static string NameKey(string item) { return "foodknowledge_name_" + item; } internal static string DescriptionKey(string item) { return "foodknowledge_description_" + item; } private List<Plan> Prepare(Catalog catalog) { //IL_03fd: Unknown result type (might be due to invalid IL or missing references) //IL_0403: Invalid comparison between Unknown and I4 //IL_0a7c: Unknown result type (might be due to invalid IL or missing references) if (nativeFoods.Count == 0) { CaptureNativeFoods(Database); } GameObject itemPrefab = Database.GetItemPrefab("RawMeat"); ItemDrop val = (Object.op_Implicit((Object)(object)itemPrefab) ? itemPrefab.GetComponent<ItemDrop>() : null); List<Plan> list = new List<Plan>(); HashSet<string> hashSet = new HashSet<string>(catalog.Foods.Select((FoodDefinition f) => f.Item), StringComparer.Ordinal); Dictionary<string, CraftingStation> dictionary = new Dictionary<string, CraftingStation>(StringComparer.Ordinal); foreach (Recipe item in Database.m_recipes.Concat(foods.Values.SelectMany((RegisteredFood f) => f.OriginalRecipes))) { if (Object.op_Implicit((Object)(object)item) && Object.op_Implicit((Object)(object)item.m_craftingStation)) { dictionary[((Object)((Component)item.m_craftingStation).gameObject).name] = item.m_craftingStation; } } if (Object.op_Implicit((Object)(object)Scene) && Scene.m_prefabs != null) { foreach (GameObject prefab in Scene.m_prefabs) { if (Object.op_Implicit((Object)(object)prefab)) { CraftingStation component = prefab.GetComponent<CraftingStation>(); if (Object.op_Implicit((Object)(object)component)) { dictionary[((Object)prefab).name] = component; } } } } Dictionary<int, GameObject> dictionary2 = (Dictionary<int, GameObject>)GameApi.ItemHashes.GetValue(Database); Dictionary<int, GameObject> dictionary3 = (Object.op_Implicit((Object)(object)Scene) ? ((Dictionary<int, GameObject>)GameApi.SceneHashes.GetValue(Scene)) : null); foreach (FoodDefinition food in catalog.Foods) { RegisteredFood own = Find(food.Item); int hash = Catalog.StableHash(food.Item); GameObject itemPrefab2 = Database.GetItemPrefab(food.Item); if (dictionary2.TryGetValue(hash, out var value) && Object.op_Implicit((Object)(object)value) && ((Object)value).name != food.Item) { throw new FormatException(food.Location + ": prefab hash collision for '" + food.Item + "'."); } if (own != null && Object.op_Implicit((Object)(object)itemPrefab2) && (Object)(object)itemPrefab2 != (Object)(object)own.Prefab) { throw new FormatException(food.Location + ": another mod replaced the tracked prefab '" + food.Item + "'. Restart to resolve the conflict."); } bool flag = own?.Created ?? (!Object.op_Implicit((Object)(object)itemPrefab2)); Plan plan = new Plan { Definition = food, Existing = own, Created = flag }; if (!flag) { ItemDrop drop = (Object.op_Implicit((Object)(object)itemPrefab2) ? itemPrefab2.GetComponent<ItemDrop>() : null); SharedData val2 = own?.Baseline ?? drop?.m_itemData?.m_shared; if (!nativeFoods.Contains(food.Item) || !Object.op_Implicit((Object)(object)drop) || val2 == null || (int)val2.m_itemType != 2 || (val2.m_food <= 0f && val2.m_foodStamina <= 0f && val2.m_foodEitr <= 0f)) { throw new FormatException(food.Location + ": existing item '" + food.Item + "' is not a native food. Only existing foods can be edited or deleted."); } if (food.Overrides("useModel") && !string.IsNullOrEmpty(food.Fields["useModel"]) && food.Fields["useModel"] != food.Item) { throw new FormatException(food.Location + ": useModel selects appearance for new foods only; existing foods keep their native model."); } plan.Template = drop; plan.BaseFood = drop; plan.Values = GameApi.Clone(val2); plan.Originals = own?.OriginalRecipes ?? Database.m_recipes.Where((Recipe r) => Object.op_Implicit((Object)(object)r) && (Object)(object)r.m_item == (Object)(object)drop && !ownedRecipes.Contains(r)).ToArray(); string text = NativeSignature(val2, plan.Originals); if (food.Synchronized && food.NativeBaseline != text) { throw new FormatException(food.Location + ": native food/recipe baseline differs from the server for '" + food.Item + "'. Install matching game and recipe/stat mods."); } plan.Delete = food.NameOnly; plan.ChangeRecipe = plan.Delete || food.Has("craftingStation") || food.Overrides("stationLevel") || food.Overrides("amountProduced") || food.Ingredients != null; if (food.Overrides("hpGain")) { plan.Values.m_food = food.Hp; } if (food.Overrides("hpPerTick")) { plan.Values.m_foodRegen = food.HpPerTick; } if (food.Overrides("staminaGain")) { plan.Values.m_foodStamina = food.Stamina; } if (food.Overrides("eitrGain")) { plan.Values.m_foodEitr = food.Eitr; } if (food.Overrides("durationMinutes")) { plan.Values.m_foodBurnTime = food.DurationMinutes * 60f; } if (food.Overrides("weightPerUnit")) { plan.Values.m_weight = food.WeightPerUnit; } if (food.Overrides("stackSize")) { plan.Values.m_maxStackSize = food.StackSize; } plan.Title = (food.Overrides("displayName") ? (food.DisplayName ?? "") : null); plan.Description = (food.Overrides("description") ? (food.Description ?? "") : null); if (plan.Title != null && !(plan.Values.m_name ?? "").StartsWith("$", StringComparison.Ordinal)) { plan.Values.m_name = "$" + NameKey(food.Item); } if (plan.Description != null && !(plan.Values.m_description ?? "").StartsWith("$", StringComparison.Ordinal)) { plan.Values.m_description = "$" + DescriptionKey(food.Item); } if (plan.ChangeRecipe && !plan.Delete && plan.Originals.Length > 1) { throw new FormatException(food.Location + ": this food has several native recipes; recipe edits require one unambiguous recipe."); } goto IL_0b98; } if (food.NativeBaseline != null) { throw new FormatException(food.Location + ": a native baseline fingerprint cannot target a new food."); } if (food.Ingredients == null) { throw new FormatException(food.Location + ": a new food requires ingredientList or an ingredients list."); } if (!Object.op_Implicit((Object)(object)val) || val.m_itemData?.m_shared == null || !Object.op_Implicit((Object)(object)FallbackIcon())) { throw new FormatException("The vanilla boar-meat (RawMeat) item/icon is not available yet."); } if ((dictionary3 != null && dictionary3.TryGetValue(hash, out value) && (own == null || (Object)(object)value != (Object)(object)own.Prefab)) || (Object.op_Implicit((Object)(object)Scene) && SceneAwaking && Scene.m_prefabs.Any((GameObject x) => Object.op_Implicit((Object)(object)x) && Catalog.StableHash(((Object)x).name) == hash && (own == null || (Object)(object)x != (Object)(object)own.Prefab)))) { throw new FormatException(food.Location + ": network prefab collision for '" + food.Item + "'."); } object obj; if (!hashSet.Contains(food.UseModel)) { RegisteredFood registeredFood = Find(food.UseModel); if (registeredFood == null || !registeredFood.Created) { obj = Database.GetItemPrefab(food.UseModel); goto IL_0987; } } obj = null; goto IL_0987; IL_0987: GameObject val3 = (GameObject)obj; ItemDrop val4 = (Object.op_Implicit((Object)(object)val3) ? val3.GetComponent<ItemDrop>() : null); if (!Object.op_Implicit((Object)(object)val4) || val4.m_itemData?.m_shared == null) { val4 = val; Warning?.Invoke(food.Location + ": useModel '" + food.UseModel + "' is unavailable; using boar meat (RawMeat)."); } if (own != null && own.UseModel != ((Object)((Component)val4).gameObject).name) { throw new FormatException(food.Location + ": changing useModel for an already registered item requires restarting the game/server."); } plan.Template = val4; plan.BaseFood = val; plan.ChangeRecipe = true; plan.Values = GameApi.Clone(val.m_itemData.m_shared); SharedData values = plan.Values; values.m_itemType = (ItemType)2; values.m_food = food.Hp; values.m_foodStamina = food.Stamina; values.m_foodEitr = food.Eitr; values.m_foodBurnTime = food.DurationMinutes * 60f; values.m_foodRegen = food.HpPerTick; values.m_name = "$" + NameKey(food.Item); values.m_description = "$" + DescriptionKey(food.Item); values.m_weight = food.WeightPerUnit; values.m_maxStackSize = food.StackSize; values.m_maxQuality = 1; values.m_teleportable = true; values.m_useDurability = false; values.m_consumeStatusEffect = null; values.m_equipStatusEffect = null; values.m_setStatusEffect = null; values.m_appendToolTip = null; plan.Title = (string.IsNullOrWhiteSpace(food.DisplayName) ? food.Item : food.DisplayName); plan.Description = food.Description ?? ""; goto IL_0b98; IL_0b98: if (plan.ChangeRecipe && !plan.Delete) { Recipe val5 = plan.Originals.FirstOrDefault(); plan.Ingredients = food.Ingredients ?? val5?.m_resources?.Select((Requirement r) => new Ingredient { Item = ((Object)((Component)r.m_resItem).gameObject).name, Amount = r.m_amount }).ToArray(); if (plan.Ingredients == null || plan.Ingredients.Length == 0) { throw new FormatException(food.Location + ": creating a recipe requires ingredients."); } Ingredient[] ingredients = plan.Ingredients; foreach (Ingredient ingredient in ingredients) { if (ingredient.Item == food.Item) { throw new FormatException(food.Location + ": a food cannot require itself as an ingredient."); } if (!hashSet.Contains(ingredient.Item)) { GameObject itemPrefab3 = Database.GetItemPrefab(ingredient.Item); if (!Object.op_Implicit((Object)(object)itemPrefab3) || !Object.op_Implicit((Object)(object)itemPrefab3.GetComponent<ItemDrop>())) { throw new FormatException(food.Location + ": unknown ingredient prefab '" + ingredient.Item + "'."); } } } List<CraftingStation> list2 = new List<CraftingStation>(); if (!plan.Created && !food.Has("craftingStation")) { list2.Add(Object.op_Implicit((Object)(object)val5) ? val5.m_craftingStation : null); } else { string[] stations = food.Stations; foreach (string text2 in stations) { if (!dictionary.TryGetValue(text2, out var value2)) { throw new FormatException(food.Location + ": unknown crafting-station prefab '" + text2 + "'. Use native crafting-menu stations such as piece_cauldron; cooking racks and ovens are conversion machines."); } list2.Add(value2); } } if (list2.Count == 0) { list2.Add(null); } plan.Stations = list2.ToArray(); plan.Amount = ((!plan.Created && !food.Overrides("amountProduced") && Object.op_Implicit((Object)(object)val5)) ? val5.m_amount : food.Amount); } list.Add(plan); } Catalog.SerializeSynchronized(catalog.Foods, list.Where((Plan p) => !p.Created).ToDictionary<Plan, string, string>((Plan p) => p.Definition.Item, (Plan p) => NativeSignature(p.Existing?.Baseline ?? p.Template.m_itemData.m_shared, p.Originals), StringComparer.Ordinal)); return list; } internal bool Apply(Catalog catalog) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown if (!DatabaseReady) { return false; } ObserveDatabase(Database); List<Plan> list = Prepare(catalog); if (!Object.op_Implicit((Object)(object)root)) { root = new GameObject("FoodKnowledge_Prefabs"); root.SetActive(false); ((Object)root).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)root); } Dictionary<string, RegisteredFood> staged = new Dictionary<string, RegisteredFood>(StringComparer.Ordinal); Dictionary<string, Recipe[]> dictionary = new Dictionary<string, Recipe[]>(StringComparer.Ordinal); List<Recipe> list2 = new List<Recipe>(); try { foreach (Plan item2 in list) { FoodDefinition definition = item2.Definition; RegisteredFood existing = item2.Existing; if (existing != null) { staged.Add(definition.Item, existing); } else if (!item2.Created) { staged.Add(definition.Item, new RegisteredFood { Item = definition.Item, UseModel = definition.Item, Prefab = ((Component)item2.Template).gameObject, Drop = item2.Template, Created = false, Baseline = GameApi.Clone(item2.Template.m_itemData.m_shared), OriginalRecipes = item2.Originals, Icons = (Sprite[])((item2.Template.m_itemData.m_shared.m_icons != null && item2.Template.m_itemData.m_shared.m_icons.Length != 0) ? ((Array)(Sprite[])item2.Template.m_itemData.m_shared.m_icons.Clone()) : ((Array)new Sprite[1])) }); } else { GameObject val = Object.Instantiate<GameObject>(((Component)item2.Template).gameObject, root.transform, false); ((Object)val).name = definition.Item; ((Object)val).hideFlags = (HideFlags)0; ItemDrop component = val.GetComponent<ItemDrop>(); component.m_itemData = item2.BaseFood.m_itemData.Clone(); component.m_itemData.m_stack = 1; component.m_itemData.m_quality = 1; component.m_itemData.m_variant = 0; component.m_itemData.m_shared = GameApi.Clone(item2.Values); component.m_itemData.m_dropPrefab = val; val.SetActive(true); staged.Add(definition.Item, new RegisteredFood { Item = definition.Item, UseModel = ((Object)((Component)item2.Template).gameObject).name, Prefab = val, Drop = component }); } } foreach (Plan item3 in list) { if (!item3.Delete) { FrameworkApi.ValidateFoodRegistration("dannanboi.foodknowledge", staged[item3.Definition.Item].Prefab, item3.Definition.ModifierSet, item3.Definition.Presentation); } } foreach (Plan item4 in list) { if (item4.Delete) { dictionary.Add(item4.Definition.Item, Array.Empty<Recipe>()); continue; } if (!item4.ChangeRecipe) { dictionary.Add(item4.Definition.Item, item4.Originals); continue; } List<Recipe> list3 = new List<Recipe>(); Recipe val2 = item4.Originals.FirstOrDefault(); CraftingStation[] stations = item4.Stations; foreach (CraftingStation val3 in stations) { Recipe val4 = (Object.op_Implicit((Object)(object)val2) ? Object.Instantiate<Recipe>(val2) : ScriptableObject.CreateInstance<Recipe>()); list3.Add(val4); list2.Add(val4); ((Object)val4).name = "FoodKnowledge_Recipe_" + item4.Definition.Item + "_" + (Object.op_Implicit((Object)(object)val3) ? ((Object)((Component)val3).gameObject).name : "Inventory"); val4.m_item = staged[item4.Definition.Item].Drop; val4.m_amount = item4.Amount; val4.m_enabled = item4.Created || !Object.op_Implicit((Object)(object)val2) || val2.m_enabled; val4.m_minStationLevel = ((!item4.Created && !item4.Definition.Overrides("stationLevel") && !item4.Definition.Has("craftingStation") && Object.op_Implicit((Object)(object)val2)) ? val2.m_minStationLevel : item4.Definition.StationLevel); val4.m_craftingStation = val3; if (item4.Created) { val4.m_qualityResultAmountMultiplier = 0f; val4.m_requireOnlyOneIngredient = false; } val4.m_resources = ((item4.Definition.Ingredients == null && Object.op_Implicit((Object)(object)val2)) ? val2.m_resources : ((IEnumerable<Ingredient>)item4.Ingredients).Select((Func<Ingredient, Requirement>)((Ingredient x) => new Requirement { m_resItem = (staged.ContainsKey(x.Item) ? staged[x.Item].Drop : Database.GetItemPrefab(x.Item).GetComponent<ItemDrop>()), m_amount = x.Amount, m_amountPerLevel = 0, m_recover = false })).ToArray()); } dictionary.Add(item4.Definition.Item, list3.ToArray()); } } catch { foreach (KeyValuePair<string, RegisteredFood> item5 in staged) { if (item5.Value.Created && Find(item5.Key) == null) { Object.Destroy((Object)(object)item5.Value.Prefab); } } foreach (Recipe item6 in list2) { Object.Destroy((Object)(object)item6); } throw; } if (!Application.isBatchMode && Object.op_Implicit((Object)(object)InventoryGui.instance)) { Recipe crafting = default(Recipe); ref Recipe reference = ref crafting; object? value = GameApi.CraftRecipe.GetValue(InventoryGui.instance); reference = (Recipe)((value is Recipe) ? value : null); if (IsOwned(crafting) || (Object.op_Implicit((Object)(object)crafting) && list.Any((Plan p) => p.ChangeRecipe && p.Originals.Contains(crafting)))) { GameApi.CancelCrafting.Invoke(InventoryGui.instance, null); } } foreach (List<Recipe> recipeList in recipeLists) { recipeList.RemoveAll(ownedRecipes.Contains); } foreach (Recipe ownedRecipe in ownedRecipes) { Object.Destroy((Object)(object)ownedRecipe); } FoodStats.PreserveNative(Player.m_localPlayer, staged.Values); ownedRecipes.Clear(); RestoreNative(); tokens.Clear(); foreach (RegisteredFood value2 in foods.Values) { value2.Active = false; value2.Deleted = false; value2.ChangedRecipe = false; value2.Recipes = (value2.Created ? Array.Empty<Recipe>() : value2.OriginalRecipes); } foreach (Plan item7 in list) { FoodDefinition definition2 = item7.Definition; RegisteredFood registeredFood = staged[definition2.Item]; SharedData shared = registeredFood.Drop.m_itemData.m_shared; ApplyValues(shared, item7.Values); if (registeredFood.Created || !item7.Delete) { shared.m_icons = registeredFood.Icons; if (!Object.op_Implicit((Object)(object)registeredFood.Icons[0])) { registeredFood.Icons[0] = (registeredFood.Created ? FallbackIcon() : registeredFood.NativeIcon); } } registeredFood.Title = item7.Title; registeredFood.Description = item7.Description; registeredFood.ModifierSet = (item7.Delete ? ModifierSet.Empty : definition2.ModifierSet); registeredFood.EffectInfo = definition2.EffectInfo; registeredFood.Presentation = definition2.Presentation; registeredFood.Recipes = dictionary[definition2.Item]; registeredFood.Active = !item7.Delete; registeredFood.Deleted = item7.Delete; registeredFood.ChangedRecipe = item7.ChangeRecipe; foods[definition2.Item] = registeredFood; if (item7.ChangeRecipe) { foreach (List<Recipe> recipeList2 in recipeLists) { recipeList2.RemoveAll(((IEnumerable<Recipe>)registeredFood.OriginalRecipes).Contains<Recipe>); } } if (item7.Created || item7.ChangeRecipe) { Recipe[] recipes = registeredFood.Recipes; foreach (Recipe item in recipes) { ownedRecipes.Add(item); } } } foreach (RegisteredFood value3 in foods.Values) { tokens[value3.Drop.m_itemData.m_shared.m_name] = value3; } RegisterDatabase(Database); RegisterScene(Scene, SceneAwaking); Localize(Localization.instance); RefreshIcons(); foreach (RegisteredFood value4 in foods.Values) { FrameworkApi.RegisterFood("dannanboi.foodknowledge", value4.Prefab, value4.ModifierSet, value4.Presentation); } LiveItemsChanged(); return true; } private Sprite FallbackIcon() { GameObject val = (Object.op_Implicit((Object)(object)Database) ? Database.GetItemPrefab("RawMeat") : null); Sprite[] array = Find("RawMeat")?.Baseline?.m_icons ?? ((!Object.op_Implicit((Object)(object)val)) ? null : val.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_icons); return (array != null && array.Length != 0) ? array[0] : null; } internal bool RefreshIcons() { Sprite val = FallbackIcon(); if (!Object.op_Implicit((Object)(object)val)) { return false; } if (Icons != null) { return Icons.Refresh(foods.Values.Where((RegisteredFood f) => f.Created || f.Active), val); } bool result = false; foreach (RegisteredFood value in foods.Values) { if (value.Created || value.Active) { Sprite val2 = (value.Created ? val : value.NativeIcon); if (value.Icons[0] != val2) { value.Icons[0] = val2; result = true; } } } return result; } private void ObserveDatabase(ObjectDB db) { if (Object.op_Implicit((Object)(object)db)) { if (db.m_items != null) { itemLists.Add(db.m_items); } if (db.m_recipes != null) { recipeLists.Add(db.m_recipes); } if (GameApi.ItemHashes.GetValue(db) is Dictionary<int, GameObject> item) { itemHashes.Add(item); } if (GameApi.ItemDataIndex?.GetValue(db) is Dictionary<SharedData, GameObject> item2) { itemData.Add(item2); } } } internal void PrepareDatabaseRebuild(ObjectDB db) { if (Object.op_Implicit((Object)(object)db)) { ObserveDatabase(db); db.m_items?.RemoveAll((GameObject prefab) => !Object.op_Implicit((Object)(object)prefab)); db.m_recipes?.RemoveAll((Recipe recipe) => !Object.op_Implicit((Object)(object)recipe)); } } internal void PrepareDatabaseCopy(ObjectDB destination, ObjectDB source) { ObserveDatabase(destination); PrepareDatabaseRebuild(source); } internal void RegisterDatabase(ObjectDB db) { if (!Object.op_Implicit((Object)(object)db) || db.m_items == null) { return; } Database = db; ObserveDatabase(db); Dictionary<int, GameObject> dictionary = (Dictionary<int, GameObject>)GameApi.ItemHashes.GetValue(db); Dictionary<SharedData, GameObject> dictionary2 = GameApi.ItemDataIndex?.GetValue(db) as Dictionary<SharedData, GameObject>; foreach (RegisteredFood value2 in foods.Values) { int key = Catalog.StableHash(value2.Item); if (dictionary.TryGetValue(key, out var value) && (Object)(object)value != (Object)(object)value2.Prefab) { throw new FormatException("Prefab registration collision: " + value2.Item); } if (!db.m_items.Contains(value2.Prefab)) { db.m_items.Add(value2.Prefab); } dictionary[key] = value2.Prefab; if (dictionary2 != null) { dictionary2[value2.Drop.m_itemData.m_shared] = value2.Prefab; } if (value2.ChangedRecipe) { db.m_recipes.RemoveAll(((IEnumerable<Recipe>)value2.OriginalRecipes).Contains<Recipe>); } Recipe[] recipes = value2.Recipes; foreach (Recipe val in recipes) { if (Object.op_Implicit((Object)(object)val) && !db.m_recipes.Contains(val)) { db.m_recipes.Add(val); } } } } internal void RegisterScene(ZNetScene scene, bool beforeAwake) { if (!Object.op_Implicit((Object)(object)scene) || scene.m_prefabs == null) { return; } Scene = scene; Dictionary<int, GameObject> dictionary = (Dictionary<int, GameObject>)GameApi.SceneHashes.GetValue(scene); sceneLists.Add(scene.m_prefabs); if (dictionary != null) { sceneHashes.Add(dictionary); } foreach (RegisteredFood item in foods.Values.Where((RegisteredFood f) => f.Created)) { int key = Catalog.StableHash(item.Item); if (!beforeAwake && dictionary.TryGetValue(key, out var value) && (Object)(object)value != (Object)(object)item.Prefab) { throw new FormatException("Network prefab registration collision: " + item.Item); } if (!scene.m_prefabs.Contains(item.Prefab)) { scene.m_prefabs.Add(item.Prefab); } if (!beforeAwake) { dictionary[key] = item.Prefab; } } } internal void Localize(Localization locale) { if (locale == null) { return; } RestoreWords(locale); foreach (RegisteredFood item in foods.Values.Where((RegisteredFood f) => f.Created || f.Active)) { if (item.Title != null) { AddWord(locale, item.Drop.m_itemData.m_shared.m_name, item.Title, !item.Created); } if (item.Description != null) { AddWord(locale, item.Drop.m_itemData.m_shared.m_description, item.Description, !item.Created); } } object obj = GameApi.LocaleCache?.GetValue(locale); if (obj != null) { GameApi.EvictLocale?.Invoke(obj, null); } } internal void ClearSession() { FrameworkApi.UnregisterFoods("dannanboi.foodknowledge"); RestoreNative(); nativeWords.Clear(); LiveItemsChanged(); HashSet<GameObject> prefabs = new HashSet<GameObject>(from food in foods.Values where food.Created select food.Prefab, References<GameObject>.Instance); foreach (List<GameObject> itemList in itemLists) { itemList.RemoveAll(prefabs.Contains); } foreach (List<Recipe> recipeList in recipeLists) { recipeList.RemoveAll(ownedRecipes.Contains); } foreach (Dictionary<int, GameObject> itemHash in itemHashes) { int[] array = (from pair in itemHash where prefabs.Contains(pair.Value) select pair.Key).ToArray(); foreach (int key in array) { itemHash.Remove(key); } } foreach (Dictionary<SharedData, GameObject> itemDatum in itemData) { SharedData[] array2 = (from pair in itemDatum where prefabs.Contains(pair.Value) select pair.Key).ToArray(); foreach (SharedData key2 in array2) { itemDatum.Remove(key2); } } foreach (List<GameObject> sceneList in sceneLists) { sceneList.RemoveAll(prefabs.Contains); } foreach (Dictionary<int, GameObject> sceneHash in sceneHashes) { int[] array3 = (from pair in sceneHash where prefabs.Contains(pair.Value) select pair.Key).ToArray(); foreach (int key3 in array3) { sceneHash.Remove(key3); } } foreach (RegisteredFood value in foods.Values) { if (value.Created) { Object.Destroy((Object)(object)value.Prefab); } } foreach (Recipe ownedRecipe in ownedRecipes) { Object.Destroy((Object)(object)ownedRecipe); } ownedRecipes.Clear(); foods.Clear(); tokens.Clear(); Scene = null; SceneAwaking = false; itemLists.Clear(); recipeLists.Clear(); itemHashes.Clear(); itemData.Clear(); sceneLists.Clear(); sceneHashes.Clear(); ObserveDatabase(Database); } internal void RefreshItem(ItemData item) { SharedData val = Find(item)?.Drop?.m_itemData?.m_shared; if (val != null && val != item.m_shared) { ApplyValues(item.m_shared, val); item.m_shared.m_icons = val.m_icons; } } internal void RefreshInventory(Inventory inventory, bool force = false, bool notify = true) { if (Application.isBatchMode || inventory == null || foods.Count == 0) { return; } InventoryRevision orCreateValue = refreshedInventories.GetOrCreateValue(inventory); if (!force && orCreateValue.Value == itemRevision) { return; } orCreateValue.Value = itemRevision; bool flag = false; foreach (ItemData allItem in inventory.GetAllItems()) { if (Find(allItem) != null) { flag = true; RefreshItem(allItem); } } if (!(notify && flag)) { return; } try { GameApi.InventoryChanged.Invoke(inventory, new object[2] { false, false }); } catch (Exception ex) { if (!inventoryWarning)