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 BreathOfTheCauldron v0.1.0
plugins\BreathOfTheCauldron\BreathOfTheCauldron.dll
Decompiled 20 hours agousing 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<bool> 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<bool>("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<string, string> BaseFoodNameByResultName = new Dictionary<string, string>(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<Recipe>) })] internal static class InventoryGuiUpdateRecipeListPatch { private sealed class PatchState { internal readonly List<Recipe> AddedRecipes = new List<Recipe>(); internal bool ContextStarted; } private const string VanillaCauldronPrefabName = "piece_cauldron"; private static void Prefix(InventoryGui __instance, List<Recipe> 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<ItemDrop>() : 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<Recipe> recipes, PatchState __state) { Cleanup(recipes, __state); } private static Exception? Finalizer(Exception? __exception, List<Recipe> recipes, PatchState __state) { Cleanup(recipes, __state); return __exception; } private static void Cleanup(List<Recipe> 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<Recipe> 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<ItemData>) })] 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<ItemData> __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<ItemDrop>() : 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<Recipe> 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<TMP_Text>(); } 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<TMP_Text>(); } 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<TMP_Text>(), 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<Recipe> 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<Recipe>) })] [HarmonyPriority(0)] internal static class CauldronUpgradeTabTextPatch { private static void Postfix(InventoryGui __instance, List<Recipe> 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<Recipe>) })] [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<Recipe> 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<Image>(); if (!((Object)(object)component == (Object)null) && ((Graphic)component).color != Color.white) { ((Graphic)component).color = Color.white; } } } } private static bool ContainsCauldronRecipe(List<Recipe> 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<string, string> FamilyByPrefabName = new Dictionary<string, string>(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<FoodDefinition> 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<FoodUpgradeDefinition> Definitions = new List<FoodUpgradeDefinition>(); internal static IReadOnlyList<FoodUpgradeDefinition> 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<IngredientClass, IReadOnlyList<string>> IngredientsByClass = new Dictionary<IngredientClass, IReadOnlyList<string>>(); 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<IngredientClassDefinitionDto>(), ingredientsFilePath); _loaded = true; Plugin.Log.LogInfo((object)($"Loaded {IngredientsByClass.Count} " + "BOTC ingredient classes.")); } } internal static IReadOnlyList<string> GetIngredients(IngredientClass ingredientClass) { EnsureLoaded(); if (!IngredientsByClass.TryGetValue(ingredientClass, out IReadOnlyList<string> 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<IngredientClassDefinitionDto> classDtos, string filePath) { if (classDtos.Count == 0) { throw new InvalidOperationException("BOTC ingredient definition file contains no classes: '" + filePath + "'."); } HashSet<IngredientClass> hashSet = new HashSet<IngredientClass>(); HashSet<string> globallyRegisteredIngredients = new HashSet<string>(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<IngredientClass> registeredClasses, HashSet<string> globallyRegisteredIngredients) { string text = classDto.IngredientClassName?.Trim() ?? string.Empty; if (!Enum.TryParse<IngredientClass>(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<string> list = classDto.Ingredients ?? new List<string>(); if (list.Count == 0) { throw new InvalidOperationException($"Ingredient class '{result}' " + "contains no ingredients."); } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); List<string> list2 = new List<string>(); 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<IngredientClassDefinitionDto> Classes { get; set; } = new List<IngredientClassDefinitionDto>(); } [DataContract] internal sealed class IngredientClassDefinitionDto { [DataMember(Name = "ingredientClass", IsRequired = true)] public string IngredientClassName { get; set; } = string.Empty; [DataMember(Name = "ingredients", IsRequired = true)] public List<string> Ingredients { get; set; } = new List<string>(); } } namespace BreathOfTheCauldron.Cooking.Registration { internal static class FoodContentRegistrar { internal static void Register(FoodDefinition food) { List<string> list = new List<string> { 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<ItemDrop>(); 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<ItemDrop>() == (Object)null) { throw new InvalidOperationException("BOTC perfect result prefab '" + text3 + "' does not contain ItemDrop."); } } CraftingStation cauldron = GetCauldron(); Recipe val = ScriptableObject.CreateInstance<Recipe>(); ((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<CraftingStation>(); 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<string> ingredients = IngredientRegistry.GetIngredients(ingredientClass); List<Requirement> list = new List<Requirement>(); foreach (string item in ingredients) { GameObject prefab = PrefabManager.Instance.GetPrefab(item); ItemDrop val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent<ItemDrop>() : 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<IngredientClass, Texture2D> BadgeTextures = new Dictionary<IngredientClass, Texture2D>(); private static readonly List<Texture2D> GeneratedTextures = new List<Texture2D>(); private static readonly List<Sprite> GeneratedSprites = new List<Sprite>(); 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<FoodSeasoningDefinition> Seasonings { get; } internal FoodDefinition(string id, string basePrefabName, int minimumStationLevel, IReadOnlyList<FoodSeasoningDefinition> 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<FoodDefinitionDto> Foods { get; set; } = new List<FoodDefinitionDto>(); } [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<FoodSeasoningDefinitionDto> Seasonings { get; set; } = new List<FoodSeasoningDefinitionDto>(); } [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<FoodDefinition> 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<FoodDefinitionDto> list = Deserialize(text, foodsFilePath).Foods ?? new List<FoodDefinitionDto>(); Plugin.Log.LogInfo((object)("BOTC deserialized food DTOs: " + $"{list.Count}.")); IReadOnlyList<FoodDefinition> 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<FoodDefinition> ConvertAndValidate(IReadOnlyList<FoodDefinitionDto> foodDtos, string filePath) { if (foodDtos.Count == 0) { throw new InvalidOperationException("BOTC food definition file contains no foods: '" + filePath + "'."); } HashSet<string> registeredIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); HashSet<string> registeredBasePrefabs = new HashSet<string>(StringComparer.Ordinal); List<FoodDefinition> list = new List<FoodDefinition>(); 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<string> registeredIds, HashSet<string> 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<FoodSeasoningDefinitionDto> list = foodDto.Seasonings ?? new List<FoodSeasoningDefinitionDto>(); if (list.Count == 0) { throw new InvalidOperationException("Food '" + text + "' has no seasonings."); } HashSet<IngredientClass> registeredClasses = new HashSet<IngredientClass>(); List<FoodSeasoningDefinition> list2 = new List<FoodSeasoningDefinition>(); 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<IngredientClass> registeredClasses) { string text = seasoningDto.IngredientClassName?.Trim() ?? string.Empty; if (!Enum.TryParse<IngredientClass>(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)); } } }