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 Odins Pantry v0.1.2
plugins/OdinsPantry.dll
Decompiled 17 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("OdinsPantry")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.2.0")] [assembly: AssemblyInformationalVersion("0.1.2+53600f8bf1d88b4e10eb1ca598e4dbbc942d34ee")] [assembly: AssemblyProduct("OdinsPantry")] [assembly: AssemblyTitle("OdinsPantry")] [assembly: AssemblyVersion("0.1.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace OdinsPantry { internal static class FlavorText { private static readonly string[] General = new string[55] { "Three carrots is not a diet, it is a cry for help.", "A warrior marches on his stomach. Mostly because his legs have given up.", "Eat like the ravens are watching. They are.", "Valhalla has an endless feast. You are not in Valhalla yet.", "Food is armour you wear on the inside.", "Hunger is the only enemy that never misses.", "Odin has seen great warriors fall. Most of them had skipped lunch.", "A full belly fears no troll. An empty one fears a Greyling.", "The cauldron does not judge. Odin does.", "Nobody sings about the hero who ran out of stamina halfway up a hill.", "Eat, rest, then do something foolish. In that order.", "Eat the good stuff. You can't take it with you, and you will die a lot.", "If it can be stewed, stew it. If it can't, you aren't trying.", "Food in a chest helps nobody. Eat it or give it a name.", "Your chests are a museum of good intentions.", "Nothing rots here. You have no excuses.", "Huginn says you're hungry. Muninn says you were hungry yesterday too.", "Sharing is noble. Eating first is wiser.", "Odin keeps a pantry too. It is mostly mead.", "The best weapon in your inventory is the soup.", "Skipping meals doesn't make you faster. Ask the wolves.", "Hearty food, heavy blows. Light snacks, light coffins.", "You eat like someone who expects to respawn.", "The bees worked hard for that honey. Do something with it.", "Every boar you meet is a sausage that hasn't been told yet.", "Feasting is a skill. You are level one.", "An empty slot in your belly is a hole in your shield.", "A warm meal and a dry cloak: the two halves of courage.", "Some pantries are full of food. Yours is full of potential.", "Plan your meals like you plan your raids. Then plan your raids.", "Nobody ever lost a fight because they ate too well.", "Cook before you need to. Hunger makes poor cooks.", "Stew today, glory tomorrow.", "Trolls don't snack. That's why they're so angry.", "The ravens eat whatever they find. You can do better.", "Food fades. So do heroes who forget about it.", "Frigg packs Odin a lunch. Nobody packs yours.", "Thor once ate an entire ox before a wedding. Aim lower, but not much.", "You can't out-swing a bad diet.", "A cauldron is a shield you can eat out of.", "Odin traded an eye for wisdom. The wisdom was: eat properly.", "The serpent eats sailors. Sailors should eat the serpent first.", "Rations are for people with a plan. Do you have a plan?", "Hunger makes every hill a mountain and every mountain a grave.", "Seasoning is optional. Eating is not.", "The longhouse fire is for cooking. The sitting is a bonus.", "Respect the pot. The pot remembers.", "Your inventory is heavy and your belly is light. Fix one of them.", "There is no shame in seconds. There is shame in none.", "A good meal is the only buff the gods hand out for free.", "Greydwarfs eat stones. Be better than a Greydwarf.", "Even the dead in Valhalla stop fighting for dinner.", "Some say the way to a Viking's heart is through his stomach. Mostly it's an axe.", "Tend your hunger like a fire. Feed it before it goes out.", "A sharp axe and a full bowl. Nothing else matters before noon." }; private static readonly string[] EmptyBelly = new string[8] { "Empty belly, empty swing. Eat something.", "Your stomach is making the noise a troll makes. See to it.", "Nothing in you but courage, and courage weighs nothing.", "Running on air and spite. Impressive. Stop it.", "That's the health of a chicken with ambitions.", "Even the Greydwarfs had breakfast.", "Odin can hear your stomach from Asgard.", "Out into the world on an empty stomach? Bold. Brief, but bold." }; private static readonly string[] NothingInReach = new string[7] { "The pantry is bare. Even the mice have moved out.", "No food anywhere. Go and punch a boar.", "Nothing to eat, nothing to cook. The Meadows are full of berries, you know.", "Odin searched every chest. He found a feather and some regret.", "Somebody ate everything. Odin suspects you.", "Your chests are full of stone and wood. Neither one is dinner.", "Not a crumb. The ravens are disappointed in you." }; private static readonly string[] ForageOnly = new string[6] { "Berries and mushrooms. Are you a warrior or a squirrel?", "Foraging is a fine start. A cauldron is a fine finish.", "That's a snack, not a meal. The trolls know the difference.", "All raw. Fire was invented for a reason.", "You've eaten like a deer. Deer get hunted.", "Picked straight off the bush. Odin admires your trust in nature." }; private static readonly string[] WellFed = new string[6] { "Three good meals in. Now go and make a mistake worth singing about.", "Well fed. Odin approves, briefly.", "Belly full, bars full. No more excuses.", "That's a proper Viking meal. Go and earn it.", "Too well fed to die stupidly. You'll find a way.", "Full to the brim. Walk it off on something dangerous." }; private static readonly string[] Underfed = new string[6] { "That much health won't last long in the {0}.", "The {0} eats people who eat like that.", "You're dressed for the {0} but eating for the Meadows.", "Odin wouldn't walk into the {0} on that menu, and Odin is a god.", "The {0} wants a bigger meal than you've got.", "Pack a lunch. The {0} isn't going to feed you. It's going to eat you." }; private static readonly string[] OneShort = new string[5] { "{0} is one {1} away. Go and find one.", "So close to {0}. All it wants is {1}.", "One {1} short of {0}. Tragic.", "Odin can smell the {0} already. It just needs {1}.", "You have nearly everything for {0}. Nearly is a sad word." }; private static readonly Dictionary<FoodPreference, string[]> ByPreference = new Dictionary<FoodPreference, string[]> { [FoodPreference.Health] = new string[2] { "Health first. Good. You'll need it.", "Built to take a hit. Odin likes a sturdy plate." }, [FoodPreference.Stamina] = new string[2] { "Built for running away. Respectable.", "All stamina. You'll go far, quickly, possibly screaming." }, [FoodPreference.Eitr] = new string[2] { "Mages eat their greens. Then they set things on fire.", "Eitr for supper. Your staff thanks you." } }; private static readonly Dictionary<string, string[]> ByFood = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase) { ["Carrot"] = new string[2] { "Three carrots is not a diet, it is a cry for help.", "Raw carrot. Good for the eyes. Odin has one left, and it agrees." }, ["Raspberry"] = new string[2] { "Raspberries. A handful of red hope.", "Raspberries. Lovely. Now eat a meal." }, ["Blueberries"] = new string[1] { "Blueberries. Purple tongue, weak swing." }, ["Cloudberry"] = new string[1] { "Cloudberries. Cold, tart and hardly a meal." }, ["Mushroom"] = new string[1] { "Mushrooms. Hopefully the red ones." }, ["MushroomYellow"] = new string[1] { "Yellow mushrooms. Bold choice. Odin respects it." }, ["Honey"] = new string[1] { "Straight honey. The bees are furious and you are sticky." }, ["CookedMeat"] = new string[1] { "Boar, cooked. Never fails, never thrills." }, ["NeckTailGrilled"] = new string[1] { "Grilled neck tail. Crunchy. Don't ask which bit." }, ["QueensJam"] = new string[1] { "Queen's jam. Fit for royalty, eaten by you." }, ["CarrotSoup"] = new string[1] { "Carrot soup. Your mother would be proud." }, ["DeerStew"] = new string[1] { "Deer stew. Warm, thick and worth the antlers." }, ["Sausages"] = new string[1] { "Sausages. Best not to ask what's in them. Entrails. It's entrails." }, ["TurnipStew"] = new string[1] { "Turnip stew. The swamp tastes better than it smells." }, ["BlackSoup"] = new string[1] { "Black soup. It is black. It is soup. Odin has no further questions." }, ["SerpentStew"] = new string[1] { "Serpent stew. You beat the sea, and then you ate it." }, ["SerpentMeatCooked"] = new string[1] { "Cooked serpent. The sea's revenge will be slow." }, ["CookedWolfMeat"] = new string[1] { "Wolf meat. The pack would be appalled." }, ["WolfJerky"] = new string[1] { "Wolf jerky. Chewy enough to count as exercise." }, ["WolfMeatSkewer"] = new string[1] { "A wolf on a stick. The circle of life, with a stick." }, ["OnionSoup"] = new string[1] { "Onion soup. Your enemies will smell you coming." }, ["Eyescream"] = new string[1] { "Eyescream. Odin refuses to comment on the name." }, ["FishCooked"] = new string[1] { "Cooked fish. Simple, honest, slightly bony." }, ["LoxPie"] = new string[1] { "Lox pie. A meat pie the size of a shield. Perfect." }, ["FishWraps"] = new string[1] { "Fish wraps. Fancy. Did you use a napkin?" }, ["BloodPudding"] = new string[1] { "Blood pudding. Dark, rich and slightly alarming." }, ["Bread"] = new string[1] { "Bread. You waited for the oven. Patience is a weapon." }, ["CookedLoxMeat"] = new string[1] { "Lox meat. A whole beast's worth of chewing." }, ["CookedChickenMeat"] = new string[1] { "Chicken. Every hen is a warning to every rooster." }, ["CookedEgg"] = new string[1] { "A cooked egg. Small, but it had dreams." }, ["MeatPlatter"] = new string[1] { "A meat platter. Odin likes a warrior who thinks big." }, ["HoneyGlazedChicken"] = new string[1] { "Honey glazed chicken. Odin is jealous." }, ["MisthareSupreme"] = new string[1] { "Misthare supreme. Odin would trade an eye for it. He knows how." }, ["Salad"] = new string[1] { "A salad. In this world. Brave." }, ["MushroomOmelette"] = new string[1] { "Mushroom omelette. Breakfast of mages." }, ["YggdrasilPorridge"] = new string[1] { "Yggdrasil porridge. You're eating the World Tree. Show some respect." }, ["SeekerAspic"] = new string[1] { "Seeker aspic. Wobbly, glowing and still faintly clicking." }, ["MagicallyStuffedShroom"] = new string[1] { "A stuffed shroom. It's magic, apparently." }, ["CookedBugMeat"] = new string[1] { "Cooked bug. Crunchy outside, regret inside." }, ["CookedHareMeat"] = new string[1] { "Hare. Fast in life, fast in the pan." }, ["CookedDeerMeat"] = new string[1] { "Venison. The deer never saw it coming. You never let it." }, ["Fiddleheadfern"] = new string[1] { "Fiddlehead fern. Eating the scenery now?" } }; internal static string Pick(PantryResult p) { //IL_0170: Unknown result type (might be due to invalid IL or missing references) if (p == null) { return Any(General); } if (Random.value < 0.2f) { return Any(General); } bool flag = p.Eat.Count == 0 && p.Cook.Count == 0; if (p.Belly.Count == 0) { if (!flag || !(Random.value < 0.6f)) { return Any(EmptyBelly); } return Any(NothingInReach); } if (flag && Random.value < 0.6f) { return Any(NothingInReach); } List<BellyFood> list = p.Belly.Where((BellyFood b) => ByFood.ContainsKey(b.Food.Prefab)).ToList(); if (list.Count > 0 && Random.value < 0.45f) { return Any(ByFood[list[Random.Range(0, list.Count)].Food.Prefab]); } if (p.Belly.All((BellyFood b) => b.Food.Forage) && Random.value < 0.7f) { return Any(ForageOnly); } float num = p.Belly.Sum((BellyFood b) => b.Health); if (PantryText.TryGetHealthWanted(p.Biome, out var wanted)) { if (num < wanted * 0.75f && Random.value < 0.6f) { return string.Format(Any(Underfed), p.BiomeName); } if (p.Belly.Count == 3 && num >= wanted && Random.value < 0.6f) { return Any(WellFed); } } if (p.OneShort.Count > 0 && Random.value < 0.4f) { CookOption cookOption = p.OneShort[Random.Range(0, p.OneShort.Count)]; return string.Format(Any(OneShort), cookOption.Food.Name, cookOption.MissingName); } if (ByPreference.TryGetValue(Plugin.Preference.Value, out var value) && Random.value < 0.25f) { return Any(value); } return Any(General); } private static string Any(string[] lines) { return lines[Random.Range(0, lines.Length)]; } } internal sealed class FoodInfo { public string Key; public string Prefab; public string Name; public float Health; public float Stamina; public float Eitr; public float BurnTime; public bool Forage; public float Score(FoodPreference pref) { return pref switch { FoodPreference.Health => Health + 0.3f * (Stamina + Eitr), FoodPreference.Stamina => Stamina + 0.3f * (Health + Eitr), FoodPreference.Eitr => Eitr + 0.3f * (Health + Stamina), _ => Health + Stamina + Eitr, }; } } internal sealed class BellyFood { public FoodInfo Food; public float SecondsLeft; public float Health; public float Stamina; public float Eitr; public bool CanTopUp; } internal sealed class EatOption { public FoodInfo Food; public int Count; public bool StillEaten; } internal sealed class CookOption { public FoodInfo Food; public int Batches; public int PerBatch = 1; public string Station; public int StationLevel = 1; public bool StationNearby = true; public string MissingName; public int MissingAmount; } internal sealed class Pick { public FoodInfo Food; public bool NeedsCooking; public bool InBelly; } internal sealed class PantryResult { public readonly List<BellyFood> Belly = new List<BellyFood>(); public readonly List<EatOption> Eat = new List<EatOption>(); public readonly List<CookOption> Cook = new List<CookOption>(); public readonly List<CookOption> OneShort = new List<CookOption>(); public readonly List<Pick> Picks = new List<Pick>(); public Biome Biome; public string BiomeName; public int ChestsSearched; public float BaseHealth; public float BaseStamina; public float PickHealth; public float PickStamina; public float PickEitr; } internal static class Pantry { private sealed class Conversion { public string FromKey; public ItemData To; public string Station; } private static readonly List<Conversion> Conversions = new List<Conversion>(); private static readonly HashSet<string> MadeFoods = new HashSet<string>(); private static ZNetScene _scannedScene; private static ObjectDB _scannedDb; private static Func<Container, long, bool> _checkAccess; private static bool _checkAccessLooked; private static readonly Dictionary<string, int> Counts = new Dictionary<string, int>(); private static readonly Dictionary<string, ItemData> Samples = new Dictionary<string, ItemData>(); private static readonly List<Container> Chests = new List<Container>(); private static float _nextChestScan; internal static bool IsFood(SharedData s) { if (s != null) { if (!(s.m_food > 0f) && !(s.m_foodStamina > 0f)) { return s.m_foodEitr > 0f; } return true; } return false; } internal static PantryResult Compute(bool rescanChests) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)ObjectDB.instance == (Object)null) { return null; } ScanWorldData(); PantryResult pantryResult = new PantryResult { Biome = localPlayer.GetCurrentBiome(), BaseHealth = localPlayer.m_baseHP, BaseStamina = localPlayer.m_baseStamina }; pantryResult.BiomeName = BiomeName(pantryResult.Biome); Counts.Clear(); Samples.Clear(); AddInventory(((Humanoid)localPlayer).GetInventory()); if (rescanChests || Time.time >= _nextChestScan) { _nextChestScan = Time.time + 3f; FindChests(localPlayer); } foreach (Container chest in Chests) { if ((Object)(object)chest != (Object)null) { AddInventory(chest.GetInventory()); } } pantryResult.ChestsSearched = Chests.Count; Dictionary<string, BellyFood> dictionary = new Dictionary<string, BellyFood>(); float foodRate = Game.m_foodRate; foreach (Food food in localPlayer.GetFoods()) { if (food?.m_item != null) { BellyFood bellyFood = new BellyFood { Food = Info(food.m_item, food.m_name), SecondsLeft = ((foodRate > 0f) ? (food.m_time / foodRate) : float.PositiveInfinity), Health = food.m_health, Stamina = food.m_stamina, Eitr = food.m_eitr, CanTopUp = food.CanEatAgain() }; pantryResult.Belly.Add(bellyFood); dictionary[bellyFood.Food.Key] = bellyFood; } } FoodPreference pref = Plugin.Preference.Value; foreach (KeyValuePair<string, ItemData> sample in Samples) { if (IsFood(sample.Value.m_shared)) { pantryResult.Eat.Add(new EatOption { Food = Info(sample.Value, null), Count = Counts[sample.Key], StillEaten = (dictionary.TryGetValue(sample.Key, out var value) && !value.CanTopUp) }); } } pantryResult.Eat.Sort((EatOption a, EatOption b) => b.Food.Score(pref).CompareTo(a.Food.Score(pref))); HashSet<string> hashSet = new HashSet<string>(); foreach (Recipe recipe in ObjectDB.instance.m_recipes) { if ((Object)(object)recipe == (Object)null || !recipe.m_enabled || (Object)(object)recipe.m_item == (Object)null) { continue; } ItemData itemData = recipe.m_item.m_itemData; if (IsFood(itemData?.m_shared) && !hashSet.Contains(itemData.m_shared.m_name) && (!Plugin.OnlyKnownRecipes.Value || localPlayer.IsRecipeKnown(itemData.m_shared.m_name))) { CookOption cookOption = Check(recipe, localPlayer); if (cookOption != null) { hashSet.Add(itemData.m_shared.m_name); ((cookOption.MissingName == null) ? pantryResult.Cook : pantryResult.OneShort).Add(cookOption); } } } foreach (Conversion conversion in Conversions) { string name = conversion.To.m_shared.m_name; if (!hashSet.Contains(name) && Counts.TryGetValue(conversion.FromKey, out var value2) && value2 > 0) { hashSet.Add(name); pantryResult.Cook.Add(new CookOption { Food = Info(conversion.To, null), Batches = value2, Station = conversion.Station }); } } pantryResult.Cook.Sort((CookOption a, CookOption b) => b.Food.Score(pref).CompareTo(a.Food.Score(pref))); pantryResult.OneShort.Sort((CookOption a, CookOption b) => b.Food.Score(pref).CompareTo(a.Food.Score(pref))); Dictionary<string, Pick> dictionary2 = new Dictionary<string, Pick>(); foreach (EatOption item in pantryResult.Eat) { dictionary2[item.Food.Key] = new Pick { Food = item.Food }; } foreach (CookOption item2 in pantryResult.Cook) { if (!dictionary2.ContainsKey(item2.Food.Key)) { dictionary2[item2.Food.Key] = new Pick { Food = item2.Food, NeedsCooking = true }; } } List<Pick> list = new List<Pick>(dictionary2.Values); list.Sort(delegate(Pick a, Pick b) { int num2 = b.Food.Score(pref).CompareTo(a.Food.Score(pref)); return (num2 == 0) ? b.Food.BurnTime.CompareTo(a.Food.BurnTime) : num2; }); for (int num = 0; num < list.Count && num < 3; num++) { Pick pick = list[num]; pick.InBelly = dictionary.ContainsKey(pick.Food.Key); pantryResult.Picks.Add(pick); pantryResult.PickHealth += pick.Food.Health; pantryResult.PickStamina += pick.Food.Stamina; pantryResult.PickEitr += pick.Food.Eitr; } return pantryResult; } private static CookOption Check(Recipe r, Player player) { //IL_0187: Unknown result type (might be due to invalid IL or missing references) CookOption cookOption = new CookOption { Food = Info(r.m_item.m_itemData, ((Object)r.m_item).name), PerBatch = Math.Max(1, r.m_amount), Batches = int.MaxValue }; int num = 0; int num2 = 0; bool flag = false; Requirement[] resources = r.m_resources; foreach (Requirement val in resources) { if ((Object)(object)val?.m_resItem == (Object)null) { continue; } int amount = val.GetAmount(1); if (amount > 0) { num2++; string name = val.m_resItem.m_itemData.m_shared.m_name; Counts.TryGetValue(name, out var value); if (value >= amount) { flag = true; cookOption.Batches = Math.Min(cookOption.Batches, value / amount); } else { num++; cookOption.MissingName = Localize(name); cookOption.MissingAmount = amount - value; } } } if (num2 == 0) { return null; } if (r.m_requireOnlyOneIngredient && flag) { num = 0; cookOption.MissingName = null; } if (num > 1) { return null; } if (num == 1) { cookOption.Batches = 0; } if ((Object)(object)r.m_craftingStation != (Object)null) { cookOption.Station = Localize(r.m_craftingStation.m_name); cookOption.StationLevel = Math.Max(1, r.m_minStationLevel); float num3 = Math.Max(Plugin.SearchRadius.Value, 20f); CraftingStation val2 = CraftingStation.FindClosestStationInRange(r.m_craftingStation.m_name, ((Component)player).transform.position, num3); cookOption.StationNearby = (Object)(object)val2 != (Object)null && val2.GetLevel(true) >= cookOption.StationLevel; } return cookOption; } private static void AddInventory(Inventory inv) { if (inv == null) { return; } foreach (ItemData allItem in inv.GetAllItems()) { string text = allItem?.m_shared?.m_name; if (!string.IsNullOrEmpty(text)) { Counts.TryGetValue(text, out var value); Counts[text] = value + allItem.m_stack; if (!Samples.ContainsKey(text)) { Samples[text] = allItem; } } } } private static void FindChests(Player player) { //IL_0024: 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_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) Chests.Clear(); float value = Plugin.SearchRadius.Value; if (value <= 0f) { return; } Vector3 position = ((Component)player).transform.position; long me = (((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile().GetPlayerID() : 0); Container[] array = Object.FindObjectsByType<Container>((FindObjectsSortMode)0); foreach (Container val in array) { if (!((Object)(object)val == (Object)null) && val.GetInventory() != null && !((Object)(object)((Component)val).GetComponent<TombStone>() != (Object)null) && val.GetInventory() != ((Humanoid)player).GetInventory() && !(Vector3.Distance(position, ((Component)val).transform.position) > value) && CanOpen(val, me)) { Chests.Add(val); } } } private static bool CanOpen(Container c, long me) { //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Invalid comparison between Unknown and I4 //IL_000e: Unknown result type (might be due to invalid IL or missing references) try { if (c.m_checkGuardStone && !PrivateArea.CheckAccess(((Component)c).transform.position, 0f, false, false)) { return false; } if (!_checkAccessLooked) { _checkAccessLooked = true; MethodInfo methodInfo = AccessTools.Method(typeof(Container), "CheckAccess", new Type[1] { typeof(long) }, (Type[])null); if (methodInfo != null) { _checkAccess = AccessTools.MethodDelegate<Func<Container, long, bool>>(methodInfo, (object)null, true); } } return _checkAccess == null || _checkAccess(c, me); } catch (Exception) { return (int)c.m_privacy == 2; } } private static void ScanWorldData() { if ((Object)(object)ZNetScene.instance == (Object)(object)_scannedScene && (Object)(object)ObjectDB.instance == (Object)(object)_scannedDb) { return; } _scannedScene = ZNetScene.instance; _scannedDb = ObjectDB.instance; Conversions.Clear(); MadeFoods.Clear(); foreach (Recipe recipe in ObjectDB.instance.m_recipes) { if ((Object)(object)recipe?.m_item != (Object)null && IsFood(recipe.m_item.m_itemData?.m_shared)) { MadeFoods.Add(recipe.m_item.m_itemData.m_shared.m_name); } } if ((Object)(object)ZNetScene.instance == (Object)null) { return; } HashSet<string> hashSet = new HashSet<string>(); foreach (GameObject prefab in ZNetScene.instance.m_prefabs) { CookingStation val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent<CookingStation>() : null); if ((Object)(object)val == (Object)null || val.m_conversion == null) { continue; } foreach (ItemConversion item in val.m_conversion) { ItemData val2 = item?.m_to?.m_itemData; ItemData val3 = item?.m_from?.m_itemData; if (val2 != null && val3 != null && IsFood(val2.m_shared)) { MadeFoods.Add(val2.m_shared.m_name); if (hashSet.Add(val3.m_shared.m_name + ">" + val2.m_shared.m_name)) { Conversions.Add(new Conversion { FromKey = val3.m_shared.m_name, To = val2, Station = Localize(val.m_name) }); } } } } Plugin.Log.LogDebug((object)$"Found {Conversions.Count} cooking station conversions and {MadeFoods.Count} made foods."); } private static FoodInfo Info(ItemData item, string prefab) { SharedData shared = item.m_shared; return new FoodInfo { Key = shared.m_name, Prefab = (prefab ?? (((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : shared.m_name)), Name = Localize(shared.m_name), Health = shared.m_food, Stamina = shared.m_foodStamina, Eitr = shared.m_foodEitr, BurnTime = shared.m_foodBurnTime, Forage = !MadeFoods.Contains(shared.m_name) }; } internal static string Localize(string token) { if (Localization.instance == null) { return token; } return Localization.instance.Localize(token); } private unsafe static string BiomeName(Biome biome) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if ((int)biome == 0) { return "the wilds"; } string text = Localize("$biome_" + ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString().ToLowerInvariant()); if (!text.StartsWith("[")) { return text; } return ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString(); } } internal static class PantryText { private const string HealthColor = "#FF8080"; private const string StaminaColor = "#FFE680"; private const string EitrColor = "#A8A8FF"; private const string Heading = "orange"; private const string FlavorColor = "#B9B3A8"; private const string NoteColor = "#B0B0B0"; private static readonly StringBuilder Sb = new StringBuilder(); private static readonly Dictionary<Biome, float> HealthWanted = new Dictionary<Biome, float> { [(Biome)1] = 40f, [(Biome)8] = 70f, [(Biome)2] = 100f, [(Biome)256] = 100f, [(Biome)4] = 120f, [(Biome)16] = 150f, [(Biome)512] = 180f, [(Biome)32] = 220f, [(Biome)64] = 220f }; internal static bool TryGetHealthWanted(Biome biome, out float wanted) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) return HealthWanted.TryGetValue(biome, out wanted); } internal static string Title() { return "Odin's Pantry"; } internal static string Body(PantryResult p, string remark) { //IL_0687: Unknown result type (might be due to invalid IL or missing references) Sb.Clear(); int value = Plugin.MaxPerList.Value; if (Plugin.ShowFlavor.Value && !string.IsNullOrEmpty(remark)) { Sb.Append("<i><color=").Append("#B9B3A8").Append(">\"") .Append(remark) .Append("\"</color></i>\n"); } if (Plugin.ShowBelly.Value) { float num = p.BaseHealth; float num2 = p.BaseStamina; float num3 = 0f; foreach (BellyFood item in p.Belly) { num += item.Health; num2 += item.Stamina; num3 += item.Eitr; } Section("In your belly", "<color=#B0B0B0>max</color> " + Stats(num, num2, num3)); foreach (BellyFood item2 in p.Belly) { Sb.Append(item2.Food.Name).Append(" ").Append(Stats(item2.Health, item2.Stamina, item2.Eitr)) .Append(" <color=") .Append("#B0B0B0") .Append('>') .Append(Duration(item2.SecondsLeft)); if (item2.CanTopUp) { Sb.Append(", can top up"); } Sb.Append("</color>\n"); } for (int i = p.Belly.Count; i < 3; i++) { Sb.Append("<color=").Append("#B0B0B0").Append(">(empty)</color>\n"); } } if (Plugin.ShowEatNow.Value) { Section("Ready to eat", Count(p.Eat.Count, value)); if (p.Eat.Count == 0) { Note("Nothing edible in reach."); } for (int j = 0; j < p.Eat.Count && j < value; j++) { EatOption eatOption = p.Eat[j]; Sb.Append(eatOption.Food.Name).Append(" <color=").Append("#B0B0B0") .Append(">x") .Append(eatOption.Count) .Append("</color> ") .Append(Stats(eatOption.Food.Health, eatOption.Food.Stamina, eatOption.Food.Eitr)); if (eatOption.StillEaten) { Sb.Append(" <color=").Append("#B0B0B0").Append(">still in you</color>"); } Sb.Append('\n'); } } if (Plugin.ShowCookNow.Value) { Section("Ready to cook", Count(p.Cook.Count, value)); if (p.Cook.Count == 0) { Note("Nothing you have every ingredient for."); } for (int k = 0; k < p.Cook.Count && k < value; k++) { CookOption cookOption = p.Cook[k]; Sb.Append(cookOption.Food.Name).Append(" <color=").Append("#B0B0B0") .Append(">x") .Append(cookOption.Batches * cookOption.PerBatch) .Append("</color> ") .Append(Stats(cookOption.Food.Health, cookOption.Food.Stamina, cookOption.Food.Eitr)) .Append(Where(cookOption)) .Append('\n'); } } if (Plugin.ShowOneShort.Value && p.OneShort.Count > 0) { Section("One ingredient short", Count(p.OneShort.Count, value)); for (int l = 0; l < p.OneShort.Count && l < value; l++) { CookOption cookOption2 = p.OneShort[l]; Sb.Append(cookOption2.Food.Name).Append(" <color=").Append("#B0B0B0") .Append(">needs ") .Append(cookOption2.MissingAmount) .Append(' ') .Append(cookOption2.MissingName) .Append("</color>") .Append('\n'); } } if (Plugin.ShowSuggestion.Value) { Section("Odin's pick for the " + p.BiomeName); if (p.Picks.Count == 0) { Note("Nothing to pick from. Odin suggests foraging."); } foreach (Pick pick in p.Picks) { Sb.Append(pick.Food.Name).Append(" ").Append(Stats(pick.Food.Health, pick.Food.Stamina, pick.Food.Eitr)); if (pick.InBelly) { Sb.Append(" <color=").Append("#B0B0B0").Append(">eaten</color>"); } else if (pick.NeedsCooking) { Sb.Append(" <color=").Append("#B0B0B0").Append(">cook first</color>"); } Sb.Append('\n'); } if (p.Picks.Count > 0) { Sb.Append("<color=").Append("#B0B0B0").Append(">Together: </color>") .Append(Stats(p.PickHealth, p.PickStamina, p.PickEitr)) .Append('\n'); if (HealthWanted.TryGetValue(p.Biome, out var value2)) { Note((p.PickHealth >= value2) ? ("Enough for the " + p.BiomeName + ".") : $"Odin would want {value2:0} health from food here. That's {value2 - p.PickHealth:0} short."); } } } if (p.ChestsSearched > 0) { Sb.Append("<size=80%><color=").Append("#B0B0B0").Append(">Counting your bag and ") .Append(p.ChestsSearched) .Append((p.ChestsSearched == 1) ? " chest" : " chests") .Append(" within ") .Append(Plugin.SearchRadius.Value.ToString("0", CultureInfo.InvariantCulture)) .Append(" m.</color></size>"); } return Sb.ToString().TrimEnd(new char[1] { '\n' }); } private static void Section(string title, string aside = null) { if (Sb.Length > 0) { Sb.Append('\n'); } Sb.Append("<color=").Append("orange").Append("><b>") .Append(title) .Append("</b></color>"); if (aside != null) { Sb.Append(" ").Append(aside); } Sb.Append('\n'); } private static void Note(string text) { Sb.Append("<color=").Append("#B0B0B0").Append('>') .Append(text) .Append("</color>\n"); } private static string Count(int count, int max) { if (count <= max) { return null; } return "<color=#B0B0B0>(" + count + ")</color>"; } private static string Where(CookOption o) { if (o.Station == null) { return ""; } string text = ((o.StationLevel > 1) ? (" lv " + o.StationLevel) : ""); string text2 = (o.StationNearby ? "#B0B0B0" : "#FF9060"); string text3 = (o.StationNearby ? (o.Station + text) : ("needs " + o.Station + text)); return " <color=" + text2 + ">" + text3 + "</color>"; } internal static string Stats(float health, float stamina, float eitr, bool showZero = false) { List<string> list = new List<string>(3); if (health >= 0.5f || showZero) { list.Add(string.Format("<color={0}>{1:0} hp</color>", "#FF8080", health)); } if (stamina >= 0.5f || showZero) { list.Add(string.Format("<color={0}>{1:0} st</color>", "#FFE680", stamina)); } if (eitr >= 0.5f) { list.Add(string.Format("<color={0}>{1:0} eitr</color>", "#A8A8FF", eitr)); } return string.Join(" ", list); } private static string Duration(float seconds) { if (float.IsInfinity(seconds)) { return "never runs out"; } int num = (int)seconds; if (num >= 60) { return $"{num / 60}m {num % 60:00}s left"; } return $"{num}s left"; } } internal class PantryTooltip : MonoBehaviour { private const float RefreshInterval = 1f; private GameObject _tooltip; private RectTransform _panel; private TMP_Text _topic; private TMP_Text _text; private readonly Vector3[] _corners = (Vector3[])(object)new Vector3[4]; private CanvasGroup _group; private float _shownAt; private float _fadeOutStart = -1f; private float _fadeOutLength; private float _nextRefresh; private bool _freshOpen; private string _remark; private void LateUpdate() { bool num = CanShow(); if (num && ShowKeyDown()) { if (IsOpen() && !IsFading()) { Close(); } else { Open(); } } if (!num || !IsOpen()) { Hide(); } else { if ((Object)(object)_tooltip == (Object)null && !CreateTooltip()) { return; } if (Time.time >= _nextRefresh) { _nextRefresh = Time.time + 1f; PantryResult pantryResult = Pantry.Compute(_freshOpen); if (pantryResult == null) { Hide(); return; } if (_freshOpen) { _remark = FlavorText.Pick(pantryResult); } _freshOpen = false; _topic.text = PantryText.Title(); _text.text = PantryText.Body(pantryResult, _remark); LayoutRebuilder.ForceRebuildLayoutImmediate(_panel); } if (!_tooltip.activeSelf) { _tooltip.SetActive(true); _tooltip.transform.SetAsLastSibling(); } _group.alpha = Alpha(); Place(); } } private void OnDestroy() { if ((Object)(object)_tooltip != (Object)null) { Object.Destroy((Object)(object)_tooltip); } } private bool IsOpen() { return Time.time < _fadeOutStart + _fadeOutLength; } private bool IsFading() { return Time.time >= _fadeOutStart; } private void Open() { float num = (IsOpen() ? Alpha() : 0f); _shownAt = Time.time - num * Plugin.FadeInSeconds.Value; _fadeOutStart = Time.time + Plugin.ShowSeconds.Value; _fadeOutLength = Plugin.FadeOutSeconds.Value; _nextRefresh = 0f; _freshOpen = true; } private void Close() { _fadeOutStart = Time.time; _fadeOutLength = Plugin.FadeInSeconds.Value; } private float Alpha() { float value = Plugin.FadeInSeconds.Value; float num = ((value > 0f) ? Mathf.Clamp01((Time.time - _shownAt) / value) : 1f); float num2 = ((_fadeOutLength > 0f) ? Mathf.Clamp01((Time.time - _fadeOutStart) / _fadeOutLength) : (IsFading() ? 1f : 0f)); return num * (1f - num2); } private static bool CanShow() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsDead() || !HudVisible()) { return false; } if (!InventoryGui.IsVisible() && !Menu.IsVisible() && !Minimap.IsOpen() && !TextInput.IsVisible() && !Console.IsVisible()) { if ((Object)(object)Chat.instance != (Object)null) { return !Chat.instance.HasFocus(); } return true; } return false; } private static bool HudVisible() { if ((Object)(object)Hud.instance != (Object)null && !Hud.IsUserHidden()) { return Hud.instance.IsVisible(); } return false; } private static bool ShowKeyDown() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = Plugin.ShowKey.Value; if ((int)((KeyboardShortcut)(ref value)).MainKey == 0 || !ZInput.GetKeyDown(((KeyboardShortcut)(ref value)).MainKey, false)) { return false; } foreach (KeyCode modifier in ((KeyboardShortcut)(ref value)).Modifiers) { if (!ZInput.GetKey(modifier, false)) { return false; } } return true; } private void Place() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: 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_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007c: 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_008e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: 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_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) Vector2 val = default(Vector2); ((Vector2)(ref val))..ctor(Plugin.PositionX.Value * (float)Screen.width, Plugin.PositionY.Value * (float)Screen.height); _tooltip.transform.position = Vector2.op_Implicit(val); _panel.GetWorldCorners(_corners); Vector2 val2 = Vector2.op_Implicit((_corners[0] + _corners[2]) / 2f); Transform transform = _tooltip.transform; transform.position += Vector2.op_Implicit(val - val2); Utils.ClampUIToScreen(_panel); } private bool CreateTooltip() { GameObject val = FindTooltipPrefab(); if ((Object)(object)val == (Object)null) { return false; } Canvas componentInParent = ((Component)InventoryGui.instance).GetComponentInParent<Canvas>(); Canvas val2 = ((componentInParent != null) ? componentInParent.rootCanvas : null); if ((Object)(object)val2 == (Object)null) { return false; } _tooltip = Object.Instantiate<GameObject>(val, ((Component)val2).transform); ((Object)_tooltip).name = "OdinsPantry"; ref RectTransform panel = ref _panel; Transform child = _tooltip.transform.GetChild(0); panel = (RectTransform)(object)((child is RectTransform) ? child : null); Transform obj = Utils.FindChild(_tooltip.transform, "Topic", (IterativeSearchType)0); _topic = ((obj != null) ? ((Component)obj).GetComponent<TMP_Text>() : null); Transform obj2 = Utils.FindChild(_tooltip.transform, "Text", (IterativeSearchType)0); _text = ((obj2 != null) ? ((Component)obj2).GetComponent<TMP_Text>() : null); if ((Object)(object)_panel == (Object)null || (Object)(object)_topic == (Object)null || (Object)(object)_text == (Object)null) { Object.Destroy((Object)(object)_tooltip); _tooltip = null; return false; } Graphic[] componentsInChildren = _tooltip.GetComponentsInChildren<Graphic>(true); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].raycastTarget = false; } _group = _tooltip.GetComponent<CanvasGroup>(); if ((Object)(object)_group == (Object)null) { _group = _tooltip.AddComponent<CanvasGroup>(); } _group.interactable = false; _group.blocksRaycasts = false; _group.alpha = 0f; return true; } private static GameObject FindTooltipPrefab() { InventoryGui instance = InventoryGui.instance; if ((Object)(object)instance == (Object)null) { return null; } UITooltip val = (((Object)(object)instance.m_playerGrid != (Object)null && (Object)(object)instance.m_playerGrid.m_elementPrefab != (Object)null) ? instance.m_playerGrid.m_elementPrefab.GetComponentInChildren<UITooltip>(true) : null); if ((Object)(object)val != (Object)null && (Object)(object)val.m_tooltipPrefab != (Object)null) { return val.m_tooltipPrefab; } UITooltip[] componentsInChildren = ((Component)instance).GetComponentsInChildren<UITooltip>(true); foreach (UITooltip val2 in componentsInChildren) { if ((Object)(object)val2.m_tooltipPrefab != (Object)null) { return val2.m_tooltipPrefab; } } return null; } private void Hide() { if ((Object)(object)_tooltip != (Object)null && _tooltip.activeSelf) { _tooltip.SetActive(false); } } } public enum FoodPreference { Balanced, Health, Stamina, Eitr } [BepInPlugin("hevyh2o.valheim.odinspantry", "Odin's Pantry", "0.1.2")] public class Plugin : BaseUnityPlugin { public const string Guid = "hevyh2o.valheim.odinspantry"; public const string Name = "Odin's Pantry"; public const string Version = "0.1.2"; internal static ManualLogSource Log; internal static ConfigEntry<KeyboardShortcut> ShowKey; internal static ConfigEntry<float> ShowSeconds; internal static ConfigEntry<float> FadeInSeconds; internal static ConfigEntry<float> FadeOutSeconds; internal static ConfigEntry<float> PositionX; internal static ConfigEntry<float> PositionY; internal static ConfigEntry<float> SearchRadius; internal static ConfigEntry<FoodPreference> Preference; internal static ConfigEntry<bool> OnlyKnownRecipes; internal static ConfigEntry<int> MaxPerList; internal static ConfigEntry<bool> ShowBelly; internal static ConfigEntry<bool> ShowEatNow; internal static ConfigEntry<bool> ShowCookNow; internal static ConfigEntry<bool> ShowOneShort; internal static ConfigEntry<bool> ShowSuggestion; internal static ConfigEntry<bool> ShowFlavor; private PantryTooltip _tooltip; private void Awake() { //IL_0022: 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_006e: Expected O, but got Unknown //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected O, but got Unknown //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Expected O, but got Unknown //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Expected O, but got Unknown //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Expected O, but got Unknown //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Expected O, but got Unknown //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ShowKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Display", "ShowKey", new KeyboardShortcut((KeyCode)112, Array.Empty<KeyCode>()), "Key that opens the pantry. Press again to close it early, or while it's fading to bring it back."); ShowSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "ShowSeconds", 20f, new ConfigDescription("How long the pantry stays fully visible after pressing ShowKey, before it starts to fade.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 120f), Array.Empty<object>())); FadeInSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "FadeInSeconds", 0.3f, new ConfigDescription("How long the pantry takes to fade in. Also used when closing it with ShowKey. 0 = instant.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>())); FadeOutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "FadeOutSeconds", 5f, new ConfigDescription("How long the pantry takes to fade away once ShowSeconds is up. 0 = vanish at once.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); PositionX = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "PositionX", 0.5f, new ConfigDescription("Horizontal position of the pantry, 0 = left edge, 1 = right edge.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); PositionY = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "PositionY", 0.6f, new ConfigDescription("Vertical position of the pantry, 0 = bottom, 1 = top.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); SearchRadius = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SearchRadius", 20f, new ConfigDescription("How far, in metres, to look for chests whose food and ingredients count. 0 = your own inventory only.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); Preference = ((BaseUnityPlugin)this).Config.Bind<FoodPreference>("General", "Preference", FoodPreference.Balanced, "What Odin's suggested meal leans towards: Balanced, Health, Stamina or Eitr."); OnlyKnownRecipes = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "OnlyKnownRecipes", true, "Only list recipes you've already discovered, so the pantry doesn't spoil what's to come."); MaxPerList = ((BaseUnityPlugin)this).Config.Bind<int>("General", "MaxPerList", 4, new ConfigDescription("How many foods to list in each section. When there are more, the heading shows the full count.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 15), Array.Empty<object>())); ShowBelly = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowBelly", true, "Your three active foods, their time left and what they give."); ShowEatNow = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowEatNow", true, "Food you could eat right now."); ShowCookNow = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowCookNow", true, "Meals you have every ingredient for."); ShowOneShort = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowOneShort", true, "Meals that are one ingredient short, and what's missing."); ShowSuggestion = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowSuggestion", true, "Odin's pick of the best three foods within reach, judged against your biome."); ShowFlavor = ((BaseUnityPlugin)this).Config.Bind<bool>("Sections", "ShowFlavor", true, "Odin's one-line remark about your eating habits."); _tooltip = ((Component)this).gameObject.AddComponent<PantryTooltip>(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Odin's Pantry 0.1.2 loaded. Odin has counted the turnips."); } private void OnDestroy() { if ((Object)(object)_tooltip != (Object)null) { Object.Destroy((Object)(object)_tooltip); } } } internal static class BuildInfo { public const string Version = "0.1.2"; } }