using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BreathOfTheCauldron.Cooking; using BreathOfTheCauldron.Cooking.Definitions; using BreathOfTheCauldron.Cooking.Registration; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BreathOfTheCauldron")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0")] [assembly: AssemblyProduct("BreathOfTheCauldron")] [assembly: AssemblyTitle("BreathOfTheCauldron")] [assembly: AssemblyVersion("0.1.0.0")] [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 BreathOfTheCauldron { internal static class ModLocalization { private const string TranslationsFolderName = "Translations"; private static readonly string[] SupportedLanguages = new string[2] { "English", "Russian" }; private static bool _registered; internal static void Register() { if (_registered) { return; } try { CustomLocalization localization = LocalizationManager.Instance.GetLocalization(); string[] supportedLanguages = SupportedLanguages; foreach (string language in supportedLanguages) { LoadLanguage(localization, language); } _registered = true; Plugin.Log.LogInfo((object)"BOTC localization registered."); } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to register BOTC localization:\n" + ex)); } } private static void LoadLanguage(CustomLocalization localization, string language) { string text = Path.Combine(GetModDirectory(), "Translations", language + ".json"); if (!File.Exists(text)) { Plugin.Log.LogWarning((object)("BOTC localization file was not found: '" + text + "'.")); return; } string text2 = File.ReadAllText(text); localization.AddJsonFile(language, text2); Plugin.Log.LogInfo((object)("Loaded BOTC localization: " + language + ".")); } private static string GetModDirectory() { string? directoryName = Path.GetDirectoryName(typeof(Plugin).Assembly.Location); if (string.IsNullOrWhiteSpace(directoryName)) { throw new InvalidOperationException("Could not determine the BOTC plugin directory."); } return directoryName; } } [BepInPlugin("sunray.valheim.breathofthecauldron", "Breath of the Cauldron", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "sunray.valheim.breathofthecauldron"; public const string PluginName = "Breath of the Cauldron"; public const string PluginVersion = "0.1.0"; private Harmony? _harmony; internal static ManualLogSource Log { get; private set; } internal static ConfigEntry AllowMultipleVariantsOfSameFood { get; private set; } private void Awake() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; RegisterConfig(); _harmony = new Harmony("sunray.valheim.breathofthecauldron"); _harmony.PatchAll(); PrefabManager.OnVanillaPrefabsAvailable += OnVanillaPrefabsAvailable; Log.LogInfo((object)"Breath of the Cauldron 0.1.0 loaded."); } private void OnDestroy() { PrefabManager.OnVanillaPrefabsAvailable -= OnVanillaPrefabsAvailable; Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void RegisterConfig() { AllowMultipleVariantsOfSameFood = ((BaseUnityPlugin)this).Config.Bind("Food", "Allow Multiple Variants Of Same Food", false, "If enabled, the original, seasoned, and perfect versions of the same base food can be eaten at the same time. Requires a game restart."); } private static void OnVanillaPrefabsAvailable() { ModLocalization.Register(); FoodManager.Register(); PrefabManager.OnVanillaPrefabsAvailable -= OnVanillaPrefabsAvailable; } } } namespace BreathOfTheCauldron.Cooking { internal static class CauldronFoodUpgradeContext { private static readonly Dictionary BaseFoodNameByResultName = new Dictionary(StringComparer.Ordinal); internal static bool IsBuildingUpgradeList { get; private set; } internal static void Begin() { BaseFoodNameByResultName.Clear(); IsBuildingUpgradeList = true; } internal static void AddMapping(string resultSharedName, string baseFoodSharedName) { if (!string.IsNullOrWhiteSpace(resultSharedName) && !string.IsNullOrWhiteSpace(baseFoodSharedName)) { BaseFoodNameByResultName[resultSharedName] = baseFoodSharedName; } } internal static bool TryGetBaseFoodSharedName(string requestedSharedName, out string baseFoodSharedName) { if (!IsBuildingUpgradeList) { baseFoodSharedName = string.Empty; return false; } return BaseFoodNameByResultName.TryGetValue(requestedSharedName, out baseFoodSharedName); } internal static void End() { IsBuildingUpgradeList = false; BaseFoodNameByResultName.Clear(); } } [HarmonyPatch(typeof(InventoryGui), "UpdateRecipeList", new Type[] { typeof(List) })] internal static class InventoryGuiUpdateRecipeListPatch { private sealed class PatchState { internal readonly List AddedRecipes = new List(); internal bool ContextStarted; } private const string VanillaCauldronPrefabName = "piece_cauldron"; private static void Prefix(InventoryGui __instance, List recipes, out PatchState __state) { __state = new PatchState(); if (recipes == null || __instance.InCraftTab() || !IsCauldronRecipeList(recipes)) { return; } CauldronFoodUpgradeContext.Begin(); __state.ContextStarted = true; foreach (FoodUpgradeDefinition item2 in FoodUpgradeRegistry.GetAll()) { Recipe runtimeRecipe = item2.RuntimeRecipe; if ((Object)(object)runtimeRecipe == (Object)null) { continue; } GameObject prefab = PrefabManager.Instance.GetPrefab(item2.BaseFoodPrefabName); ItemDrop val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent() : null); ItemDrop item = runtimeRecipe.m_item; if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("BOTC could not inject upgrade '" + item2.Id + "': base food prefab '" + item2.BaseFoodPrefabName + "' was not found.")); continue; } if ((Object)(object)item == (Object)null) { Plugin.Log.LogWarning((object)("BOTC could not inject upgrade '" + item2.Id + "': runtime recipe has no result item.")); continue; } string name = val.m_itemData.m_shared.m_name; CauldronFoodUpgradeContext.AddMapping(item.m_itemData.m_shared.m_name, name); if (!recipes.Contains(runtimeRecipe)) { recipes.Add(runtimeRecipe); __state.AddedRecipes.Add(runtimeRecipe); Plugin.Log.LogDebug((object)("Injected BOTC upgrade recipe '" + item2.Id + "' into the cauldron Upgrade list.")); } } } private static void Postfix(List recipes, PatchState __state) { Cleanup(recipes, __state); } private static Exception? Finalizer(Exception? __exception, List recipes, PatchState __state) { Cleanup(recipes, __state); return __exception; } private static void Cleanup(List recipes, PatchState? state) { if (state == null) { return; } if (recipes != null) { foreach (Recipe addedRecipe in state.AddedRecipes) { recipes.Remove(addedRecipe); } state.AddedRecipes.Clear(); } if (state.ContextStarted) { CauldronFoodUpgradeContext.End(); state.ContextStarted = false; } } private static bool IsCauldronRecipeList(List recipes) { foreach (Recipe recipe in recipes) { CraftingStation val = recipe?.m_craftingStation; if (!((Object)(object)val == (Object)null)) { string text = ((Object)((Component)val).gameObject).name; if (text.EndsWith("(Clone)", StringComparison.Ordinal)) { text = text.Substring(0, text.Length - "(Clone)".Length).TrimEnd(); } if (string.Equals(text, "piece_cauldron", StringComparison.Ordinal)) { return true; } } } return false; } } [HarmonyPatch(typeof(Inventory), "GetAllItems", new Type[] { typeof(string), typeof(List) })] internal static class InventoryGetAllItemsPatch { private static void Prefix(ref string __0, out bool __state) { __state = false; if (!string.IsNullOrWhiteSpace(__0) && CauldronFoodUpgradeContext.TryGetBaseFoodSharedName(__0, out string baseFoodSharedName)) { Plugin.Log.LogDebug((object)("BOTC redirected inventory lookup from '" + __0 + "' to base food '" + baseFoodSharedName + "'.")); __0 = baseFoodSharedName; __state = true; } } private static void Postfix(List __1, bool __state) { if (__state && __1 != null && __1.Count > 1) { int count = __1.Count; __1.RemoveRange(1, __1.Count - 1); Plugin.Log.LogDebug((object)("BOTC reduced base food candidates " + $"from {count} to 1.")); } } } [HarmonyPatch(typeof(InventoryGui), "DoCrafting", new Type[] { typeof(Player) })] internal static class InventoryGuiDoCraftingPatch { private static readonly FieldInfo? CraftRecipeField = AccessTools.Field(typeof(InventoryGui), "m_craftRecipe"); private static readonly FieldInfo? CraftUpgradeItemField = AccessTools.Field(typeof(InventoryGui), "m_craftUpgradeItem"); private static readonly FieldInfo? CraftItemDoneEffectsField = AccessTools.Field(typeof(CraftingStation), "m_craftItemDoneEffects"); private static readonly MethodInfo? GetCurrentCraftingStationMethod = AccessTools.Method(typeof(Player), "GetCurrentCraftingStation", Type.EmptyTypes, (Type[])null); private static bool Prefix(InventoryGui __instance, Player player) { //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Unknown result type (might be due to invalid IL or missing references) //IL_022f: Unknown result type (might be due to invalid IL or missing references) //IL_0234: Unknown result type (might be due to invalid IL or missing references) //IL_0239: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return true; } if (CraftRecipeField == null) { Plugin.Log.LogError((object)"BOTC could not find InventoryGui.m_craftRecipe."); return true; } if (CraftUpgradeItemField == null) { Plugin.Log.LogError((object)"BOTC could not find InventoryGui.m_craftUpgradeItem."); return true; } object? value = CraftRecipeField.GetValue(__instance); Recipe val = (Recipe)((value is Recipe) ? value : null); if ((Object)(object)val == (Object)null) { return true; } FoodUpgradeDefinition foodUpgradeDefinition = FoodUpgradeRegistry.FindByRecipe(val); if (foodUpgradeDefinition == null) { return true; } object? value2 = CraftUpgradeItemField.GetValue(__instance); ItemData val2 = (ItemData)((value2 is ItemData) ? value2 : null); if (val2 == null) { AbortCraft(__instance, player, foodUpgradeDefinition, "selected base food is missing"); return false; } Inventory inventory = ((Humanoid)player).GetInventory(); if (!inventory.ContainsItem(val2)) { AbortCraft(__instance, player, foodUpgradeDefinition, "selected base food is no longer in inventory"); return false; } int num = val2.m_quality + 1; int num2 = default(int); ItemData val3 = default(ItemData); val.GetAmount(num, ref num2, ref val3, 1); if (val3 == null || num2 <= 0) { AbortCraft(__instance, player, foodUpgradeDefinition, "no suitable seasoning was selected"); return false; } if (!inventory.ContainsItem(val3)) { AbortCraft(__instance, player, foodUpgradeDefinition, "selected seasoning is no longer in inventory"); return false; } string prefabName = GetPrefabName(val2); string prefabName2 = GetPrefabName(val3); bool flag = foodUpgradeDefinition.HasPerfect && string.Equals(prefabName2, foodUpgradeDefinition.PerfectIngredientPrefabName, StringComparison.Ordinal); string text; if (flag) { if (string.IsNullOrWhiteSpace(foodUpgradeDefinition.PerfectResultPrefabName)) { AbortCraft(__instance, player, foodUpgradeDefinition, "perfect result prefab is missing"); return false; } text = foodUpgradeDefinition.PerfectResultPrefabName; } else { text = foodUpgradeDefinition.NormalResultPrefabName; } GameObject prefab = PrefabManager.Instance.GetPrefab(text); ItemDrop val4 = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent() : null); if ((Object)(object)prefab == (Object)null || (Object)(object)val4 == (Object)null) { Plugin.Log.LogError((object)("BOTC result prefab '" + text + "' was not found or has no ItemDrop component.")); ResetCraftFields(__instance); return false; } ItemData val5 = val4.m_itemData.Clone(); val5.m_stack = 1; val5.m_quality = 1; val5.m_variant = 0; val5.m_dropPrefab = prefab; inventory.RemoveItem(val2, 1); inventory.RemoveItem(val3.m_shared.m_name, num2, val3.m_quality, true); if (!inventory.AddItem(val5)) { ItemDrop.DropItem(val5, 1, ((Component)player).transform.position + ((Component)player).transform.forward + Vector3.up, Quaternion.identity); Plugin.Log.LogWarning((object)("BOTC crafted '" + text + "', but could not place it in inventory. The result was dropped near the player.")); } Plugin.Log.LogInfo((object)("BOTC crafted upgrade '" + foodUpgradeDefinition.Id + "': base='" + prefabName + "', ingredient='" + prefabName2 + "', " + $"amount={num2}, " + $"perfect={flag}, " + "result='" + text + "'.")); PlayCraftCompletionEffects(player, val); string text2 = ((Localization.instance != null) ? Localization.instance.Localize(val5.m_shared.m_name) : val5.m_shared.m_name); ((Character)player).Message((MessageType)2, text2, 0, (Sprite)null); ResetCraftFields(__instance); return false; } private static void AbortCraft(InventoryGui inventoryGui, Player player, FoodUpgradeDefinition definition, string reason) { Plugin.Log.LogWarning((object)("BOTC upgrade '" + definition.Id + "' aborted: " + reason + ".")); ((Character)player).Message((MessageType)2, "$msg_missingrequirement", 0, (Sprite)null); ResetCraftFields(inventoryGui); } private static void ResetCraftFields(InventoryGui inventoryGui) { CraftRecipeField?.SetValue(inventoryGui, null); CraftUpgradeItemField?.SetValue(inventoryGui, null); } private static string GetPrefabName(ItemData item) { if ((Object)(object)item.m_dropPrefab != (Object)null) { return ((Object)item.m_dropPrefab).name; } return item.m_shared.m_name; } private static void PlayCraftCompletionEffects(Player player, Recipe recipe) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) CraftingStation val = TryGetCurrentCraftingStation(player); if (val == null) { val = recipe.m_craftingStation; } if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"BOTC could not play craft completion SFX: crafting station was not found."); return; } if (CraftItemDoneEffectsField == null) { Plugin.Log.LogWarning((object)"BOTC could not play craft completion SFX: CraftingStation.m_craftItemDoneEffects was not found."); return; } object value = CraftItemDoneEffectsField.GetValue(val); if (value == null) { Plugin.Log.LogWarning((object)"BOTC could not play craft completion SFX: the station has no craft completion effects."); return; } Scene scene = ((Component)val).gameObject.scene; bool num = ((Scene)(ref scene)).IsValid(); Vector3 position = (num ? ((Component)val).transform.position : ((Component)player).transform.position); Quaternion rotation = (num ? ((Component)val).transform.rotation : ((Component)player).transform.rotation); Transform parent = (num ? ((Component)val).transform : ((Component)player).transform); if (!TryCreateEffects(value, position, rotation, parent)) { Plugin.Log.LogWarning((object)"BOTC could not invoke the cauldron craft completion effect."); } else { Plugin.Log.LogDebug((object)"BOTC played cauldron craft completion effects."); } } private static CraftingStation? TryGetCurrentCraftingStation(Player player) { if (GetCurrentCraftingStationMethod == null) { Plugin.Log.LogDebug((object)"BOTC could not find Player.GetCurrentCraftingStation()."); return null; } try { object? obj = GetCurrentCraftingStationMethod.Invoke(player, null); return (CraftingStation?)((obj is CraftingStation) ? obj : null); } catch (Exception ex) { Plugin.Log.LogDebug((object)("BOTC could not get current crafting station: " + ex.Message)); return null; } } private static bool TryCreateEffects(object effectList, Vector3 position, Quaternion rotation, Transform parent) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) MethodInfo[] methods = effectList.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "Create", StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); object[] array = new object[parameters.Length]; bool flag = true; for (int j = 0; j < parameters.Length; j++) { ParameterInfo parameterInfo = parameters[j]; Type parameterType = parameterInfo.ParameterType; if (parameterType == typeof(Vector3)) { array[j] = position; continue; } if (parameterType == typeof(Quaternion)) { array[j] = rotation; continue; } if (parameterType == typeof(Transform)) { array[j] = parent; continue; } if (parameterType == typeof(float)) { array[j] = 1f; continue; } if (parameterType == typeof(int)) { array[j] = -1; continue; } if (parameterType == typeof(bool)) { array[j] = false; continue; } if (parameterInfo.HasDefaultValue) { array[j] = parameterInfo.DefaultValue; continue; } flag = false; break; } if (flag) { try { methodInfo.Invoke(effectList, array); return true; } catch (Exception ex) { Plugin.Log.LogDebug((object)("BOTC could not invoke effect method " + $"'{methodInfo}': {ex.Message}")); } } } return false; } } internal static class CauldronFoodUpgradeUi { private const string CauldronPrefabName = "piece_cauldron"; private const string SeasonToken = "$botc_season"; private const string SeasoningToken = "$botc_seasoning"; private static readonly FieldInfo? TabUpgradeField = AccessTools.Field(typeof(InventoryGui), "m_tabUpgrade"); private static readonly FieldInfo? ItemCraftTypeField = AccessTools.Field(typeof(InventoryGui), "m_itemCraftType"); private static readonly FieldInfo? CraftButtonField = AccessTools.Field(typeof(InventoryGui), "m_craftButton"); private static readonly FieldInfo? CraftProgressPanelField = AccessTools.Field(typeof(InventoryGui), "m_craftProgressPanel"); private static string? _originalCraftButtonText; private static string? _originalProgressText; internal static bool IsCauldronUpgradeUi; internal static void UpdateStation(InventoryGui inventoryGui, List recipes) { IsCauldronUpgradeUi = ContainsCauldronRecipe(recipes); SetUpgradeTabText(inventoryGui, IsCauldronUpgradeUi); if (!IsCauldronUpgradeUi) { RestoreActionTexts(inventoryGui); } } internal static void UpdateRecipeDetails(InventoryGui inventoryGui) { if (IsCauldronUpgradeUi) { if (inventoryGui.InCraftTab()) { RestoreActionTexts(inventoryGui); return; } ClearUpgradeStatusText(inventoryGui); SetSeasonButtonText(inventoryGui); SetSeasoningProgressText(inventoryGui); } } private static void SetUpgradeTabText(InventoryGui inventoryGui, bool seasonMode) { object? obj = TabUpgradeField?.GetValue(inventoryGui); Button val = (Button)((obj is Button) ? obj : null); if (!((Object)(object)val == (Object)null)) { string value = (seasonMode ? GetLocalizedText("$botc_season", "Season").ToUpperInvariant() : GetVanillaUpgradeText()); SetChildText(((Component)val).transform, "Text", value); SetChildText(((Component)val).transform, "Selected/Text (1)", value); } } private static void ClearUpgradeStatusText(InventoryGui inventoryGui) { object? obj = ItemCraftTypeField?.GetValue(inventoryGui); TMP_Text val = (TMP_Text)((obj is TMP_Text) ? obj : null); if (!((Object)(object)val == (Object)null)) { SetTextIfChanged(val, string.Empty); } } private static void SetSeasonButtonText(InventoryGui inventoryGui) { TMP_Text craftButtonText = GetCraftButtonText(inventoryGui); if (!((Object)(object)craftButtonText == (Object)null)) { if (_originalCraftButtonText == null) { _originalCraftButtonText = craftButtonText.text; } string localizedText = GetLocalizedText("$botc_season", "Season"); SetTextIfChanged(craftButtonText, localizedText); } } private static void SetSeasoningProgressText(InventoryGui inventoryGui) { TMP_Text craftProgressText = GetCraftProgressText(inventoryGui); if (!((Object)(object)craftProgressText == (Object)null)) { if (_originalProgressText == null) { _originalProgressText = craftProgressText.text; } string localizedText = GetLocalizedText("$botc_seasoning", "Seasoning"); SetTextIfChanged(craftProgressText, localizedText); } } private static void RestoreActionTexts(InventoryGui inventoryGui) { TMP_Text craftButtonText = GetCraftButtonText(inventoryGui); if ((Object)(object)craftButtonText != (Object)null && !string.IsNullOrWhiteSpace(_originalCraftButtonText)) { SetTextIfChanged(craftButtonText, _originalCraftButtonText); } TMP_Text craftProgressText = GetCraftProgressText(inventoryGui); if ((Object)(object)craftProgressText != (Object)null && !string.IsNullOrWhiteSpace(_originalProgressText)) { SetTextIfChanged(craftProgressText, _originalProgressText); } } private static TMP_Text? GetCraftButtonText(InventoryGui inventoryGui) { object? obj = CraftButtonField?.GetValue(inventoryGui); Button val = (Button)((obj is Button) ? obj : null); if ((Object)(object)val == (Object)null) { return null; } Transform val2 = ((Component)val).transform.Find("Text"); if ((Object)(object)val2 == (Object)null) { return null; } return ((Component)val2).GetComponent(); } private static TMP_Text? GetCraftProgressText(InventoryGui inventoryGui) { object? obj = CraftProgressPanelField?.GetValue(inventoryGui); Transform val = (Transform)((obj is Transform) ? obj : null); if ((Object)(object)val == (Object)null) { return null; } Transform val2 = val.Find("Text"); if ((Object)(object)val2 == (Object)null) { return null; } return ((Component)val2).GetComponent(); } private static void SetChildText(Transform root, string childPath, string value) { Transform val = root.Find(childPath); if (!((Object)(object)val == (Object)null)) { SetTextIfChanged(((Component)val).GetComponent(), value); } } private static void SetTextIfChanged(TMP_Text? text, string value) { if (!((Object)(object)text == (Object)null) && !string.Equals(text.text, value, StringComparison.Ordinal)) { text.text = value; } } private static string GetLocalizedText(string token, string fallback) { if (Localization.instance == null) { return fallback; } string text = Localization.instance.Localize(token); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, token, StringComparison.Ordinal)) { return fallback; } return text; } private static string GetVanillaUpgradeText() { if (Localization.instance == null) { return "UPGRADE"; } string text = Localization.instance.Localize("$inventory_upgrade"); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, "$inventory_upgrade", StringComparison.Ordinal)) { return "UPGRADE"; } return text.ToUpperInvariant(); } private static bool ContainsCauldronRecipe(List recipes) { if (recipes == null) { return false; } foreach (Recipe recipe in recipes) { if (!((Object)(object)recipe == (Object)null) && !((Object)(object)recipe.m_craftingStation == (Object)null) && string.Equals(NormalizeName(((Object)((Component)recipe.m_craftingStation).gameObject).name), "piece_cauldron", StringComparison.Ordinal)) { return true; } } return false; } private static string NormalizeName(string objectName) { if (objectName.EndsWith("(Clone)", StringComparison.Ordinal)) { return objectName.Substring(0, objectName.Length - "(Clone)".Length).TrimEnd(); } return objectName; } } [HarmonyPatch(typeof(InventoryGui), "UpdateRecipeList", new Type[] { typeof(List) })] [HarmonyPriority(0)] internal static class CauldronUpgradeTabTextPatch { private static void Postfix(InventoryGui __instance, List recipes) { CauldronFoodUpgradeUi.UpdateStation(__instance, recipes); } } [HarmonyPatch(typeof(InventoryGui), "UpdateRecipe", new Type[] { typeof(Player), typeof(float) })] [HarmonyPriority(0)] internal static class CauldronUpgradeDetailsPatch { private static void Postfix(InventoryGui __instance) { CauldronFoodUpgradeUi.UpdateRecipeDetails(__instance); } } [HarmonyPatch(typeof(InventoryGui), "UpdateRecipeList", new Type[] { typeof(List) })] [HarmonyPriority(0)] internal static class CauldronSeasonRecipeIconPatch { private const string CauldronPrefabName = "piece_cauldron"; private static readonly FieldInfo? RecipeListRootField = AccessTools.Field(typeof(InventoryGui), "m_recipeListRoot"); private static void Postfix(InventoryGui __instance, List recipes) { if (!((Object)(object)__instance == (Object)null) && recipes != null && !__instance.InCraftTab() && ContainsCauldronRecipe(recipes)) { object? obj = RecipeListRootField?.GetValue(__instance); RectTransform val = (RectTransform)((obj is RectTransform) ? obj : null); if (!((Object)(object)val == (Object)null)) { RestoreRecipeIconColors(val); } } } private static void RestoreRecipeIconColors(RectTransform recipeListRoot) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: 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) for (int i = 0; i < ((Transform)recipeListRoot).childCount; i++) { Transform child = ((Transform)recipeListRoot).GetChild(i); if ((Object)(object)child == (Object)null || !((Component)child).gameObject.activeSelf) { continue; } Transform val = child.Find("icon"); if (!((Object)(object)val == (Object)null)) { Image component = ((Component)val).GetComponent(); if (!((Object)(object)component == (Object)null) && ((Graphic)component).color != Color.white) { ((Graphic)component).color = Color.white; } } } } private static bool ContainsCauldronRecipe(List recipes) { foreach (Recipe recipe in recipes) { if (!((Object)(object)recipe == (Object)null) && !((Object)(object)recipe.m_craftingStation == (Object)null) && string.Equals(NormalizeName(((Object)((Component)recipe.m_craftingStation).gameObject).name), "piece_cauldron", StringComparison.Ordinal)) { return true; } } return false; } private static string NormalizeName(string objectName) { if (objectName.EndsWith("(Clone)", StringComparison.Ordinal)) { return objectName.Substring(0, objectName.Length - "(Clone)".Length).TrimEnd(); } return objectName; } } [HarmonyPatch(typeof(Player), "CanEat", new Type[] { typeof(ItemData), typeof(bool) })] internal static class FoodFamilyPatch { private static bool Prefix(Player __instance, ItemData item, bool showMessages, ref bool __result) { if (Plugin.AllowMultipleVariantsOfSameFood.Value) { return true; } if (item == null || (Object)(object)item.m_dropPrefab == (Object)null) { return true; } string name = ((Object)item.m_dropPrefab).name; if (!FoodFamilyRegistry.TryGetFamilyId(name, out string familyId)) { return true; } foreach (Food food in __instance.GetFoods()) { if (food != null && !string.IsNullOrWhiteSpace(food.m_name) && !(food.m_name == name) && FoodFamilyRegistry.TryGetFamilyId(food.m_name, out string familyId2) && !(familyId2 != familyId)) { if (showMessages) { ((Character)__instance).Message((MessageType)2, Localization.instance.Localize("$msg_nomore", new string[1] { item.m_shared.m_name }), 0, (Sprite)null); } Plugin.Log.LogDebug((object)("Blocked eating '" + name + "': active food '" + food.m_name + "' belongs to the same family '" + familyId + "'.")); __result = false; return false; } } return true; } } internal static class FoodFamilyRegistry { private static readonly Dictionary FamilyByPrefabName = new Dictionary(StringComparer.Ordinal); internal static void RegisterFamily(string familyId, params string[] prefabNames) { if (string.IsNullOrWhiteSpace(familyId)) { throw new ArgumentException("Food family ID cannot be empty.", "familyId"); } if (prefabNames == null || prefabNames.Length == 0) { throw new ArgumentException("Food family '" + familyId + "' has no items.", "prefabNames"); } foreach (string text in prefabNames) { if (string.IsNullOrWhiteSpace(text)) { continue; } if (FamilyByPrefabName.TryGetValue(text, out string value)) { if (!string.Equals(value, familyId, StringComparison.Ordinal)) { throw new InvalidOperationException("Food prefab '" + text + "' is already registered in family '" + value + "'."); } } else { FamilyByPrefabName.Add(text, familyId); } } Plugin.Log.LogInfo((object)("Registered food family '" + familyId + "': " + string.Join(", ", prefabNames))); } internal static bool TryGetFamilyId(string prefabName, out string familyId) { if (string.IsNullOrWhiteSpace(prefabName)) { familyId = string.Empty; return false; } return FamilyByPrefabName.TryGetValue(prefabName, out familyId); } internal static bool AreInSameFamily(string firstPrefabName, string secondPrefabName) { if (!TryGetFamilyId(firstPrefabName, out string familyId)) { return false; } if (!TryGetFamilyId(secondPrefabName, out string familyId2)) { return false; } return string.Equals(familyId, familyId2, StringComparison.Ordinal); } } internal static class FoodManager { private static bool _registered; internal static void Register() { if (_registered) { return; } try { IngredientRegistry.Load(); IReadOnlyList readOnlyList = FoodDefinitionLoader.Load(); foreach (FoodDefinition item in readOnlyList) { FoodContentRegistrar.Register(item); } _registered = true; Plugin.Log.LogInfo((object)$"Registered {readOnlyList.Count} BOTC base foods."); } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to register BOTC food content:\n" + ex)); } } } internal sealed class FoodUpgradeDefinition { internal string Id { get; } internal string BaseFoodPrefabName { get; } internal IngredientClass IngredientClass { get; } internal string? PerfectIngredientPrefabName { get; } internal string NormalResultPrefabName { get; } internal string? PerfectResultPrefabName { get; } internal Recipe RuntimeRecipe { get; } internal bool HasPerfect { get { if (!string.IsNullOrWhiteSpace(PerfectIngredientPrefabName)) { return !string.IsNullOrWhiteSpace(PerfectResultPrefabName); } return false; } } internal FoodUpgradeDefinition(string id, string baseFoodPrefabName, IngredientClass ingredientClass, string? perfectIngredientPrefabName, string normalResultPrefabName, string? perfectResultPrefabName, Recipe runtimeRecipe) { if ((Object)(object)runtimeRecipe == (Object)null) { throw new ArgumentNullException("runtimeRecipe"); } bool flag = !string.IsNullOrWhiteSpace(perfectIngredientPrefabName); bool flag2 = !string.IsNullOrWhiteSpace(perfectResultPrefabName); if (flag != flag2) { throw new ArgumentException("Perfect ingredient and perfect result must either both be defined or both be null."); } Id = id; BaseFoodPrefabName = baseFoodPrefabName; IngredientClass = ingredientClass; PerfectIngredientPrefabName = (flag ? perfectIngredientPrefabName : null); NormalResultPrefabName = normalResultPrefabName; PerfectResultPrefabName = (flag2 ? perfectResultPrefabName : null); RuntimeRecipe = runtimeRecipe; } } internal static class FoodUpgradeRegistry { private static readonly List Definitions = new List(); internal static IReadOnlyList GetAll() { return Definitions; } internal static void Register(FoodUpgradeDefinition definition) { foreach (FoodUpgradeDefinition definition2 in Definitions) { if (string.Equals(definition2.Id, definition.Id, StringComparison.Ordinal)) { throw new InvalidOperationException("Food upgrade '" + definition.Id + "' is already registered."); } } Definitions.Add(definition); Plugin.Log.LogInfo((object)("Registered food upgrade '" + definition.Id + "'.")); } internal static FoodUpgradeDefinition? FindByRecipe(Recipe recipe) { foreach (FoodUpgradeDefinition definition in Definitions) { if (definition.RuntimeRecipe == recipe) { return definition; } } return null; } } internal enum IngredientClass { Sweet, Spicy, Meaty, Thick } internal static class IngredientRegistry { private const string CookingDirectoryName = "Cooking"; private const string IngredientsFileName = "Ingredients.json"; private static readonly Dictionary> IngredientsByClass = new Dictionary>(); private static bool _loaded; internal static void Load() { if (!_loaded) { string ingredientsFilePath = GetIngredientsFilePath(); if (!File.Exists(ingredientsFilePath)) { throw new FileNotFoundException("BOTC ingredient definition file was not found.", ingredientsFilePath); } string text = File.ReadAllText(ingredientsFilePath); if (string.IsNullOrWhiteSpace(text)) { throw new InvalidOperationException("BOTC ingredient definition file is empty: '" + ingredientsFilePath + "'."); } Plugin.Log.LogInfo((object)("Loading BOTC ingredient definitions from '" + ingredientsFilePath + "'.")); ValidateAndRegister(Deserialize(text, ingredientsFilePath).Classes ?? new List(), ingredientsFilePath); _loaded = true; Plugin.Log.LogInfo((object)($"Loaded {IngredientsByClass.Count} " + "BOTC ingredient classes.")); } } internal static IReadOnlyList GetIngredients(IngredientClass ingredientClass) { EnsureLoaded(); if (!IngredientsByClass.TryGetValue(ingredientClass, out IReadOnlyList value)) { throw new InvalidOperationException($"Ingredient class '{ingredientClass}' " + "is not registered."); } return value; } internal static bool Contains(IngredientClass ingredientClass, string prefabName) { if (string.IsNullOrWhiteSpace(prefabName)) { return false; } foreach (string ingredient in GetIngredients(ingredientClass)) { if (string.Equals(ingredient, prefabName, StringComparison.Ordinal)) { return true; } } return false; } private static void EnsureLoaded() { if (!_loaded) { Load(); } } private static IngredientDefinitionFileDto Deserialize(string json, string filePath) { DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(IngredientDefinitionFileDto)); byte[] bytes = Encoding.UTF8.GetBytes(json); try { using MemoryStream stream = new MemoryStream(bytes); return (dataContractJsonSerializer.ReadObject(stream) as IngredientDefinitionFileDto) ?? throw new InvalidOperationException("The JSON root object could not be converted to IngredientDefinitionFileDto."); } catch (SerializationException innerException) { throw new InvalidOperationException("BOTC Ingredients.json could not be deserialized: '" + filePath + "'.", innerException); } catch (Exception innerException2) { throw new InvalidOperationException("Failed to read BOTC Ingredients.json: '" + filePath + "'.", innerException2); } } private static void ValidateAndRegister(IReadOnlyList classDtos, string filePath) { if (classDtos.Count == 0) { throw new InvalidOperationException("BOTC ingredient definition file contains no classes: '" + filePath + "'."); } HashSet hashSet = new HashSet(); HashSet globallyRegisteredIngredients = new HashSet(StringComparer.Ordinal); for (int i = 0; i < classDtos.Count; i++) { RegisterClass(classDtos[i] ?? throw new InvalidOperationException("Ingredient class entry at index " + $"{i} is null."), i, hashSet, globallyRegisteredIngredients); } foreach (IngredientClass value in Enum.GetValues(typeof(IngredientClass))) { if (!hashSet.Contains(value)) { throw new InvalidOperationException($"Ingredient class '{value}' " + "is missing from Ingredients.json."); } } } private static void RegisterClass(IngredientClassDefinitionDto classDto, int classIndex, HashSet registeredClasses, HashSet globallyRegisteredIngredients) { string text = classDto.IngredientClassName?.Trim() ?? string.Empty; if (!Enum.TryParse(text, ignoreCase: true, out var result)) { throw new InvalidOperationException($"Ingredient class at index {classIndex} " + "has invalid name '" + text + "'."); } if (!registeredClasses.Add(result)) { throw new InvalidOperationException($"Ingredient class '{result}' " + "is registered more than once."); } List list = classDto.Ingredients ?? new List(); if (list.Count == 0) { throw new InvalidOperationException($"Ingredient class '{result}' " + "contains no ingredients."); } HashSet hashSet = new HashSet(StringComparer.Ordinal); List list2 = new List(); for (int i = 0; i < list.Count; i++) { string text2 = list[i]?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(text2)) { throw new InvalidOperationException($"Ingredient class '{result}' " + "contains an empty prefab name at index " + $"{i}."); } if (!hashSet.Add(text2)) { throw new InvalidOperationException("Ingredient '" + text2 + "' appears more " + $"than once in class '{result}'."); } if (!globallyRegisteredIngredients.Add(text2)) { throw new InvalidOperationException("Ingredient '" + text2 + "' is registered in more than one ingredient class."); } list2.Add(text2); } IngredientsByClass.Add(result, list2); Plugin.Log.LogInfo((object)("Registered ingredient class " + $"'{result}': " + string.Join(", ", list2))); } private static string GetIngredientsFilePath() { string? directoryName = Path.GetDirectoryName(typeof(Plugin).Assembly.Location); if (string.IsNullOrWhiteSpace(directoryName)) { throw new InvalidOperationException("Could not determine BOTC plugin directory."); } return Path.Combine(directoryName, "Cooking", "Ingredients.json"); } } [DataContract] internal sealed class IngredientDefinitionFileDto { [DataMember(Name = "classes", IsRequired = true)] public List Classes { get; set; } = new List(); } [DataContract] internal sealed class IngredientClassDefinitionDto { [DataMember(Name = "ingredientClass", IsRequired = true)] public string IngredientClassName { get; set; } = string.Empty; [DataMember(Name = "ingredients", IsRequired = true)] public List Ingredients { get; set; } = new List(); } } namespace BreathOfTheCauldron.Cooking.Registration { internal static class FoodContentRegistrar { internal static void Register(FoodDefinition food) { List list = new List { food.BasePrefabName }; foreach (FoodSeasoningDefinition seasoning in food.Seasonings) { FoodPrefabFactory.RegisterVariants(food, seasoning); string item = FoodNaming.BuildResultPrefabName(food, seasoning, isPerfect: false); list.Add(item); if (seasoning.HasPerfect) { string item2 = FoodNaming.BuildResultPrefabName(food, seasoning, isPerfect: true); list.Add(item2); } } FoodFamilyRegistry.RegisterFamily(food.Id, list.ToArray()); foreach (FoodSeasoningDefinition seasoning2 in food.Seasonings) { FoodRecipeFactory.Register(food, seasoning2); } Plugin.Log.LogInfo((object)("Registered BOTC food definition '" + food.Id + "'.")); } } internal static class FoodNaming { internal static string BuildResultPrefabName(FoodDefinition food, FoodSeasoningDefinition seasoning, bool isPerfect) { string classPascalName = GetClassPascalName(seasoning.IngredientClass); string text = (isPerfect ? "Plus" : string.Empty); return "BOTC_" + classPascalName + food.BasePrefabName + text; } internal static string BuildUpgradeId(FoodDefinition food, FoodSeasoningDefinition seasoning) { return food.Id + "_" + GetClassSnakeName(seasoning.IngredientClass); } internal static string BuildNameToken(FoodDefinition food, FoodSeasoningDefinition seasoning, bool isPerfect) { string text = (isPerfect ? "_plus" : string.Empty); return "$botc_" + GetClassSnakeName(seasoning.IngredientClass) + "_" + food.Id + text; } internal static string BuildDescriptionToken(FoodDefinition food, FoodSeasoningDefinition seasoning, bool isPerfect) { return BuildNameToken(food, seasoning, isPerfect) + "_description"; } private static string GetClassPascalName(IngredientClass ingredientClass) { return ingredientClass switch { IngredientClass.Sweet => "Sweet", IngredientClass.Spicy => "Spicy", IngredientClass.Meaty => "Meaty", IngredientClass.Thick => "Thick", _ => throw new ArgumentOutOfRangeException("ingredientClass", ingredientClass, null), }; } private static string GetClassSnakeName(IngredientClass ingredientClass) { return ingredientClass switch { IngredientClass.Sweet => "sweet", IngredientClass.Spicy => "spicy", IngredientClass.Meaty => "meaty", IngredientClass.Thick => "thick", _ => throw new ArgumentOutOfRangeException("ingredientClass", ingredientClass, null), }; } } internal static class FoodPrefabFactory { internal static void RegisterVariants(FoodDefinition food, FoodSeasoningDefinition seasoning) { RegisterVariant(food, seasoning, isPerfect: false); if (seasoning.HasPerfect) { RegisterVariant(food, seasoning, isPerfect: true); } } private static void RegisterVariant(FoodDefinition food, FoodSeasoningDefinition seasoning, bool isPerfect) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Expected O, but got Unknown if (isPerfect && !seasoning.HasPerfect) { throw new InvalidOperationException("Cannot register perfect variant for food '" + food.Id + "', seasoning " + $"'{seasoning.IngredientClass}': " + "no perfect pairing is configured."); } string text = FoodNaming.BuildResultPrefabName(food, seasoning, isPerfect); string name = FoodNaming.BuildNameToken(food, seasoning, isPerfect); string description = FoodNaming.BuildDescriptionToken(food, seasoning, isPerfect); CustomItem val = new CustomItem(text, food.BasePrefabName); SharedData shared = val.ItemDrop.m_itemData.m_shared; shared.m_name = name; shared.m_description = description; ApplySeasoningIcon(shared, seasoning, isPerfect, text); float food2 = shared.m_food; float foodStamina = shared.m_foodStamina; float foodRegen = shared.m_foodRegen; float foodBurnTime = shared.m_foodBurnTime; ApplySeasoningBonus(shared, seasoning, isPerfect); shared.m_maxQuality = 2; ItemManager.Instance.AddItem(val); Plugin.Log.LogInfo((object)("Registered BOTC food prefab '" + text + "': " + $"health {food2:0.##} -> " + $"{shared.m_food:0.##}, " + $"stamina {foodStamina:0.##} -> " + $"{shared.m_foodStamina:0.##}, " + $"regen {foodRegen:0.##} -> " + $"{shared.m_foodRegen:0.##}, " + $"duration {foodBurnTime:0.##} -> " + $"{shared.m_foodBurnTime:0.##}.")); } private static void ApplySeasoningIcon(SharedData sharedData, FoodSeasoningDefinition seasoning, bool isPerfect, string prefabName) { if (sharedData.m_icons == null || sharedData.m_icons.Length == 0 || (Object)(object)sharedData.m_icons[0] == (Object)null) { Plugin.Log.LogWarning((object)("BOTC could not compose icon for '" + prefabName + "': base food has no icon.")); return; } try { Sprite val = SeasoningIconComposer.Compose(sharedData.m_icons[0], seasoning.IngredientClass, isPerfect); sharedData.m_icons = (Sprite[])(object)new Sprite[1] { val }; Plugin.Log.LogInfo((object)($"Applied '{seasoning.IngredientClass}' " + "seasoning badge to '" + prefabName + "'.")); } catch (Exception ex) { Plugin.Log.LogError((object)("BOTC could not compose icon for '" + prefabName + "':\n" + ex)); } } private static void ApplySeasoningBonus(SharedData sharedData, FoodSeasoningDefinition seasoning, bool isPerfect) { float runtimeBonus = seasoning.GetRuntimeBonus(isPerfect); switch (seasoning.IngredientClass) { case IngredientClass.Sweet: sharedData.m_food += runtimeBonus; break; case IngredientClass.Spicy: sharedData.m_foodStamina += runtimeBonus; break; case IngredientClass.Meaty: sharedData.m_foodRegen += runtimeBonus; break; case IngredientClass.Thick: sharedData.m_foodBurnTime += runtimeBonus; break; default: throw new ArgumentOutOfRangeException("IngredientClass", seasoning.IngredientClass, "Unsupported seasoning class."); } } } internal static class FoodRecipeFactory { private const string CauldronPrefabName = "piece_cauldron"; internal static void Register(FoodDefinition food, FoodSeasoningDefinition seasoning) { string text = FoodNaming.BuildUpgradeId(food, seasoning); string text2 = FoodNaming.BuildResultPrefabName(food, seasoning, isPerfect: false); string text3 = (seasoning.HasPerfect ? FoodNaming.BuildResultPrefabName(food, seasoning, isPerfect: true) : null); GameObject prefab = PrefabManager.Instance.GetPrefab(text2); if ((Object)(object)prefab == (Object)null) { throw new InvalidOperationException("BOTC result prefab '" + text2 + "' was not found."); } ItemDrop component = prefab.GetComponent(); if ((Object)(object)component == (Object)null) { throw new InvalidOperationException("BOTC result prefab '" + text2 + "' does not contain ItemDrop."); } if (seasoning.HasPerfect) { GameObject prefab2 = PrefabManager.Instance.GetPrefab(text3); if ((Object)(object)prefab2 == (Object)null) { throw new InvalidOperationException("BOTC perfect result prefab '" + text3 + "' was not found."); } if ((Object)(object)prefab2.GetComponent() == (Object)null) { throw new InvalidOperationException("BOTC perfect result prefab '" + text3 + "' does not contain ItemDrop."); } } CraftingStation cauldron = GetCauldron(); Recipe val = ScriptableObject.CreateInstance(); ((Object)val).name = "Recipe_BOTC_" + text; val.m_item = component; val.m_amount = 1; val.m_enabled = true; val.m_craftingStation = cauldron; val.m_minStationLevel = food.MinimumStationLevel; val.m_requireOnlyOneIngredient = true; val.m_resources = CreateRequirements(seasoning.IngredientClass); FoodUpgradeRegistry.Register(new FoodUpgradeDefinition(text, food.BasePrefabName, seasoning.IngredientClass, seasoning.PerfectIngredientPrefabName, text2, text3, val)); Plugin.Log.LogInfo((object)("Registered BOTC seasoning recipe '" + text + "'" + (seasoning.HasPerfect ? (" with perfect pairing '" + seasoning.PerfectIngredientPrefabName + "'.") : " without perfect pairing."))); } private static CraftingStation GetCauldron() { GameObject prefab = PrefabManager.Instance.GetPrefab("piece_cauldron"); if ((Object)(object)prefab == (Object)null) { throw new InvalidOperationException("Prefab 'piece_cauldron' was not found."); } CraftingStation component = prefab.GetComponent(); if ((Object)(object)component == (Object)null) { throw new InvalidOperationException("Prefab 'piece_cauldron' does not contain CraftingStation."); } return component; } private static Requirement[] CreateRequirements(IngredientClass ingredientClass) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Expected O, but got Unknown IReadOnlyList ingredients = IngredientRegistry.GetIngredients(ingredientClass); List list = new List(); foreach (string item in ingredients) { GameObject prefab = PrefabManager.Instance.GetPrefab(item); ItemDrop val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent() : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("BOTC ingredient prefab '" + item + "' was not found for class " + $"'{ingredientClass}'.")); continue; } list.Add(new Requirement { m_resItem = val, m_amount = 1, m_amountPerLevel = 1, m_recover = false }); } if (list.Count == 0) { throw new InvalidOperationException("No valid ingredients were found " + $"for class '{ingredientClass}'."); } return list.ToArray(); } } internal static class SeasoningIconComposer { private const string AssetsDirectoryName = "Assets"; private const float BadgeSizeRatio = 0.36f; private const float BadgeMarginRatio = 0.025f; private static readonly Dictionary BadgeTextures = new Dictionary(); private static readonly List GeneratedTextures = new List(); private static readonly List GeneratedSprites = new List(); internal static Sprite Compose(Sprite sourceIcon, IngredientClass ingredientClass, bool isPerfect) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)sourceIcon == (Object)null) { throw new ArgumentNullException("sourceIcon"); } Texture2D badgeTexture = GetBadgeTexture(ingredientClass); Texture2D val = CopySpriteToReadableTexture(sourceIcon); Texture2D val2 = OverlayBadge(val, badgeTexture, isPerfect); ((Object)val2).name = $"BOTC_{ingredientClass}_" + ((Object)sourceIcon).name + (isPerfect ? "_Plus" : string.Empty); Sprite val3 = Sprite.Create(val2, new Rect(0f, 0f, (float)((Texture)val2).width, (float)((Texture)val2).height), new Vector2(0.5f, 0.5f), sourceIcon.pixelsPerUnit, 0u, (SpriteMeshType)0); ((Object)val3).name = ((Object)val2).name; GeneratedTextures.Add(val); GeneratedTextures.Add(val2); GeneratedSprites.Add(val3); return val3; } private static Texture2D GetBadgeTexture(IngredientClass ingredientClass) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown if (BadgeTextures.TryGetValue(ingredientClass, out Texture2D value)) { return value; } string badgeFilePath = GetBadgeFilePath(ingredientClass switch { IngredientClass.Sweet => "sweet.png", IngredientClass.Spicy => "spicy.png", IngredientClass.Meaty => "meaty.png", IngredientClass.Thick => "thick.png", _ => throw new ArgumentOutOfRangeException("ingredientClass", ingredientClass, "Unsupported ingredient class."), }); if (!File.Exists(badgeFilePath)) { throw new FileNotFoundException("BOTC seasoning badge was not found: '" + badgeFilePath + "'.", badgeFilePath); } byte[] array = File.ReadAllBytes(badgeFilePath); Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); ((Object)val).name = $"BOTC_Badge_{ingredientClass}"; ((Texture)val).filterMode = (FilterMode)1; ((Texture)val).wrapMode = (TextureWrapMode)1; if (!ImageConversion.LoadImage(val, array, false)) { Object.Destroy((Object)(object)val); throw new InvalidOperationException("BOTC could not load seasoning badge '" + badgeFilePath + "'."); } BadgeTextures.Add(ingredientClass, val); Plugin.Log.LogInfo((object)("Loaded BOTC seasoning badge " + $"'{ingredientClass}' from '{badgeFilePath}'.")); return val; } private static Texture2D CopySpriteToReadableTexture(Sprite sprite) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown Texture texture = (Texture)(object)sprite.texture; Rect textureRect = sprite.textureRect; int num = Mathf.Max(1, Mathf.RoundToInt(((Rect)(ref textureRect)).width)); int num2 = Mathf.Max(1, Mathf.RoundToInt(((Rect)(ref textureRect)).height)); RenderTexture temporary = RenderTexture.GetTemporary(texture.width, texture.height, 0, (RenderTextureFormat)0, (RenderTextureReadWrite)0); RenderTexture active = RenderTexture.active; try { Graphics.Blit(texture, temporary); RenderTexture.active = temporary; Texture2D val = new Texture2D(num, num2, (TextureFormat)4, false) { name = "BOTC_Readable_" + ((Object)sprite).name, filterMode = (FilterMode)1, wrapMode = (TextureWrapMode)1 }; val.ReadPixels(new Rect(((Rect)(ref textureRect)).x, ((Rect)(ref textureRect)).y, (float)num, (float)num2), 0, 0, false); val.Apply(false, false); return val; } finally { RenderTexture.active = active; RenderTexture.ReleaseTemporary(temporary); } } private static Texture2D OverlayBadge(Texture2D sourceTexture, Texture2D badgeTexture, bool isPerfect) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) int width = ((Texture)sourceTexture).width; int height = ((Texture)sourceTexture).height; Texture2D val = new Texture2D(width, height, (TextureFormat)4, false); ((Texture)val).filterMode = ((Texture)sourceTexture).filterMode; ((Texture)val).wrapMode = (TextureWrapMode)1; Color[] pixels = sourceTexture.GetPixels(); val.SetPixels(pixels); int size = Mathf.Max(1, Mathf.RoundToInt((float)Mathf.Min(width, height) * 0.36f)); int num = Mathf.Max(1, Mathf.RoundToInt((float)Mathf.Min(width, height) * 0.025f)); if (isPerfect) { DrawPerfectGlow(val, num, num, size); } DrawBadge(val, badgeTexture, num, num, size); val.Apply(false, false); return val; } private static void DrawBadge(Texture2D destination, Texture2D badge, int startX, int startY, int size) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: 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_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { float num = ((size <= 1) ? 0.5f : ((float)j / (float)(size - 1))); float num2 = ((size <= 1) ? 0.5f : ((float)i / (float)(size - 1))); Color pixelBilinear = badge.GetPixelBilinear(num, num2); if (!(pixelBilinear.a <= 0.001f)) { int num3 = startX + j; int num4 = startY + i; if (num3 >= 0 && num3 < ((Texture)destination).width && num4 >= 0 && num4 < ((Texture)destination).height) { Color val = AlphaBlend(destination.GetPixel(num3, num4), pixelBilinear); destination.SetPixel(num3, num4, val); } } } } } private static void DrawPerfectGlow(Texture2D destination, int startX, int startY, int size) { //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: 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_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) int num = Mathf.Max(1, Mathf.RoundToInt((float)size * 0.09f)); int num2 = startX + size / 2; int num3 = startY + size / 2; float num4 = (float)size * 0.58f; Color val = default(Color); ((Color)(ref val))..ctor(1f, 0.85f, 0.25f, 0.28f); int num5 = num2 - Mathf.RoundToInt(num4) - num; int num6 = num2 + Mathf.RoundToInt(num4) + num; int num7 = num3 - Mathf.RoundToInt(num4) - num; int num8 = num3 + Mathf.RoundToInt(num4) + num; for (int i = num7; i <= num8; i++) { for (int j = num5; j <= num6; j++) { if (j >= 0 && j < ((Texture)destination).width && i >= 0 && i < ((Texture)destination).height) { float num9 = Vector2.Distance(new Vector2((float)j, (float)i), new Vector2((float)num2, (float)num3)); float num10 = 1f - Mathf.InverseLerp(num4 - (float)num, num4 + (float)num, num9); if (!(num10 <= 0f)) { Color pixel = destination.GetPixel(j, i); Color foreground = val; foreground.a *= num10; destination.SetPixel(j, i, AlphaBlend(pixel, foreground)); } } } } } private static Color AlphaBlend(Color background, Color foreground) { //IL_0000: 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_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: 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) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) float num = foreground.a + background.a * (1f - foreground.a); if (num <= 0.0001f) { return Color.clear; } float num2 = (foreground.r * foreground.a + background.r * background.a * (1f - foreground.a)) / num; float num3 = (foreground.g * foreground.a + background.g * background.a * (1f - foreground.a)) / num; float num4 = (foreground.b * foreground.a + background.b * background.a * (1f - foreground.a)) / num; return new Color(num2, num3, num4, num); } private static string GetBadgeFilePath(string fileName) { string? directoryName = Path.GetDirectoryName(typeof(Plugin).Assembly.Location); if (string.IsNullOrWhiteSpace(directoryName)) { throw new InvalidOperationException("Could not determine BOTC plugin directory."); } return Path.Combine(directoryName, "Assets", fileName); } } } namespace BreathOfTheCauldron.Cooking.Definitions { internal sealed class FoodDefinition { internal string Id { get; } internal string BasePrefabName { get; } internal int MinimumStationLevel { get; } internal IReadOnlyList Seasonings { get; } internal FoodDefinition(string id, string basePrefabName, int minimumStationLevel, IReadOnlyList seasonings) { Id = id; BasePrefabName = basePrefabName; MinimumStationLevel = minimumStationLevel; Seasonings = seasonings; } } internal sealed class FoodSeasoningDefinition { internal IngredientClass IngredientClass { get; } internal string? PerfectIngredientPrefabName { get; } internal float NormalBonus { get; } internal float? PerfectBonus { get; } internal bool HasPerfect { get { if (!string.IsNullOrWhiteSpace(PerfectIngredientPrefabName)) { return PerfectBonus.HasValue; } return false; } } internal FoodSeasoningDefinition(IngredientClass ingredientClass, string? perfectIngredientPrefabName, float normalBonus, float? perfectBonus) { IngredientClass = ingredientClass; PerfectIngredientPrefabName = perfectIngredientPrefabName; NormalBonus = normalBonus; PerfectBonus = perfectBonus; } internal float GetRuntimeBonus(bool isPerfect) { float num; if (isPerfect) { if (!HasPerfect) { throw new InvalidOperationException($"Seasoning class '{IngredientClass}' " + "does not have a perfect variant."); } num = PerfectBonus.Value; } else { num = NormalBonus; } if (IngredientClass == IngredientClass.Thick) { return num * 60f; } return num; } } [DataContract] internal sealed class FoodDefinitionFileDto { [DataMember(Name = "foods", IsRequired = true)] public List Foods { get; set; } = new List(); } [DataContract] internal sealed class FoodDefinitionDto { [DataMember(Name = "id", IsRequired = true)] public string Id { get; set; } = string.Empty; [DataMember(Name = "basePrefabName", IsRequired = true)] public string BasePrefabName { get; set; } = string.Empty; [DataMember(Name = "minimumStationLevel", IsRequired = true)] public int MinimumStationLevel { get; set; } = 1; [DataMember(Name = "seasonings", IsRequired = true)] public List Seasonings { get; set; } = new List(); } [DataContract] internal sealed class FoodSeasoningDefinitionDto { [DataMember(Name = "ingredientClass", IsRequired = true)] public string IngredientClassName { get; set; } = string.Empty; [DataMember(Name = "perfectIngredientPrefabName", IsRequired = false, EmitDefaultValue = false)] public string? PerfectIngredientPrefabName { get; set; } [DataMember(Name = "normalBonus", IsRequired = true)] public float NormalBonus { get; set; } [DataMember(Name = "perfectBonus", IsRequired = false, EmitDefaultValue = false)] public float? PerfectBonus { get; set; } } internal static class FoodDefinitionLoader { private const string CookingDirectoryName = "Cooking"; private const string FoodsFileName = "Foods.json"; internal static IReadOnlyList Load() { string foodsFilePath = GetFoodsFilePath(); if (!File.Exists(foodsFilePath)) { throw new FileNotFoundException("BOTC food definition file was not found.", foodsFilePath); } string text = File.ReadAllText(foodsFilePath); if (string.IsNullOrWhiteSpace(text)) { throw new InvalidOperationException("BOTC food definition file is empty: '" + foodsFilePath + "'."); } Plugin.Log.LogInfo((object)("Loading BOTC food definitions from '" + foodsFilePath + "'.")); List list = Deserialize(text, foodsFilePath).Foods ?? new List(); Plugin.Log.LogInfo((object)("BOTC deserialized food DTOs: " + $"{list.Count}.")); IReadOnlyList readOnlyList = ConvertAndValidate(list, foodsFilePath); Plugin.Log.LogInfo((object)$"Loaded {readOnlyList.Count} BOTC food definitions."); return readOnlyList; } private static FoodDefinitionFileDto Deserialize(string json, string filePath) { DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(FoodDefinitionFileDto)); byte[] bytes = Encoding.UTF8.GetBytes(json); try { using MemoryStream stream = new MemoryStream(bytes); return (dataContractJsonSerializer.ReadObject(stream) as FoodDefinitionFileDto) ?? throw new InvalidOperationException("The JSON root object could not be converted to FoodDefinitionFileDto."); } catch (SerializationException innerException) { throw new InvalidOperationException("BOTC Foods.json could not be deserialized: '" + filePath + "'.", innerException); } catch (Exception innerException2) { throw new InvalidOperationException("Failed to read BOTC Foods.json: '" + filePath + "'.", innerException2); } } private static string GetFoodsFilePath() { string? directoryName = Path.GetDirectoryName(typeof(Plugin).Assembly.Location); if (string.IsNullOrWhiteSpace(directoryName)) { throw new InvalidOperationException("Could not determine BOTC plugin directory."); } return Path.Combine(directoryName, "Cooking", "Foods.json"); } private static IReadOnlyList ConvertAndValidate(IReadOnlyList foodDtos, string filePath) { if (foodDtos.Count == 0) { throw new InvalidOperationException("BOTC food definition file contains no foods: '" + filePath + "'."); } HashSet registeredIds = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet registeredBasePrefabs = new HashSet(StringComparer.Ordinal); List list = new List(); for (int i = 0; i < foodDtos.Count; i++) { FoodDefinition item = ConvertFood(foodDtos[i] ?? throw new InvalidOperationException($"Food entry at index {i} is null."), i, registeredIds, registeredBasePrefabs); list.Add(item); } return list; } private static FoodDefinition ConvertFood(FoodDefinitionDto foodDto, int foodIndex, HashSet registeredIds, HashSet registeredBasePrefabs) { string text = foodDto.Id?.Trim() ?? string.Empty; string text2 = foodDto.BasePrefabName?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) { throw new InvalidOperationException($"Food at index {foodIndex} has no ID."); } if (!registeredIds.Add(text)) { throw new InvalidOperationException("Duplicate BOTC food ID '" + text + "'."); } if (string.IsNullOrWhiteSpace(text2)) { throw new InvalidOperationException("Food '" + text + "' has no basePrefabName."); } if (!registeredBasePrefabs.Add(text2)) { throw new InvalidOperationException("Base prefab '" + text2 + "' is registered more than once."); } if (foodDto.MinimumStationLevel < 1) { throw new InvalidOperationException("Food '" + text + "' has invalid minimumStationLevel " + $"{foodDto.MinimumStationLevel}."); } List list = foodDto.Seasonings ?? new List(); if (list.Count == 0) { throw new InvalidOperationException("Food '" + text + "' has no seasonings."); } HashSet registeredClasses = new HashSet(); List list2 = new List(); for (int i = 0; i < list.Count; i++) { FoodSeasoningDefinitionDto foodSeasoningDefinitionDto = list[i]; if (foodSeasoningDefinitionDto == null) { throw new InvalidOperationException("Food '" + text + "' contains a null seasoning " + $"at index {i}."); } FoodSeasoningDefinition item = ConvertSeasoning(text, foodSeasoningDefinitionDto, i, registeredClasses); list2.Add(item); } return new FoodDefinition(text, text2, foodDto.MinimumStationLevel, list2); } private static FoodSeasoningDefinition ConvertSeasoning(string foodId, FoodSeasoningDefinitionDto seasoningDto, int seasoningIndex, HashSet registeredClasses) { string text = seasoningDto.IngredientClassName?.Trim() ?? string.Empty; if (!Enum.TryParse(text, ignoreCase: true, out var result)) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"{seasoningIndex}: invalid ingredientClass " + "'" + text + "'."); } if (!registeredClasses.Add(result)) { throw new InvalidOperationException("Food '" + foodId + "' contains more than one " + $"'{result}' seasoning."); } if (seasoningDto.NormalBonus < 0f) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"'{result}' has a negative " + "normalBonus."); } string text2 = seasoningDto.PerfectIngredientPrefabName?.Trim(); bool flag = !string.IsNullOrWhiteSpace(text2); bool hasValue = seasoningDto.PerfectBonus.HasValue; if (flag != hasValue) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"'{result}' must define both " + "perfectIngredientPrefabName and perfectBonus, or neither of them."); } if (hasValue) { float value = seasoningDto.PerfectBonus.Value; if (value < 0f) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"'{result}' has a negative " + "perfectBonus."); } if (value < seasoningDto.NormalBonus) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"'{result}': perfectBonus " + "cannot be lower than normalBonus."); } if (!IngredientRegistry.Contains(result, text2)) { throw new InvalidOperationException("Food '" + foodId + "', seasoning " + $"'{result}': perfect ingredient " + "'" + text2 + "' is not registered in IngredientRegistry."); } } return new FoodSeasoningDefinition(result, flag ? text2 : null, seasoningDto.NormalBonus, hasValue ? seasoningDto.PerfectBonus : ((float?)null)); } } }