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 FeastMaster v4.7.5
plugins\FeastMaster.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Charter; using ConfigReload; using HarmonyLib; using ItemCopies; using Microsoft.CodeAnalysis; using PatchGuard; using SyncedConfig; using TMPro; using UnityEngine; using UnityEngine.UI; using YamlConfig; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_guiutils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("FeastMaster")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("FeastMaster: configure food and mead values in Valheim")] [assembly: AssemblyFileVersion("4.7.5.0")] [assembly: AssemblyInformationalVersion("4.7.5+e50f37f63d8e3c570a7a95a035e18c20dc2ae350")] [assembly: AssemblyProduct("FeastMaster")] [assembly: AssemblyTitle("FeastMaster")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("4.7.5.0")] [module: UnverifiableCode] [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 FeastMaster { [HarmonyPatch(typeof(Player), "UpdateFood")] public static class AutoEatPatch { private static bool eating; public static bool Prepare() { if (Settings.AllowAutoEat.Value) { return Settings.AutoEat.Value; } return false; } [HarmonyPrefix] public static void Prefix(Player __instance, out Food __state) { __state = (eating ? null : Expiring(__instance)); } [HarmonyPostfix] public static void Postfix(Player __instance, Food __state) { if (__state == null || __instance.m_foods.Contains(__state)) { return; } Inventory inventory = ((Humanoid)__instance).GetInventory(); ItemData item = inventory.GetItem(__state.m_item.m_shared.m_name, -1, false); if (item == null) { return; } eating = true; try { ((Humanoid)__instance).UseItem(inventory, item, true); } finally { eating = false; } } private static Food Expiring(Player player) { if ((Object)(object)player != (Object)(object)Player.m_localPlayer || !Settings.AllowAutoEat.Value || !Settings.AutoEat.Value) { return null; } foreach (Food food in player.m_foods) { if (food.m_time <= 1f) { return food; } } return null; } } [HarmonyPatch(typeof(Player), "GetTotalFoodValue")] public static class BaseValuesPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase)Settings.BaseHealth, (ConfigEntryBase)Settings.BaseStamina, (ConfigEntryBase)Settings.RunSkillStamina, (ConfigEntryBase)Settings.JumpSkillStamina, (ConfigEntryBase)Settings.SneakSkillStamina, (ConfigEntryBase)Settings.SwimSkillStamina, (ConfigEntryBase)Settings.FishingSkillStamina); } public static void Removed() { BaseOriginals.RestoreAll(); } [HarmonyPrefix] public static void Prefix(Player __instance) { BaseOriginals.Write(__instance); } [HarmonyPostfix] public static void Postfix(Player __instance, ref float stamina) { stamina += SkillBonus(__instance); } public static float SkillBonus(Player player) { return Bonus(player, (SkillType)102, Settings.RunSkillStamina.Value) + Bonus(player, (SkillType)100, Settings.JumpSkillStamina.Value) + Bonus(player, (SkillType)101, Settings.SneakSkillStamina.Value) + Bonus(player, (SkillType)103, Settings.SwimSkillStamina.Value) + Bonus(player, (SkillType)104, Settings.FishingSkillStamina.Value); } private static float Bonus(Player player, SkillType skill, float atSkill100) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (atSkill100 != 0f) { return atSkill100 * ((Character)player).GetSkillFactor(skill); } return 0f; } } public static class BaseOriginals { private sealed class Kept { public float? Health; public float? Stamina; } private static readonly ConditionalWeakTable<Player, Kept> kept = new ConditionalWeakTable<Player, Kept>(); public static void Write(Player player) { Kept orCreateValue = kept.GetOrCreateValue(player); orCreateValue.Health = Replace(ref player.m_baseHP, Settings.BaseHealth, orCreateValue.Health); orCreateValue.Stamina = Replace(ref player.m_baseStamina, Settings.BaseStamina, orCreateValue.Stamina); } private static float? Replace(ref float field, ConfigEntry<float> setting, float? own) { if (Customized.Any(setting)) { float value = own ?? field; field = setting.Value; return value; } if (own.HasValue) { field = own.Value; } return null; } public static void RestoreAll() { foreach (Player s_player in Player.s_players) { if (!((Object)(object)s_player == (Object)null) && kept.TryGetValue(s_player, out var value)) { if (value.Health.HasValue) { s_player.m_baseHP = value.Health.Value; } if (value.Stamina.HasValue) { s_player.m_baseStamina = value.Stamina.Value; } kept.Remove(s_player); } } } } public static class ChangedRules { public static bool FoodValues { get; private set; } public static bool Vigor { get; private set; } public static bool EitrVigor { get; private set; } public static bool StaminaRegenRules { get; private set; } public static bool RegenBasics { get; private set; } public static bool EncumberedCost { get; private set; } public static bool RestrictedRegen { get; private set; } public static void Refresh() { FoodValues = Customized.AnyFoodValues(); Vigor = Customized.AnyFood("Vigor") || Customized.Any(Settings.VigorPerStaminaPoint); EitrVigor = Customized.AnyFood("EitrVigor") || Customized.Any(Settings.EitrVigorPerEitrPoint); StaminaRegenRules = StaminaRegenMultiplierPatch.Rules(); RegenBasics = RegenBasicsRule.Rules(); EncumberedCost = EncumberedCostRule.Rules(); RestrictedRegen = RestrictedStaminaRegen.Rules(); if (FoodValues) { ItemValues.HookSpawns(); } } } public static class ConfigMigration { private static readonly MethodInfo orphansGetter = AccessTools.PropertyGetter(typeof(ConfigFile), "OrphanedEntries"); public static string TakeOrphan(ConfigFile config, string oldSection, string newSection, string key) { //IL_0003: 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_0014: Expected O, but got Unknown //IL_0014: Expected O, but got Unknown return Take(config, new ConfigDefinition(oldSection, key), new ConfigDefinition(newSection, key)); } public static string TakeRenamed(ConfigFile config, string section, string oldKey, string newKey) { //IL_0003: 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_0014: Expected O, but got Unknown //IL_0014: Expected O, but got Unknown return Take(config, new ConfigDefinition(section, oldKey), new ConfigDefinition(section, newKey)); } public static void Apply(ConfigEntryBase entry, string text) { if (text != null) { entry.SetSerializedValue(text); } } public static void ApplyDivided(ConfigEntry<float> entry, string text, float divisor) { if (text != null && float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { entry.Value = result / divisor; } } private static string Take(ConfigFile config, ConfigDefinition old, ConfigDefinition current) { Dictionary<ConfigDefinition, string> dictionary = Orphans(config); if (dictionary == null || !dictionary.TryGetValue(old, out var value)) { return null; } dictionary.Remove(old); if (!dictionary.ContainsKey(current)) { return value; } return null; } private static Dictionary<ConfigDefinition, string> Orphans(ConfigFile config) { if (orphansGetter == null) { FeastMaster.Log.LogWarning((object)"ConfigFile.OrphanedEntries not found; old config sections are not migrated."); return null; } return orphansGetter.Invoke(config, null) as Dictionary<ConfigDefinition, string>; } } public struct CookingSwap { public bool CanOvercook; public float[] Times; } [HarmonyPatch(typeof(CookingStation), "UpdateCooking")] public static class CookingPatch { public static bool Prepare() { if (!Customized.Any((ConfigEntryBase)Settings.CookTimeMultiplier, (ConfigEntryBase)Settings.FoodCanBurn)) { return CookTimes.AnyChanged(); } return true; } [HarmonyPrefix] public static void Prefix(CookingStation __instance, out CookingSwap __state) { __state = CookingRules.Swap(__instance); } [HarmonyFinalizer] public static void Finalizer(CookingStation __instance, CookingSwap __state) { CookingRules.Restore(__instance, __state); } } public static class CookingRules { private static float[] buffer = new float[8]; private static bool bufferInUse; public static CookingSwap Swap(CookingStation station) { ZNetView nview = station.m_nview; if ((Object)(object)nview == (Object)null || !nview.IsValid() || !nview.IsOwner()) { return default(CookingSwap); } CookingSwap result = new CookingSwap { CanOvercook = station.m_canOvercookItems }; station.m_canOvercookItems &= Settings.FoodCanBurn.Value; CookTimes.TryGet(nview.GetZDO().GetPrefab(), out var entries); result.Times = SwapTimes(station.m_conversion, entries, Settings.CookTimeMultiplier.Value); return result; } private static float[] Times(int count) { if (bufferInUse) { return new float[count]; } if (buffer.Length < count) { buffer = new float[count]; } bufferInUse = true; return buffer; } private static float[] SwapTimes(List<ItemConversion> recipes, Dictionary<ItemDrop, ConfigEntry<float>> entries, float multiplier) { float[] array = Times(recipes.Count); for (int i = 0; i < recipes.Count; i++) { ItemConversion val = recipes[i]; if (val != null) { array[i] = val.m_cookTime; ConfigEntry<float> value = null; bool flag = entries != null && (Object)(object)val.m_from != (Object)null && entries.TryGetValue(val.m_from, out value); val.m_cookTime = (flag ? value.Value : val.m_cookTime) * multiplier; } } return array; } public static void Restore(CookingStation station, CookingSwap saved) { if (saved.Times == null) { return; } station.m_canOvercookItems = saved.CanOvercook; List<ItemConversion> conversion = station.m_conversion; for (int i = 0; i < saved.Times.Length && i < conversion.Count; i++) { if (conversion[i] != null) { conversion[i].m_cookTime = saved.Times[i]; } } if (saved.Times == buffer) { bufferInUse = false; } } } public static class CookTimes { private static readonly Dictionary<int, Dictionary<ItemDrop, ConfigEntry<float>>> byStation = new Dictionary<int, Dictionary<ItemDrop, ConfigEntry<float>>>(); public static bool TryGet(int prefabHash, out Dictionary<ItemDrop, ConfigEntry<float>> entries) { return byStation.TryGetValue(prefabHash, out entries); } public static bool AnyChanged() { foreach (Dictionary<ItemDrop, ConfigEntry<float>> value in byStation.Values) { foreach (ConfigEntry<float> value2 in value.Values) { if (Customized.Any(value2)) { return true; } } } return false; } public static bool Bind(string prefabName, CookingStation station) { int stableHashCode = StringExtensionMethods.GetStableHashCode(prefabName); if (byStation.ContainsKey(stableHashCode)) { return false; } try { byStation[stableHashCode] = BindRecipes(prefabName, station.m_conversion); return true; } catch (ArgumentException ex) { FeastMaster.Log.LogWarning((object)("Cook times of " + prefabName + " cannot be configured: " + ex.Message)); return false; } } private static Dictionary<ItemDrop, ConfigEntry<float>> BindRecipes(string station, List<ItemConversion> recipes) { Dictionary<ItemDrop, ConfigEntry<float>> dictionary = new Dictionary<ItemDrop, ConfigEntry<float>>(); foreach (ItemConversion recipe in recipes) { if (recipe != null && !((Object)(object)recipe.m_from == (Object)null) && !((Object)(object)recipe.m_to == (Object)null) && !dictionary.ContainsKey(recipe.m_from)) { dictionary[recipe.m_from] = FeastMaster.Synced.Bind(station, ((Object)recipe.m_from).name, recipe.m_cookTime, "Seconds " + ((Object)recipe.m_from).name + " takes to cook into " + ((Object)recipe.m_to).name + " on this station. Cook Time Multiplier in section 9 applies on top.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0.1f, 86400f)); } } return dictionary; } } public struct ScaledFields { public bool Set; public float First; public float Second; public float Third; } public struct ScaledGroup { public ScaledFields First; public ScaledFields Second; public ScaledFields Third; } public static class CostRules { public static bool OutOfCombat(Player player) { if (!player.IsTargeted() && !player.IsSensed()) { return !((Character)player).InAttack(); } return false; } public static float OutOfCombatFactor(Player player, float outOfCombatCost) { if (outOfCombatCost == 1f) { return 1f; } if (!OutOfCombat(player)) { return 1f; } return outOfCombatCost; } public static float SkillDiscountFactor(Player player, SkillType skill) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) float value = Settings.SkillDiscount.Value; if (value == 0f) { return 1f; } return Mathf.Max(0f, 1f - value / 100f * ((Character)player).GetSkillFactor(skill)); } public static bool SneakingIsFree(Player player) { if (!Settings.FreeSneakingWithoutEnemies.Value) { return false; } if (!BaseAI.InStealthRange((Character)(object)player) && !player.IsSensed()) { return !player.IsTargeted(); } return false; } public static ScaledFields Scale(ref float field, float multiplier) { if (multiplier == 1f) { return default(ScaledFields); } ScaledFields result = new ScaledFields { Set = true, First = field }; field *= multiplier; return result; } public static ScaledFields Scale(ref float first, ref float second, float multiplier) { if (multiplier == 1f) { return default(ScaledFields); } ScaledFields result = new ScaledFields { Set = true, First = first, Second = second }; first *= multiplier; second *= multiplier; return result; } public static ScaledFields Swap(ref float field, float value) { if (field == value) { return default(ScaledFields); } ScaledFields result = new ScaledFields { Set = true, First = field }; field = value; return result; } public static void Restore(ref float field, ScaledFields saved) { if (saved.Set) { field = saved.First; } } public static void Restore(ref float first, ref float second, ScaledFields saved) { if (saved.Set) { first = saved.First; second = saved.Second; } } } public static class Customized { public static bool Any(params ConfigEntryBase[] entries) { foreach (ConfigEntryBase val in entries) { if (val != null && !Same(val.BoxedValue, val.DefaultValue)) { return true; } } return false; } public static bool Any(ConfigEntry<float> entry) { if (entry != null) { return !Same(entry.Value, (float)((ConfigEntryBase)entry).DefaultValue); } return false; } private static bool Same(object value, object defaultValue) { if (value is float a && defaultValue is float b) { return Same(a, b); } return object.Equals(value, defaultValue); } private static bool Same(float a, float b) { return Math.Abs(a - b) <= 1E-05f * Math.Max(1f, Math.Abs(b)); } public static bool AnyFood(string key) { foreach (Dictionary<string, ConfigEntry<float>> value in FeastMasterData.FoodConfigs.Values) { if (Any(value[key])) { return true; } } return false; } public static bool FoodValues(Dictionary<string, ConfigEntry<float>> configs) { if (!GlobalFoodValues() && !Any(configs["Health"]) && !Any(configs["Stamina"]) && !Any(configs["Duration"]) && !Any(configs["HealthRegen"])) { return Any(configs["Eitr"]); } return true; } public static bool AnyFoodValues() { if (GlobalFoodValues()) { return true; } foreach (Dictionary<string, ConfigEntry<float>> value in FeastMasterData.FoodConfigs.Values) { if (FoodValues(value)) { return true; } } return false; } private static bool GlobalFoodValues() { if (!Any(FeastMasterData.HealthModifier) && !Any(FeastMasterData.StaminaModifier) && !Any(FeastMasterData.DurationModifier) && !Any(FeastMasterData.HealthRegenModifier)) { return Any(FeastMasterData.EitrModifier); } return true; } public static bool Mead(MeadEffectConfig config) { return Any((ConfigEntryBase)config.Duration, (ConfigEntryBase)config.HealthOverTime, (ConfigEntryBase)config.StaminaOverTime, (ConfigEntryBase)config.EitrOverTime, (ConfigEntryBase)config.HealthRegenMultiplier, (ConfigEntryBase)config.StaminaRegenMultiplier, (ConfigEntryBase)config.EitrRegenMultiplier, (ConfigEntryBase)config.RunStaminaModifier, (ConfigEntryBase)config.JumpStaminaModifier); } public static bool AnyMead() { foreach (MeadEffectConfig value in FeastMasterData.MeadConfigs.Values) { if (Mead(value)) { return true; } } return false; } } public static class HudNumbers { public static void Show(TMP_Text text, bool show) { if (!((Object)(object)text == (Object)null)) { GameObject gameObject = ((Component)text).gameObject; if (gameObject.activeSelf != show) { gameObject.SetActive(show); } } } public static void ShowAgain(Func<Hud, TMP_Text> text) { if ((Object)(object)Hud.instance != (Object)null) { Show(text(Hud.instance), show: true); } } } [HarmonyPatch(typeof(Hud), "UpdateHealth")] public static class HideHealthNumberPatch { public static bool Prepare() { return Settings.HideHealthNumber.Value; } public static void Removed() { HudNumbers.ShowAgain((Hud hud) => hud.m_healthText); } [HarmonyPostfix] public static void Postfix(Hud __instance) { HudNumbers.Show(__instance.m_healthText, !Settings.HideHealthNumber.Value); } } [HarmonyPatch(typeof(Hud), "UpdateStamina")] public static class HideStaminaNumberPatch { public static bool Prepare() { return Settings.HideStaminaNumber.Value; } public static void Removed() { HudNumbers.ShowAgain((Hud hud) => hud.m_staminaText); } [HarmonyPostfix] public static void Postfix(Hud __instance) { HudNumbers.Show(__instance.m_staminaText, !Settings.HideStaminaNumber.Value); } } [HarmonyPatch(typeof(Hud), "UpdateEitr")] public static class HideEitrNumberPatch { public static bool Prepare() { return Settings.HideEitrNumber.Value; } public static void Removed() { HudNumbers.ShowAgain((Hud hud) => hud.m_eitrText); } [HarmonyPostfix] public static void Postfix(Hud __instance) { HudNumbers.Show(__instance.m_eitrText, !Settings.HideEitrNumber.Value); } } [HarmonyPatch(typeof(Hud), "UpdateFood")] public static class FoodTimersPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FoodTimersMode }); } [HarmonyPostfix] public static void Postfix(Hud __instance, Player player) { FoodTimers value = Settings.FoodTimersMode.Value; if (value != FoodTimers.Vanilla && __instance.m_foodTime != null) { int num = Mathf.Min(player.GetFoods().Count, __instance.m_foodTime.Length); for (int i = 0; i < num; i++) { HudNumbers.Show(__instance.m_foodTime[i], value == FoodTimers.Always); } } } } [HarmonyPatch(typeof(ItemData), "GetTooltip", new Type[] { typeof(ItemData), typeof(int), typeof(bool), typeof(float), typeof(int), typeof(bool) })] public static class VigorTooltipPatch { public const string VigorKey = "fm_vigor"; public const string VigorUnitKey = "fm_vigor_regen"; public const string EitrVigorKey = "fm_eitr_vigor"; public const string EitrVigorUnitKey = "fm_eitr_vigor_regen"; public static bool Prepare() { if (!Customized.Any((ConfigEntryBase)Settings.VigorPerStaminaPoint, (ConfigEntryBase)Settings.EitrVigorPerEitrPoint) && !Customized.AnyFood("Vigor")) { return Customized.AnyFood("EitrVigor"); } return true; } [HarmonyPostfix] public static void Postfix(ItemData item, ref string __result) { if (item != null && IsFood(item.m_shared)) { string text = Line("fm_vigor", "fm_vigor_regen", ItemValues.VigorOf(item)) + Line("fm_eitr_vigor", "fm_eitr_vigor_regen", ItemValues.EitrVigorOf(item)); if (text.Length > 0) { __result = __result.Insert(InsertAt(__result), text); } } } private static string Line(string key, string unitKey, float percent) { if (percent == 0f) { return ""; } return "\n$" + key + ": <color=orange>" + percent.ToString("+0.#;-0.#") + "% $" + unitKey + "</color>"; } private static bool IsFood(SharedData shared) { if (!(shared.m_food > 0f) && !(shared.m_foodStamina > 0f)) { return shared.m_foodEitr > 0f; } return true; } private static int InsertAt(string tooltip) { int num = tooltip.IndexOf("$item_food_regen", StringComparison.Ordinal); if (num < 0) { num = tooltip.IndexOf("$item_food_duration", StringComparison.Ordinal); } if (num < 0) { return tooltip.Length; } int num2 = tooltip.IndexOf('\n', num); if (num2 >= 0) { return num2; } return tooltip.Length; } } [HarmonyPatch(typeof(Localization), "SetupLanguage")] public static class VigorWordsPatch { [HarmonyPostfix] public static void Postfix(Localization __instance) { __instance.AddWord("fm_vigor", "Vigor"); __instance.AddWord("fm_vigor_regen", "stamina regen"); __instance.AddWord("fm_eitr_vigor", "Eitr Vigor"); __instance.AddWord("fm_eitr_vigor_regen", "eitr regen"); } } [HarmonyPatch(typeof(SEMan), "ModifyEitrRegen")] public static class EitrRegenMultiplierPatch { public static bool Prepare() { if (!Customized.AnyFood("EitrVigor")) { return Customized.Any((ConfigEntryBase)Settings.EitrVigorPerEitrPoint, (ConfigEntryBase)Settings.EitrVigorMultiplier, (ConfigEntryBase)Settings.EitrRegenCurveStrength, (ConfigEntryBase)Settings.EitrRegenCurvePivot, (ConfigEntryBase)Settings.BlockingEitrRegenFactor); } return true; } [HarmonyPostfix] public static void Postfix(SEMan __instance, ref float eitrMultiplier) { Character character = __instance.m_character; Player val = (Player)(object)((character is Player) ? character : null); if (val != null) { if (ChangedRules.EitrVigor) { eitrMultiplier += EitrVigor(val) / 100f; } eitrMultiplier *= CurveFactor(val); if (((Character)val).IsBlocking()) { eitrMultiplier *= Settings.BlockingEitrRegenFactor.Value / 0.8f; } } } public static float EitrVigor(Player player) { float num = 0f; foreach (Food food in player.m_foods) { num += ItemValues.EitrVigorOf(food.m_item); } return num; } private static float CurveFactor(Player player) { float maxEitr = ((Character)player).GetMaxEitr(); return RegenCurve.Factor((maxEitr > 0f) ? (player.GetEitr() / maxEitr) : 1f, Settings.EitrRegenCurveStrength.Value, Settings.EitrRegenCurvePivot.Value); } } [BepInPlugin("com.FeastMaster", "FeastMaster", "4.7.5")] public class FeastMaster : BaseUnityPlugin { public const string PluginGuid = "com.FeastMaster"; public const string PluginName = "FeastMaster"; public const string PluginVersion = "4.7.5"; public static ManualLogSource Log { get; private set; } public static SyncedConfiguration Synced { get; private set; } private void Awake() { //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Synced = new SyncedConfiguration((BaseUnityPlugin)(object)this, ((BaseUnityPlugin)this).Logger, "FeastMaster", "4.7.5"); FeastMasterData.Initialize(Synced); Settings.Initialize(Synced); ItemValues.HookConfig(((BaseUnityPlugin)this).Config); WorldRates.HookConfig(((BaseUnityPlugin)this).Config); ((BaseUnityPlugin)this).Config.SettingChanged += delegate { PatchSwitch.MarkDirty(); }; ((BaseUnityPlugin)this).Config.ConfigReloaded += delegate { PatchSwitch.MarkDirty(); }; Harmony val = new Harmony("com.FeastMaster"); ItemValues.UseHarmony(val); int num = PatchSwitch.Initialize(val, Assembly.GetExecutingAssembly()); Synced.Finish(val); Log.LogInfo((object)("Loading [FeastMaster 4.7.5]" + ((num > 0) ? $" with {num} failed patches, see above" : ""))); Guard.Install(val, ((BaseUnityPlugin)this).Logger, Assembly.GetExecutingAssembly()); } } public static class FeastMasterData { public const string GlobalSection = "0. Global Settings"; private const string OldLockSection = "General"; private const string OldMeadPrefix = "0Meads_"; public const string Health = "Health"; public const string Stamina = "Stamina"; public const string Duration = "Duration"; public const string HealthRegen = "HealthRegen"; public const string Eitr = "Eitr"; public const string Vigor = "Vigor"; public const string EitrVigor = "EitrVigor"; private static readonly Dictionary<string, string> foodsByToken = new Dictionary<string, string>(); private static SyncedConfiguration synced; public static Dictionary<string, Dictionary<string, ConfigEntry<float>>> FoodConfigs { get; } = new Dictionary<string, Dictionary<string, ConfigEntry<float>>>(); public static Dictionary<string, MeadEffectConfig> MeadConfigs { get; } = new Dictionary<string, MeadEffectConfig>(); public static Dictionary<int, MeadEffectConfig> MeadsByEffectHash { get; } = new Dictionary<int, MeadEffectConfig>(); public static ConfigEntry<float> HealthModifier { get; private set; } public static ConfigEntry<float> StaminaModifier { get; private set; } public static ConfigEntry<float> DurationModifier { get; private set; } public static ConfigEntry<float> HealthRegenModifier { get; private set; } public static ConfigEntry<float> EitrModifier { get; private set; } public static ConfigEntry<bool> DisableFoodDegradation { get; private set; } public static ConfigEntry<float> DegradationCurve { get; private set; } public static ConfigEntry<float> EatAgainAt { get; private set; } public static ConfigEntry<bool> LockConfiguration { get; private set; } public static void Initialize(SyncedConfiguration config) { synced = config; HealthModifier = config.Bind("0. Global Settings", "Health Modifier", 1f, "Multiplier for health value of all foods.", true, null); StaminaModifier = config.Bind("0. Global Settings", "Stamina Modifier", 1f, "Multiplier for stamina value of all foods.", true, null); DurationModifier = config.Bind("0. Global Settings", "Duration Modifier", 1f, "Multiplier for duration of all foods. Food Rate in section 7 also changes how fast food runs out; both apply.", true, null); HealthRegenModifier = config.Bind("0. Global Settings", "Health Regen Modifier", 1f, "Multiplier for health regeneration of all foods.", true, null); EitrModifier = config.Bind("0. Global Settings", "Eitr Modifier", 1f, "Multiplier for eitr value of all foods.", true, null); DisableFoodDegradation = config.Bind("0. Global Settings", "Disable Food Degradation", false, "When enabled, food stats do not decrease over time.", true, null); DegradationCurve = config.Bind("0. Global Settings", "Degradation Curve", 0.3f, "Exponent of the food strength curve: strength = remaining fraction ^ curve. 0.3 is the game's curve, 1 is linear, higher values make food fade sooner, 0 means no fading. Disable Food Degradation overrides it.", true, null); EatAgainAt = config.Bind("0. Global Settings", "Eat Again At", 0.5f, "Fraction of a food's duration below which it can be eaten again. The game uses 0.5; 1 allows it at any time, 0 only once the food has run out.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f)); BindLock(config); } private static void BindLock(SyncedConfiguration config) { string text = ConfigMigration.TakeOrphan(config.Config, "General", "0. Global Settings", "Lock Configuration"); LockConfiguration = config.BindLocking("0. Global Settings", "Lock Configuration", defaultValue: true, "Server only. When on, every player uses the server's values for this file and cannot override them locally."); ConfigMigration.Apply((ConfigEntryBase)(object)LockConfiguration, text); } public static void LoadConfigurations() { ObjectDB instance = ObjectDB.instance; if (!((Object)(object)instance == (Object)null) && instance.m_items.Count != 0) { Stopwatch stopwatch = Stopwatch.StartNew(); if (BindAll(instance)) { FeastMaster.Log.LogInfo((object)$"Loaded {FoodConfigs.Count} food and {MeadConfigs.Count} mead configurations in {stopwatch.ElapsedMilliseconds} ms."); } ItemValues.ApplyAll(); ChangedRules.Refresh(); PatchSwitch.MarkDirty(); } } private static bool BindAll(ObjectDB db) { int num = FoodConfigs.Count + MeadConfigs.Count; bool flag = false; ConfigFile config = synced.Config; bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; ItemValues.SuspendWhileBinding(suspend: true); try { foreach (GameObject item in db.m_items) { BindConsumable(item); } flag = Rested.Bind(db); } finally { config.SaveOnConfigSet = saveOnConfigSet; ItemValues.SuspendWhileBinding(suspend: false); } bool num2 = flag || FoodConfigs.Count + MeadConfigs.Count != num; if (num2 && saveOnConfigSet) { config.Save(); } return num2; } private static void BindConsumable(GameObject prefab) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 ItemDrop val = (((Object)(object)prefab != (Object)null) ? prefab.GetComponent<ItemDrop>() : null); if ((Object)(object)val == (Object)null) { return; } SharedData shared = val.m_itemData.m_shared; if ((int)shared.m_itemType != 2) { return; } if (shared.m_food > 0f || shared.m_foodStamina > 0f || shared.m_foodEitr > 0f) { BindFood(((Object)prefab).name, shared); ItemValues.RegisterFood(((Object)prefab).name); return; } StatusEffect consumeStatusEffect = shared.m_consumeStatusEffect; SE_Stats val2 = (SE_Stats)(object)((consumeStatusEffect is SE_Stats) ? consumeStatusEffect : null); if (val2 != null) { BindMead(((Object)prefab).name, val2); ItemValues.RegisterMead(((Object)prefab).name, val2); } } private static void BindFood(string prefabName, SharedData shared) { if (!FoodConfigs.ContainsKey(prefabName)) { FoodConfigs[prefabName] = new Dictionary<string, ConfigEntry<float>> { ["Health"] = synced.Bind(prefabName, "Health", shared.m_food, "Health value of " + prefabName + ".", true, null), ["Stamina"] = synced.Bind(prefabName, "Stamina", shared.m_foodStamina, "Stamina value of " + prefabName + ".", true, null), ["Duration"] = synced.Bind(prefabName, "Duration", shared.m_foodBurnTime, "Duration of " + prefabName + " in seconds.", true, null), ["HealthRegen"] = synced.Bind(prefabName, "HealthRegen", shared.m_foodRegen, "Health regeneration of " + prefabName + ".", true, null), ["Eitr"] = synced.Bind(prefabName, "Eitr", shared.m_foodEitr, "Eitr value of " + prefabName + ".", true, null), ["Vigor"] = synced.Bind(prefabName, "Vigor", 0f, "Extra Vigor of " + prefabName + ": percent added to stamina regeneration while the food is active, on top of its Stamina times Vigor Per Stamina Point. Does not fade with the food. Stacks with Regen Per Extra Stamina Point.", true, null), ["EitrVigor"] = synced.Bind(prefabName, "EitrVigor", 0f, "Extra Eitr Vigor of " + prefabName + ": percent added to eitr regeneration while the food is active, on top of its Eitr times Eitr Vigor Per Eitr Point. Does not fade with the food.", true, null) }; if (!string.IsNullOrEmpty(shared.m_name)) { foodsByToken[shared.m_name] = prefabName; } FoodLookup.Forget(); } } public static bool BindFeastFood(ItemDrop food) { SharedData shared = food.m_itemData.m_shared; string name = ((Object)((Component)food).gameObject).name; if (FoodConfigs.ContainsKey(name) || (shared.m_food <= 0f && shared.m_foodStamina <= 0f && shared.m_foodEitr <= 0f)) { return false; } BindFood(name, shared); ItemValues.RegisterFood(name); return true; } private static void BindMead(string prefabName, SE_Stats effect) { if (!MeadConfigs.ContainsKey(prefabName)) { MeadEffectConfig meadEffectConfig = new MeadEffectConfig { Duration = BindMigratedMead(prefabName, "Duration", ((StatusEffect)effect).m_ttl, "Duration of " + prefabName + " effect in seconds. While it runs, neither this mead nor another of its group (the healing meads, for example) can be drunk, so this is also the mead's cooldown."), HealthOverTime = BindMigratedMead(prefabName, "HealthOverTime", effect.m_healthOverTime, "Total health restored over time by " + prefabName + "."), StaminaOverTime = BindMigratedMead(prefabName, "StaminaOverTime", effect.m_staminaOverTime, "Total stamina restored over time by " + prefabName + "."), EitrOverTime = BindMigratedMead(prefabName, "EitrOverTime", effect.m_eitrOverTime, "Total eitr restored over time by " + prefabName + ".") }; BindMeadBonuses(prefabName, effect, meadEffectConfig); MeadConfigs[prefabName] = meadEffectConfig; MeadsByEffectHash[((StatusEffect)effect).NameHash()] = meadEffectConfig; } } private static ConfigEntry<float> BindMigratedMead(string prefabName, string key, float defaultValue, string description) { string text = ConfigMigration.TakeOrphan(synced.Config, "0Meads_" + prefabName, prefabName, key); ConfigEntry<float> obj = synced.Bind(prefabName, key, defaultValue, description, true, null); ConfigMigration.Apply((ConfigEntryBase)(object)obj, text); return obj; } private static void BindMeadBonuses(string prefabName, SE_Stats effect, MeadEffectConfig config) { config.HealthRegenMultiplier = synced.Bind(prefabName, "HealthRegenMultiplier", effect.m_healthRegenMultiplier, "Health regeneration multiplier while " + prefabName + " lasts (1 = no change).", true, null); config.StaminaRegenMultiplier = synced.Bind(prefabName, "StaminaRegenMultiplier", effect.m_staminaRegenMultiplier, "Stamina regeneration multiplier while " + prefabName + " lasts (1 = no change). Combines with the rules in section 2 like the game's own meads.", true, null); config.EitrRegenMultiplier = synced.Bind(prefabName, "EitrRegenMultiplier", effect.m_eitrRegenMultiplier, "Eitr regeneration multiplier while " + prefabName + " lasts (1 = no change). Combines with the rules in section 3.", true, null); config.RunStaminaModifier = synced.Bind(prefabName, "RunStaminaModifier", effect.m_runStaminaDrainModifier, "Added fraction of the run stamina drain while " + prefabName + " lasts: -0.2 makes running 20% cheaper.", true, null); config.JumpStaminaModifier = synced.Bind(prefabName, "JumpStaminaModifier", effect.m_jumpStaminaUseModifier, "Added fraction of the jump stamina cost while " + prefabName + " lasts: -0.2 makes jumping 20% cheaper.", true, null); } public static bool TryGetFood(ItemData item, out Dictionary<string, ConfigEntry<float>> configs) { configs = null; if (item != null) { return FoodLookup.TryGet(item, out configs); } return false; } internal static bool FindFood(ItemData item, out Dictionary<string, ConfigEntry<float>> configs) { configs = null; if ((Object)(object)item.m_dropPrefab != (Object)null && FoodConfigs.TryGetValue(((Object)item.m_dropPrefab).name, out configs)) { return true; } if (item.m_shared.m_name != null && foodsByToken.TryGetValue(item.m_shared.m_name, out var value)) { return FoodConfigs.TryGetValue(value, out configs); } return false; } } public static class FeastServings { public static int Swap(Feast feast) { int value = Settings.FeastServings.Value; if (value <= 0 || feast.m_eatStacks == value) { return -1; } int eatStacks = feast.m_eatStacks; feast.m_eatStacks = value; return eatStacks; } public static void Restore(Feast feast, int own) { if (own >= 0) { feast.m_eatStacks = own; } } } [HarmonyPatch(typeof(Feast), "GetStack")] public static class FeastStackPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FeastServings }); } [HarmonyPrefix] public static void Prefix(Feast __instance, out int __state) { __state = FeastServings.Swap(__instance); } [HarmonyFinalizer] public static void Finalizer(Feast __instance, int __state) { FeastServings.Restore(__instance, __state); } } [HarmonyPatch(typeof(Feast), "GetStackPercentige")] public static class FeastShareLeftPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FeastServings }); } [HarmonyPrefix] public static void Prefix(Feast __instance, out int __state) { __state = FeastServings.Swap(__instance); } [HarmonyPostfix] public static void Postfix(ref float __result) { __result = Mathf.Min(__result, 1f); } [HarmonyFinalizer] public static void Finalizer(Feast __instance, int __state) { FeastServings.Restore(__instance, __state); } } [HarmonyPatch(typeof(Feast), "GetHoverText")] public static class FeastHoverPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FeastServings }); } [HarmonyPrefix] public static void Prefix(Feast __instance, out int __state) { __state = FeastServings.Swap(__instance); } [HarmonyFinalizer] public static void Finalizer(Feast __instance, int __state) { FeastServings.Restore(__instance, __state); } } [HarmonyPatch(typeof(Fermenter), "GetStatus")] public static class FermentationTimePatch { public static bool Prepare() { return Customized.Any(Settings.FermentationTime); } [HarmonyPrefix] public static void Prefix(Fermenter __instance, out ScaledFields __state) { __state = default(ScaledFields); float value = Settings.FermentationTime.Value; if (value > 0f) { __state = CostRules.Swap(ref __instance.m_fermentationDuration, value); } } [HarmonyFinalizer] public static void Finalizer(Fermenter __instance, ScaledFields __state) { CostRules.Restore(ref __instance.m_fermentationDuration, __state); } } public static class FermenterYield { public static int[] Swap(Fermenter fermenter) { int value = Settings.BatchYield.Value; if (value <= 0) { return null; } List<ItemConversion> conversion = fermenter.m_conversion; int[] array = new int[conversion.Count]; for (int i = 0; i < conversion.Count; i++) { array[i] = conversion[i].m_producedItems; conversion[i].m_producedItems = value; } return array; } public static void Restore(Fermenter fermenter, int[] saved) { if (saved != null) { List<ItemConversion> conversion = fermenter.m_conversion; for (int i = 0; i < saved.Length && i < conversion.Count; i++) { conversion[i].m_producedItems = saved[i]; } } } } [HarmonyPatch(typeof(Fermenter), "DelayedTap")] public static class TapYieldPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.BatchYield }); } [HarmonyPrefix] public static void Prefix(Fermenter __instance, out int[] __state) { __state = FermenterYield.Swap(__instance); } [HarmonyFinalizer] public static void Finalizer(Fermenter __instance, int[] __state) { FermenterYield.Restore(__instance, __state); } } [HarmonyPatch(typeof(Player), "GetTotalFoodValue")] public static class PlayerFoodDegradationPatch { private const float GameCurve = 0.3f; public static bool Prepare() { return Customized.Any((ConfigEntryBase)FeastMasterData.DisableFoodDegradation, (ConfigEntryBase)FeastMasterData.DegradationCurve); } [HarmonyPrefix] public static void Prefix(Player __instance) { bool value = FeastMasterData.DisableFoodDegradation.Value; float num = Mathf.Max(0f, FeastMasterData.DegradationCurve.Value); if (!value && Mathf.Approximately(num, 0.3f)) { return; } foreach (Food food in __instance.m_foods) { Recompute(food, value ? 1f : Strength(food, num)); } } public static float Strength(Food food, float curve) { if (curve <= 0f) { return 1f; } float foodBurnTime = food.m_item.m_shared.m_foodBurnTime; return Mathf.Pow((foodBurnTime > 0f) ? Mathf.Clamp01(food.m_time / foodBurnTime) : 0f, curve); } private static void Recompute(Food food, float strength) { SharedData shared = food.m_item.m_shared; food.m_health = shared.m_food * strength; food.m_stamina = shared.m_foodStamina * strength; food.m_eitr = shared.m_foodEitr * strength; } } internal static class FoodLookup { private sealed class Answer { public int Generation; public Dictionary<string, ConfigEntry<float>> Configs; } private static readonly ConditionalWeakTable<SharedData, Answer> answers = new ConditionalWeakTable<SharedData, Answer>(); private static int generation; public static void Forget() { generation++; } public static bool TryGet(ItemData item, out Dictionary<string, ConfigEntry<float>> configs) { SharedData shared = item.m_shared; if (shared == null) { return FeastMasterData.FindFood(item, out configs); } if (answers.TryGetValue(shared, out var value) && value.Generation == generation) { configs = value.Configs; return configs != null; } bool result = FeastMasterData.FindFood(item, out configs); if ((Object)(object)item.m_dropPrefab == (Object)null) { return result; } if (value == null) { answers.Add(shared, value = new Answer()); } value.Generation = generation; value.Configs = configs; return result; } } [HarmonyPatch(typeof(Hud), "UpdateFood")] public static class FoodSlotFrames { private static Transform[][] frames = new Transform[0][]; public static bool Prepare() { if (!Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FoodSlots })) { return FoodSlotsHud.CurrentHudExtended; } return true; } public static void Capture(Hud hud) { int num = hud.m_foodBars.Length; frames = new Transform[num][]; for (int i = 0; i < num; i++) { frames[i] = FramesOf(hud, i, (i == 0) ? 1 : (i - 1)); } } private static Transform[] FramesOf(Hud hud, int slot, int other) { Component[] array = (Component[])(object)new Component[3] { (Component)hud.m_foodBars[slot], (Component)hud.m_foodIcons[slot], (Component)hud.m_foodTime[slot] }; Component[] array2 = (Component[])(object)new Component[3] { (Component)hud.m_foodBars[other], (Component)hud.m_foodIcons[other], (Component)hud.m_foodTime[other] }; List<Transform> list = new List<Transform>(); for (int i = 0; i < array.Length; i++) { Transform val = FoodSlotsHud.SlotRoot(array[i].transform, array2[i].transform); if (!list.Contains(val) && !IsPart(val, array)) { list.Add(val); } } return list.ToArray(); } private static bool IsPart(Transform root, Component[] parts) { for (int i = 0; i < parts.Length; i++) { if ((Object)(object)parts[i].transform == (Object)(object)root) { return true; } } return false; } [HarmonyPostfix] public static void Postfix(Player player) { int num = Mathf.Max(Settings.FoodSlots.Value, player.GetFoods().Count); for (int i = 0; i < frames.Length; i++) { Transform[] array = frames[i]; foreach (Transform val in array) { if ((Object)(object)val != (Object)null && ((Component)val).gameObject.activeSelf != i < num) { ((Component)val).gameObject.SetActive(i < num); } } } } } public static class FoodSlots { public const int GameSlots = 3; public const int MaxSlots = 5; public static bool HasSame(Player player, ItemData item) { foreach (Food food in player.m_foods) { if (food.m_item.m_shared.m_name == item.m_shared.m_name) { return true; } } return false; } public static bool AnyCanEatAgain(Player player) { foreach (Food food in player.m_foods) { if (food.CanEatAgain()) { return true; } } return false; } public static bool Eat(Player player, ItemData item, bool add) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) Food val = (Food)((!add) ? ((object)player.GetMostDepletedFood()) : ((object)new Food())); if (val == null) { return false; } ((Character)player).Message((MessageType)2, EatenText(item.m_shared), 0, (Sprite)null, false); val.m_name = (((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : val.m_name); val.m_item = item; val.m_time = item.m_shared.m_foodBurnTime; val.m_health = item.m_shared.m_food; val.m_stamina = item.m_shared.m_foodStamina; val.m_eitr = item.m_shared.m_foodEitr; if (add) { player.m_foods.Add(val); } Game.instance.IncrementPlayerStat((PlayerStatType)49, 1f, false); Game.instance.GetPlayerProfile().IncrementStatFoodEaten(item.m_shared.m_name, 1f, item.m_cheated); player.UpdateFood(0f, true); return true; } private static string EatenText(SharedData shared) { string text = ""; if (shared.m_food > 0f) { text = text + " +" + shared.m_food + " $item_food_health "; } if (shared.m_foodStamina > 0f) { text = text + " +" + shared.m_foodStamina + " $item_food_stamina "; } if (shared.m_foodEitr > 0f) { text = text + " +" + shared.m_foodEitr + " $item_food_eitr "; } return text; } } [HarmonyPatch(typeof(Player), "CanEat")] public static class FoodSlotsCanEatPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FoodSlots }); } [HarmonyPrefix] public static bool Prefix(Player __instance, ItemData item, bool showMessages, ref bool __result) { int value = Settings.FoodSlots.Value; int count = __instance.m_foods.Count; if (value == 3 || FoodSlots.HasSame(__instance, item)) { return true; } if (value > 3) { if (count < 3 || count >= value) { return true; } __result = true; return false; } if (count < value) { return true; } __result = FoodSlots.AnyCanEatAgain(__instance); if (!__result && showMessages) { ((Character)__instance).Message((MessageType)2, "$msg_isfull", 0, (Sprite)null, false); } return false; } } [HarmonyPatch(typeof(Player), "EatFood")] public static class FoodSlotsEatPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FoodSlots }); } [HarmonyPrefix] [HarmonyPriority(200)] public static bool Prefix(Player __instance, ItemData item, ref bool __result) { int value = Settings.FoodSlots.Value; if (value == 3 || FoodSlots.HasSame(__instance, item) || !__instance.CanEat(item, false)) { return true; } int count = __instance.m_foods.Count; bool flag = count < value; if (flag == count < 3) { return true; } __result = FoodSlots.Eat(__instance, item, flag); return false; } } [HarmonyPatch(typeof(Hud), "Awake")] public static class FoodSlotsHud { private static Hud extended; public static bool CurrentHudExtended { get { if ((Object)(object)Hud.instance != (Object)null) { return (Object)(object)extended == (Object)(object)Hud.instance; } return false; } } public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.FoodSlots }); } public static void Installed() { if ((Object)(object)Hud.instance != (Object)null) { Extend(Hud.instance); } } [HarmonyPostfix] public static void Postfix(Hud __instance) { Extend(__instance); } private static void Extend(Hud hud) { Image[] foodBars = hud.m_foodBars; int num = ((foodBars != null) ? foodBars.Length : 0); if (num < 2 || num >= 5) { return; } Image[] foodIcons = hud.m_foodIcons; if (foodIcons == null || foodIcons.Length != num) { return; } TMP_Text[] foodTime = hud.m_foodTime; if (foodTime != null && foodTime.Length == num) { List<Image> list = new List<Image>(hud.m_foodBars); List<Image> list2 = new List<Image>(hud.m_foodIcons); List<TMP_Text> list3 = new List<TMP_Text>(hud.m_foodTime); while (list.Count < 5) { AddSlot(list, list2, list3); } hud.m_foodBars = list.ToArray(); hud.m_foodIcons = list2.ToArray(); hud.m_foodTime = list3.ToArray(); FoodSlotFrames.Capture(hud); extended = hud; PatchSwitch.MarkDirty(); } } private static void AddSlot(List<Image> bars, List<Image> icons, List<TMP_Text> times) { Dictionary<Transform, Transform> copies = new Dictionary<Transform, Transform>(); bars.Add(CopyNext<Image>(bars, copies)); icons.Add(CopyNext<Image>(icons, copies)); times.Add(CopyNext<TMP_Text>(times, copies)); } private static T CopyNext<T>(List<T> list, Dictionary<Transform, Transform> copies) where T : Component { Transform transform = ((Component)list[list.Count - 1]).transform; Transform transform2 = ((Component)list[list.Count - 2]).transform; Transform val = SlotRoot(transform, transform2); if (!copies.TryGetValue(val, out var value)) { value = (copies[val] = CopyShifted(val, SlotRoot(transform2, transform))); } return ((Component)Counterpart(value, val, transform)).GetComponent<T>(); } public static Transform SlotRoot(Transform element, Transform other) { Transform val = element; while ((Object)(object)val.parent != (Object)null && !other.IsChildOf(val.parent)) { val = val.parent; } return val; } private static Transform CopyShifted(Transform root, Transform previousRoot) { //IL_003c: 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_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_0052: Unknown result type (might be due to invalid IL or missing references) Transform transform = Object.Instantiate<GameObject>(((Component)root).gameObject, root.parent).transform; ((Object)transform).name = ((Object)root).name + "_feastmaster"; transform.SetSiblingIndex(root.GetSiblingIndex() + 1); transform.localPosition = root.localPosition + (root.localPosition - previousRoot.localPosition); return transform; } private static Transform Counterpart(Transform copy, Transform root, Transform target) { Stack<int> stack = new Stack<int>(); Transform val = target; while ((Object)(object)val != (Object)(object)root) { stack.Push(val.GetSiblingIndex()); val = val.parent; } Transform val2 = copy; while (stack.Count > 0) { val2 = val2.GetChild(stack.Pop()); } return val2; } } [HarmonyPatch(typeof(Food), "CanEatAgain")] public static class EatAgainPatch { private const float GameFraction = 0.5f; public static bool Prepare() { return Customized.Any(FeastMasterData.EatAgainAt); } [HarmonyPostfix] public static void Postfix(Food __instance, ref bool __result) { float num = Mathf.Clamp01(FeastMasterData.EatAgainAt.Value); if (!Mathf.Approximately(num, 0.5f)) { __result = __instance.m_time < __instance.m_item.m_shared.m_foodBurnTime * num; } } } [HarmonyPatch(typeof(Skills), "RaiseSkill")] public static class SkillGainPatch { public static bool Prepare() { return Customized.Any((ConfigEntryBase)Settings.RunSkillGain, (ConfigEntryBase)Settings.JumpSkillGain, (ConfigEntryBase)Settings.SneakSkillGain, (ConfigEntryBase)Settings.SwimSkillGain, (ConfigEntryBase)Settings.FishingSkillGain); } [HarmonyPrefix] public static void Prefix(SkillType skillType, ref float factor) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) factor *= Mathf.Max(0f, Multiplier(skillType)); } private static float Multiplier(SkillType skill) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected I4, but got Unknown return (skill - 100) switch { 2 => Settings.RunSkillGain.Value, 0 => Settings.JumpSkillGain.Value, 1 => Settings.SneakSkillGain.Value, 3 => Settings.SwimSkillGain.Value, 4 => Settings.FishingSkillGain.Value, _ => 1f, }; } } [HarmonyPatch(typeof(Character), "Damage")] public static class DrowningDamagePatch { public static bool Prepare() { return Customized.Any(Settings.DrowningDamage); } [HarmonyPrefix] public static void Prefix(Character __instance, HitData hit) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Invalid comparison between Unknown and I4 if (hit != null && (int)hit.m_hitType == 4 && __instance is Player) { hit.m_damage.m_damage *= Mathf.Max(0f, Settings.DrowningDamage.Value); } } } [HarmonyPatch(typeof(Player), "UpdateFood")] public static class ContinuousFoodHealingPatch { private const float TickSeconds = 10f; public static bool Prepare() { return Customized.Any((ConfigEntryBase[])(object)new ConfigEntryBase[1] { (ConfigEntryBase)Settings.ContinuousFoodHealing }); } [HarmonyPrefix] public static void Prefix(Player __instance, float dt, bool forceUpdate) { if (!forceUpdate && !(dt <= 0f) && Settings.ContinuousFoodHealing.Value) { __instance.m_foodRegenTimer = 0f; float num = RegenPerTick(__instance); if (num > 0f) { ((Character)__instance).Heal(num * dt / 10f, false); } } } private static float RegenPerTick(Player player) { float num = 0f; foreach (Food food in player.m_foods) { num += food.m_item.m_shared.m_foodRegen; } if (num <= 0f) { return 0f; } float num2 = 1f; ((Character)player).m_seman.ModifyHealthRegen(ref num2); return num * num2; } } public static class ItemValues { private static readonly HashSet<string> foods = new HashSet<string>(); private static readonly Dictionary<string, SE_Stats> meads = new Dictionary<string, SE_Stats>(); private static readonly HashSet<Dictionary<string, ConfigEntry<float>>> writtenFoods = new HashSet<Dictionary<string, ConfigEntry<float>>>(); private static readonly HashSet<MeadEffectConfig> writtenMeads = new HashSet<MeadEffectConfig>(); private static readonly HashSet<string> changedFoods = new HashSet<string>(); private static readonly HashSet<string> changedMeads = new HashSet<string>(); private static bool changedAll; private static bool suspended; private static Harmony harmony; private static bool spawnsHooked; public static void UseHarmony(Harmony instance) { harmony = instance; } public static void HookSpawns() { if (!spawnsHooked && harmony != null) { spawnsHooked = true; Copies.HookSpawns(harmony, ApplyCopy); } } public static void SuspendWhileBinding(bool suspend) { suspended = suspend; } public static void HookConfig(ConfigFile config) { config.SettingChanged += OnSettingChanged; config.ConfigReloaded += delegate { changedAll = true; }; } private static void OnSettingChanged(object sender, SettingChangedEventArgs args) { if (!suspended) { string section = args.ChangedSetting.Definition.Section; if (foods.Contains(section)) { changedFoods.Add(section); } else if (meads.ContainsKey(section)) { changedMeads.Add(section); } else { changedAll = true; } } } public static void Tick() { if (changedAll) { ApplyAll(); } else if (changedFoods.Count > 0 || changedMeads.Count > 0) { ApplyChanged(); } } public static void RegisterFood(string prefabName) { foods.Add(prefabName); } public static void RegisterMead(string prefabName, SE_Stats effect) { meads[prefabName] = effect; } public static void ApplyAll() { ForgetChanges(); Copies.ApplyAll(ApplyNamed); foreach (KeyValuePair<string, SE_Stats> mead in meads) { ApplyMead(mead.Key, mead.Value); } Rested.ApplyAll(); RefreshEatenFood(); } private static void ApplyChanged() { try { if (changedFoods.Count > 0) { Copies.Apply(changedFoods, ApplyNamed); RefreshEatenFood(); } foreach (string changedMead in changedMeads) { ApplyMead(changedMead, meads[changedMead]); } } finally { ForgetChanges(); } } private static void ForgetChanges() { changedAll = false; changedFoods.Clear(); changedMeads.Clear(); } private static void ApplyNamed(string prefabName, SharedData shared) { if (FeastMasterData.FoodConfigs.TryGetValue(prefabName, out var value)) { Apply(shared, value); } } public static void ApplyCopy(ItemData item) { if (ChangedRules.FoodValues && FeastMasterData.TryGetFood(item, out var configs)) { Apply(item.m_shared, configs); } } public static void Apply(SharedData shared, Dictionary<string, ConfigEntry<float>> configs) { if (ShouldWrite(writtenFoods, configs, Customized.FoodValues(configs))) { shared.m_food = configs["Health"].Value * FeastMasterData.HealthModifier.Value; shared.m_foodStamina = configs["Stamina"].Value * FeastMasterData.StaminaModifier.Value; shared.m_foodBurnTime = configs["Duration"].Value * FeastMasterData.DurationModifier.Value; shared.m_foodRegen = configs["HealthRegen"].Value * FeastMasterData.HealthRegenModifier.Value; shared.m_foodEitr = configs["Eitr"].Value * FeastMasterData.EitrModifier.Value; } } private static void RefreshEatenFood() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && localPlayer.GetFoods().Count > 0) { localPlayer.UpdateFood(0f, true); } } private static void ApplyMead(string prefabName, SE_Stats effect) { if (!((Object)(object)effect == (Object)null) && FeastMasterData.MeadConfigs.TryGetValue(prefabName, out var value)) { Apply(effect, value); RefreshActiveMead(((StatusEffect)effect).NameHash(), value); } } private static void RefreshActiveMead(int nameHash, MeadEffectConfig config) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } foreach (StatusEffect statusEffect in ((Character)localPlayer).GetSEMan().GetStatusEffects()) { SE_Stats val = (SE_Stats)(object)((statusEffect is SE_Stats) ? statusEffect : null); if (val != null && ((StatusEffect)val).NameHash() == nameHash) { ApplyKeepingProgress(val, config); } } } private static void ApplyKeepingProgress(SE_Stats stats, MeadEffectConfig config) { float num = ((((StatusEffect)stats).m_ttl > 0f) ? (((StatusEffect)stats).m_time / ((StatusEffect)stats).m_ttl) : 0f); Apply(stats, config); ((StatusEffect)stats).m_time = num * ((StatusEffect)stats).m_ttl; } public static void Apply(SE_Stats effect, MeadEffectConfig config) { if (ShouldWrite(writtenMeads, config, Customized.Mead(config))) { ((StatusEffect)effect).m_ttl = config.Duration.Value; effect.m_healthOverTime = config.HealthOverTime.Value; effect.m_staminaOverTime = config.StaminaOverTime.Value; effect.m_eitrOverTime = config.EitrOverTime.Value; effect.m_healthRegenMultiplier = config.HealthRegenMultiplier.Value; effect.m_staminaRegenMultiplier = config.StaminaRegenMultiplier.Value; effect.m_eitrRegenMultiplier = config.EitrRegenMultiplier.Value; effect.m_runStaminaDrainModifier = config.RunStaminaModifier.Value; effect.m_jumpStaminaUseModifier = config.JumpStaminaModifier.Value; } } private static bool ShouldWrite<T>(HashSet<T> written, T item, bool customized) { if (customized) { written.Add(item); } if (!customized) { return written.Contains(item); } return true; } public static float VigorOf(ItemData item) { if (!FeastMasterData.TryGetFood(item, out var configs)) { return 0f; } float num = configs["Stamina"].Value * FeastMasterData.StaminaModifier.Value * Settings.VigorPerStaminaPoint.Value; return (configs["Vigor"].Value + num) * Settings.VigorMultiplier.Value; } public static float EitrVigorOf(ItemData item) { if (!FeastMasterData.TryGetFood(item, out var configs)) { return 0f; } float num = configs["Eitr"].Value * FeastMasterData.EitrModifier.Value * Settings.EitrVigorPerEitrPoint.Value; return (configs["EitrVigor"].Value + num) * Settings.EitrVigorMultiplier.Value; } } public class MeadEffectConfig { public ConfigEntry<float> Duration { get; set; } public ConfigEntry<float> HealthOverTime { get; set; } public ConfigEntry<float> StaminaOverTime { get; set; } public ConfigEntry<float> EitrOverTime { get; set; } public ConfigEntry<float> HealthRegenMultiplier { get; set; } public ConfigEntry<float> StaminaRegenMultiplier { get; set; } public ConfigEntry<float> EitrRegenMultiplier { get; set; } public ConfigEntry<float> RunStaminaModifier { get; set; } public ConfigEntry<float> JumpStaminaModifier { get; set; } } [HarmonyPatch(typeof(ObjectDB), "Awake")] public static class ObjectDBAwakePatch { [HarmonyPostfix] public static void Postfix() { FeastMasterData.LoadConfigurations(); } } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] public static class ObjectDBCopyOtherDBPatch { [HarmonyPostfix] public static void Postfix() { FeastMasterData.LoadConfigurations(); } } [HarmonyPatch(typeof(Player), "EatFood")] public static class PlayerEatFoodPatch { public static bool Prepare() { return Customized.AnyFoodValues(); } [HarmonyPrefix] public static void Prefix(ItemData item) { if (FeastMasterData.TryGetFood(item, out var configs)) { ItemValues.Apply(item.m_shared, configs); } } } [HarmonyPatch] public static class SEManAddStatusEffectPatch { public static bool Prepare() { return Customized.AnyMead(); } public static MethodBase TargetMethod() { return AccessTools.GetDeclaredMethods(typeof(SEMan)).First((MethodInfo m) => m.Name == "AddStatusEffect" && m.GetParameters().Length != 0 && m.GetParameters()[0].ParameterType == typeof(StatusEffect)); } [HarmonyPrefix] public static void Prefix(StatusEffect statusEffect) { SE_Stats val = (SE_Stats)(object)((statusEffect is SE_Stats) ? statusEffect : null); if (val != null && FeastMasterData.MeadsByEffectHash.TryGetValue(((StatusEffect)val).NameHash(), out var value)) { ItemValues.Apply(val, value); } } } public static class PatchSwitch { private const string RuleMethod = "Prepare"; private const string InstalledHook = "Installed"; private const string RemovedHook = "Removed"; private const BindingFlags OwnStatic = BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly List<Type> classes = new List<Type>(); private static readonly HashSet<Type> installed = new HashSet<Type>(); private static readonly HashSet<Type> failed = new HashSet<Type>(); private static Harmony harmony; private static bool dirty; public static int Initialize(Harmony instance, Assembly assembly) { harmony = instance; Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly); foreach (Type type in typesFromAssembly) { if (type.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Length != 0) { classes.Add(type); } } Refresh(log: true); SwitchTicker.Ensure(); return failed.Count; } public static void MarkDirty() { dirty = true; } public static void Tick() { if (dirty && harmony != null) { dirty = false; Refresh(log: false); } } private static void Refresh(bool log) { ChangedRules.Refresh(); foreach (Type @class in classes) { if (failed.Contains(@class)) { continue; } bool flag = Wanted(@class); if (flag != installed.Contains(@class)) { log = true; if (flag) { Install(@class); } else { Remove(@class); } } } if (log) { LogActive(); } } private static bool Wanted(Type type) { MethodInfo methodInfo = OwnMethod(type, "Prepare"); if (methodInfo == null) { return true; } try { return (bool)methodInfo.Invoke(null, null); } catch (Exception ex) { FeastMaster.Log.LogError((object)(type.Name + ".Prepare failed, the patch stays off: " + (ex.InnerException ?? ex).Message)); return false; } } private static void Install(Type type) { try { harmony.CreateClassProcessor(type).Patch(); installed.Add(type); } catch (Exception ex) { failed.Add(type); FeastMaster.Log.LogError((object)("Patch " + type.FullName + " failed to apply: " + ex.Message)); return; } RunHook(type, "Installed"); } private static void Remove(Type type) { foreach (MethodBase item in harmony.GetPatchedMethods().ToList()) { foreach (Patch item2 in PatchesOf(item)) { if (item2.owner == harmony.Id && item2.PatchMethod.DeclaringType == type) { harmony.Unpatch(item, item2.PatchMethod); } } } installed.Remove(type); RunHook(type, "Removed"); } private static List<Patch> PatchesOf(MethodBase original) { Patches patchInfo = Harmony.GetPatchInfo(original); if (patchInfo == null) { return new List<Patch>(); } return patchInfo.Prefixes.Concat(patchInfo.Postfixes).Concat(patchInfo.Transpilers).Concat(patchInfo.Finalizers) .Concat(patchInfo.ILManipulators) .ToList(); } private static MethodInfo OwnMethod(Type type, string name) { return type.GetMethod(name, BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); } private static void RunHook(Type type, string name) { MethodInfo methodInfo = OwnMethod(type, name); if (methodInfo == null) { return; } try { methodInfo.Invoke(null, null); } catch (Exception ex) { FeastMaster.Log.LogError((object)(type.Name + "." + name + " failed: " + (ex.InnerException ?? ex).Message)); } } private static void LogActive() { List<string> list = (from t in classes where installed.Contains(t) && OwnMethod(t, "Prepare") != null select t.Name).ToList(); FeastMaster.Log.LogInfo((object)((list.Count == 0) ? "Every setting is at its default: no gameplay patches installed." : ("Patches installed for changed settings: " + string.Join(", ", list)))); } } internal sealed class SwitchTicker : MonoBehaviour { private static SwitchTicker instance; public static void Ensure() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!((Object)(object)instance != (Object)null)) { GameObject val = new GameObject("FeastMaster.PatchSwitch") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); instance = val.AddComponent<SwitchTicker>(); } } private void Update() { Guard.Run("item values", ItemValues.Tick); Guard.Run("patch switch", PatchSwitch.Tick); } } public static class RegenBasicsRule { public static bool Rules() { return Customized.Any((ConfigEntryBase)Settings.StaminaRegenMultiplier, (ConfigEntryBase)Settings.LowStaminaRegenBonus, (ConfigEntryBase)Settings.EitrRegenMultiplier); } public static ScaledGroup Scale(Player player) { return new ScaledGroup { First = CostRules.Scale(ref player.m_staminaRegen, Mathf.Max(0f, Settings.StaminaRegenMultiplier.Value)), Second = CostRules.Scale(ref player.m_staminaRegenTimeMultiplier, Mathf.Max(0f, Settings.LowStaminaRegenBonus.Value)), Third = CostRules.Scale(ref player.m_eiterRegen, Mathf.Max(0f, Settings.EitrRegenMultiplier.Value)) }; } public static void Restore(Player player, ScaledGroup saved) { CostRules.Restore(ref player.m_staminaRegen, saved.First); CostRules.Restore(ref player.m_staminaRegenTimeMultiplier, saved.Second); CostRules.Restore(ref player.m_eiterRegen, saved.Third); } } [HarmonyPatch(typeof(Player), "RPC_UseStamina")] public static class StaminaRegenDelayPatch { public static bool Prepare() { return Customized.Any(Settings.StaminaRegenDelay); } [HarmonyPrefix] public static void Prefix(Player __instance, out ScaledFields __state) { __state = CostRules.Swap(ref __instance.m_staminaRegenDelay, Mathf.Max(0f, Settings.StaminaRegenDelay.Value)); } [HarmonyFinalizer] public static void Finalizer(Player __instance, ScaledFields __state) { CostRules.Restore(ref __instance.m_staminaRegenDelay, __state); } } [HarmonyPatch(typeof(Player), "RPC_UseEitr")] public static class EitrRegenDelayPatch { public static bool Prepare() { return Customized.Any(Settings.EitrRegenDelay); } [HarmonyPrefix] public static void Prefix(Player __instance, out ScaledFields __state) { __state = CostRules.Swap(ref __instance.m_eitrRegenDelay, Mathf.Max(0f, Settings.EitrRegenDelay.Value)); } [HarmonyFinalizer] public static void Finalizer(Player __instance, ScaledFields __state) { CostRules.Restore(ref __instance.m_eitrRegenDelay, __state); } } public static class Rested { private static bool written; public static ConfigEntry<float> Duration { get; private set; } public static ConfigEntry<float> DurationPerComfort { get; private set; } public static ConfigEntry<float> StaminaRegen { get; private set; } public static ConfigEntry<float> HealthRegen { get; private set; } public static ConfigEntry<float> EitrRegen { get; private set; } public static bool Bind(ObjectDB db) { if (Duration == null) { StatusEffect statusEffect = db.GetStatusEffect(SEMan.s_statusEffectRested); SE_Rested val = (SE_Rested)(object)((statusEffect is SE_Rested) ? statusEffect : null); if (val != null) { SyncedConfiguration synced = FeastMaster.Synced; string section = "2. Stamina Regeneration"; Duration = synced.Bind(section, "Rested Duration", val.m_baseTTL, "Seconds the Rested effect lasts at comfort level 1. Takes effect the next time a player rests.", true, null); DurationPerComfort = synced.Bind(section, "Rested Duration Per Comfort", val.m_TTLPerComfortLevel, "Seconds added to Rested Duration for every comfort level above 1. Takes effect the next time a player rests.", true, null); StaminaRegen = synced.Bind(section, "Rested Stamina Regen", ((SE_Stats)val).m_staminaRegenMultiplier, "Stamina regeneration multiplier while rested (1 = no bonus). Combines with the other regen rules like a mead's.", true, null); HealthRegen = synced.Bind(section, "Rested Health Regen", ((SE_Stats)val).m_healthRegenMultiplier, "Health regeneration multiplier while rested (1 = no bonus).", true, null); EitrRegen = synced.Bind(section, "Rested Eitr Regen", ((SE_Stats)val).m_eitrRegenMultiplier, "Eitr regeneration multiplier while rested (1 = no bonus).", true, null); return true; } } return false; } public static void ApplyAll() { if (Duration == null || (Object)(object)ObjectDB.instance == (Object)null) { return; } written |= Customized.Any((ConfigEntryBase)Duration, (ConfigEntryBase)DurationPerComfort, (ConfigEntryBase)StaminaRegen, (ConfigEntryBase)HealthRegen, (ConfigEntryBase)EitrRegen); if (written) { StatusEffect statusEffect = ObjectDB.instance.GetStatusEffect(SEMan.s_statusEffectRested); Apply((SE_Rested)(object)((statusEffect is SE_Rested) ? statusEffect : null)); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { StatusEffect statusEffect2 = ((Character)localPlayer).GetSEMan().GetStatusEffect(SEMan.s_statusEffectRested); Apply((SE_Rested)(object)((statusEffect2 is SE_Rested) ? statusEffect2 : null)); } } } private static void Apply(SE_Rested rested) { if (!((Object)(object)rested == (Object)null)) { rested.m_baseTTL = Duration.Value; rested.m_TTLPerComfortLevel = DurationPerComfort.Value; ((SE_Stats)rested).m_staminaRegenMultiplier = StaminaRegen.Value; ((SE_Stats)rested).m_healthRegenMultiplier = HealthRegen.Value; ((SE_Stats)rested).m_eitrRegenMultiplier = EitrRegen.Value; } } } [HarmonyPatch(typeof(ZNetScene), "Awake")] public static class ScenePrefabs { [HarmonyPostfix] [HarmonyPriority(0)] public static void Postfix(ZNetScene __instance) { ConfigFile config = FeastMaster.Synced.Config; if (BindAll(config, __instance.m_prefabs)) { if (config.SaveOnConfigSet) { config.Save(); } ItemValues.ApplyAll(); PatchSwitch.MarkDirty(); } } private static bool BindAll(ConfigFile file, List<GameObject> prefabs) { bool saveOnConfigSet = file.SaveOnConfigSet; bool flag = false; file.SaveOnConfigSet = false; ItemValues.SuspendWhileBinding(suspend: true); try { foreach (GameObject prefab in prefabs) { flag |= Bind(prefab); } return flag; } finally { file.SaveOnConfigSet = saveOnConfigSet; ItemValues.SuspendWhileBinding(suspend: false); } } private static bool Bind(GameObject prefab) { if ((Object)(object)prefab == (Object)null) { return false; } CookingStation component = prefab.GetComponent<CookingStation>(); bool flag = (Object)(object)component != (Object)null && CookTimes.Bind(((Object)prefab).name, component); Feast component2 = prefab.GetComponent<Feast>(); ItemDrop val = (((Object)(object)component2 == (Object)null) ? null : (((Object)(object)component2.m_foodItem != (Object)null) ? component2.m_foodItem : prefab.GetComponent<ItemDrop>())); return ((Object)(object)val != (Object)null && FeastMasterData.BindFeastFood(val)) || flag; } } public static class Settings { public const string HealthRegenSection = "1. Health Regeneration"; public const string StaminaRegenSection = "2. Stamina Regeneration"; public const string EitrRegenSection = "3. Eitr Regeneration"; public const string StaminaCostsSection = "4. Stamina Costs"; public const string BaseValuesSection = "5. Base Values"; public const string SkillsSection = "6. Skills"; public const string WorldRatesSection = "7. World Rates"; public const string DisplaySection = "8. Display"; public const string KitchenSection = "9. Kitchen"; public const float GameBlockingRegenFactor = 0.8f; private const string OldFermenterSection = "9. Fermenter"; public static ConfigEntry<bool> ContinuousFoodHealing { get; private set; } public static ConfigEntry<float> VigorPerStaminaPoint { get; private set; } public static ConfigEntry<float> VigorMultiplier { get; private set; } public static ConfigEntry<float> RegenCurveStrength { get; private set; } public static ConfigEntry<float> RegenCurvePivot { get; private set; } public static ConfigEntry<float> RegenPerExtraStaminaPoint { get; private set; } public static ConfigEntry<bool> CountFoodStaminaOnly { get; private set; } public static ConfigEntry<float> SneakSkillRegenBonus { get; private set; } public static ConfigEntry<float> EncumberedRegenFraction { get; private set; } public static ConfigEntry<float> SwimmingRegenFraction { get; private set; } public static ConfigEntry<float> SwimmingRegenDelay { get; private set; } public static ConfigEntry<float> StaminaRegenMultiplier { get; private set; } public static ConfigEntry<float> LowStaminaRegenBonus { get; private set; } public static ConfigEntry<float> StaminaRegenDelay { get; private set; } public static ConfigEntry<float> BlockingRegenFactor { get; private set; } public static ConfigEntry<float> EitrVigorPerEitrPoint { get; private set; } public static ConfigEntry<float> EitrVigorMultiplier { get; private set; } public static ConfigEntry<float> EitrRegenMultiplier { get; private set; } public static ConfigEntry<float> EitrRegenDelay { get; private set; } public static ConfigEntry<float> EitrRegenCurveStrength { get; private set; } public static ConfigEntry<float> EitrRegenCurvePivot { get; private set; } public static ConfigEntry<float> BlockingEitrRegenFactor { get; private set; } public static ConfigEntry<bool> AllowAutoEat { get; private set; } public static ConfigEntry<bool> AutoEat { get; private set; } public static ConfigEntry<int> FoodSlots { get; private set; } public static ConfigEntry<float> FermentationTime { get; private set; } public static ConfigEntry<int> BatchYield { get; private set; } public static ConfigEntry<float> CookTimeMultiplier { get; private set; } public static ConfigEntry<bool> FoodCanBurn { get; private set; } public static ConfigEntry<int> FeastServings { get; private set; } public static ConfigEntry<float> RunCost { get; private set; } public static ConfigEntry<float> JumpCost { get; private set; } public static ConfigEntry<float> DodgeCost { get; private set; } public static ConfigEntry<float> BlockCost { get; private set; } public static ConfigEntry<float> AttackCost { get; private set; } public static ConfigEntry<float> SneakCost { get; private set; } public static ConfigEntry<float> SwimCost { get; private set; } public static ConfigEntry<float> EncumberedCost { get; private set; } public static ConfigEntry<float> ToolCost { get; private set; } public static ConfigEntry<float> FishingPullCost { get; private set; } public static ConfigEntry<float> FishingHookedCost { get; private set; } public static ConfigEntry<float> HarpoonCost { get; private set; } public static ConfigEntry<float> OutOfCombatRunCost { get; private set; } public static ConfigEntry<float> OutOfCombatJumpCost { get; private set; } public static ConfigEntry<float> OutOfCombatDodgeCost { get; private set; } public static ConfigEntry<float> OutOfCombatSneakCost { get; private set; } public static ConfigEntry<bool> FreeSneakingWithoutEnemies { get; private set; } public static ConfigEntry<float> SkillDiscount { get; private set; } public static ConfigEntry<float> DrowningDamage { get; private set; } public static ConfigEntry<float> BaseHealth { get; private set; } public static ConfigEntry<float> BaseStamina { get; private set; } public static ConfigEntry<float> RunSkillStamina { get; private set; } public static ConfigEntry<float> JumpSkillStamina { get; private set; } public static ConfigEntry<float> SneakSkillStamina { get; private set; } public static ConfigEntry<float> SwimSkillStamina { get; private set; } public static ConfigEntry<float> FishingSkillStamina { get; private set; } public static ConfigEntry<float> RunSkillGain { get; private set; } public static ConfigEntry<float> JumpSkillGain { get; private set; } public static ConfigEntry<float> SneakSkillGain { get; private set; } public static ConfigEntry<float> SwimSkillGain { get; private set; } public static ConfigEntry<float> FishingSkillGain { get; private set; } public static ConfigEntry<float> FoodRate { get; private set; } public static ConfigEntry<float> StaminaRate { get; private set; } public static ConfigEntry<float> MoveStaminaRate { get; private set; } public static ConfigEntry<float> StaminaRegenRate { get; private set; } public static ConfigEntry<bool> HideHealthNumber { get; private set; } public static ConfigEntry<bool> HideStaminaNumber { get; private set; } public static ConfigEntry<bool> HideEitrNumber { get; private set; } public static ConfigEntry<FoodTimers> FoodTimersMode { get; private set; } public static void Initialize(SyncedConfiguration config) { BindHealthRegen(config); BindVigorAndCurve(config); BindRegenRules(config); BindRegenBasics(config); BindEitrRegen(config); BindStaminaCosts(config); BindOutOfCombat(config); BindBaseValues(config); BindSkills(config); BindWorldRates(config); BindDisplay(config); BindKitchen(config); BindEating(config); } private static void BindHealthRegen(SyncedConfiguration config) { string text = ConfigMigration.TakeRenamed(config.Config, "1. Health Regeneration", "Steady Regeneration", "Continuous Food Healing"); ContinuousFoodHealing = config.Bind("1. Health Regeneration", "Continuous Food Healing", false, "Off: food heals its Health Regen once every 10 seconds. On: the same amount is spread over the 10 seconds, applied a little every frame, without floating heal numbers.", true, null); ConfigMigration.Apply((ConfigEntryBase)(object)ContinuousFoodHealing, text); } private static void BindVigorAndCurve(SyncedConfiguration config) { VigorPerStaminaPoint = config.Bind("2. Stamina Regeneration", "Vigor Per Stamina Point", 0f, "Percent of stamina regeneration every food grants per point of its configured Stamina while it is active (its Vigor). 0 keeps vanilla; 0.1 turns a 60-stamina food into +6% regeneration, three such foods into +18%. Each food's own Vigor entry adds on top. Stacks with Regen Per Extra Stamina Point, which also counts food stamina; prefer one of the two.", true, null); VigorMultiplier = config.Bind("2. Stamina Regeneration", "Vigor Multiplier", 1f, "Scales the Vigor of every food. Stacks with Regen Per Extra Stamina Point.", true, null); RegenCurveStrength = config.Bind("2. Stamina Regeneration", "Regen Curve Strength", 1f, "1 = off. Above 1 stamina regenerates faster when the bar is low and slower when it is high: at an empty bar regeneration is multiplied by this value, at a full bar divided by it, 1x at the pivot. Below 1 does the opposite. Stacks with Low Stamina Regen Bonus and the game's own low-bar bonus, all boosting an empty bar.", true, null); RegenCurvePivot = config.Bind("2. Stamina Regeneration", "Regen Curve Pivot", 0.5f, "Bar fraction (0..1) where the regen curve is exactly 1x.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f)); } private static void BindRegenRules(SyncedConfiguration config) { BindExtraStaminaRegen(config); SneakSkillRegenBonus = config.Bind("2. Stamina Regeneration", "Sneak Skill Regen Bonus", 0f, "Percent of stamina regeneration added while crouching and standing still, at Sneak skill 100. Scales with the skill.", true, null); EncumberedRegenFraction = config.Bind("2. Stamina Regeneration", "Encumbered Regen Fraction", 0f, "0 = vanilla, no regeneration while encumbered. Above 0 the player regenerates at this fraction of the normal rate while encumbered (walking still drains).", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f)); SwimmingRegenFraction = config.Bind("2. Stamina Regeneration", "Swimming Regen Fraction", 0f, "0 = vanilla, no regeneration while swimming. Above 0 the player regenerates at this fraction of the normal rate while in the water, after the swimming regen delay.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f)); SwimmingRegenDelay = config.Bind("2. Stamina Regeneration", "Swimming Regen Delay", 3f, "Seconds after the last swimming stroke before swimming regeneration starts (treading water or floating still).", true, null); } private static void BindExtraStaminaRegen(SyncedConfiguration config) { string text = ConfigMigration.TakeRenamed(config.Config, "2. Stamina Regeneration", "Regen Per 10 Extra Stamina", "Regen Per Extra Stamina Point"); RegenPerExtraStaminaPoint = config.Bind("2. Stamina Regeneration", "Regen Per Extra Stamina Point", 0f, "Percent of stamina regeneration added per point of stamina above Base Stamina (and above the skill stamina of section 5). 0 keeps vanilla; 0.2 with 150 extra stamina gives +30%. Stacks with Vigor, which also counts food stamina; prefer one of the two. Base Stamina in section 5 moves the reference point.", true, null); ConfigMigration.ApplyDivided(RegenPerExtraStaminaPoint, text, 10f); string text2 = ConfigMigration.TakeRenamed(config.Config, "2. Stamina Regeneration", "Extra Stamina From Food Only", "Count Food Stamina Only"); CountFoodStaminaOnly = config.Bind("2. Stamina Regeneration", "Count Food Stamina Only", false, "Off: extra stamina is the whole maximum above the base, from food, meads, gear and other mods. On: only the current stamina of the active foods counts.", true, null); ConfigMigration.Apply((ConfigEntryBase)(object)CountFoodStaminaOnly, text2); } private static void BindRegenBasics(SyncedConfiguration config) { StaminaRegenMultiplier = config.Bind("2. Stamina Regeneration", "Stamina Regen Multiplier", 1f, "Scales the base stamina regeneration. 1 = untouched. The curve, Vigor and extra stamina rules apply on top. Multiplies with the world Stamina Regen Rate in section 7 and with Low Stamina Regen Bonus.", true, null); LowStaminaRegenBonus = config.Bind("2. Stamina Regeneration", "Low Stamina Regen Bonus", 1f, "Scales the game's own bonus for a low bar (regeneration rises as the bar empties). 1 = untouched, 0 removes it. Stacks with Regen Curve Strength and Stamina Regen Multiplier.", true, null); StaminaRegenDelay = config.Bind("2. Stamina Regeneration", "Stamina Regen Delay", 1f, "Seconds after any stamina use before regeneration resumes. The game uses 1; at 1 FeastMaster leaves it alone.", true, null); BlockingRegenFactor = config.Bind("2. Stamina Regeneration", "Blocking Regen Factor", 0.8f, "Stamina regeneration factor while holding block. The game uses 0.8; 1 means no slowdown.", true, null); } private static void BindEitrRegen(SyncedConfiguration config) { EitrVigorPerEitrPoint = config.Bind("3. Eitr Regeneration", "Eitr Vigor Per Eitr Point", 0f, "Percent of eitr regeneration every food grants per point of its configured Eitr while it is active (its Eitr Vigor). 0 keeps vanilla. Each food's own EitrVigor entry adds on top.", true, null); EitrVigorMultiplier = config.Bind("3. Eitr Regeneration", "Eitr Vigor Multiplier", 1f, "Scales the Eitr Vigor of every food.", true, null); EitrRegenMultiplier = config.Bind("3. Eitr Regeneration", "Eitr Regen Multiplier", 1f, "Scales the base eitr regeneration. 1 = untouched. Eitr Vigor and the eitr curve apply on top.", true, null); EitrRegenDelay = config.Bind("3. Eitr Regeneration", "Eitr Regen Delay", 1f, "Seconds after any eitr use before regeneration resumes. The game uses 1; at 1 FeastMaster leaves it alone.", true, null); EitrRegenCurveStrength = config.Bind("3. Eitr Regeneration", "Eitr Regen Curve Strength", 1f, "1 = off. Above 1 eitr regenerates faster when the bar is low and slower when it is high, like the stamina curve. Below 1 does the opposite. Stacks with the game's own low-bar eitr bonus.", true, null); EitrRegenCurvePivot = config.Bind("3. Eitr Regeneration", "Eitr Regen Curve Pivot", 0.5f, "Eitr bar fraction (0..1) where the eitr regen curve is exactly 1x.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f)); BlockingEitrRegenFactor = config.Bind("3. Eitr Regeneration", "Blocking Eitr Regen Factor", 0.8f, "Eitr regeneration factor while holding block. The game uses 0.8; 1 means no slowdown.", true, null); } private static void BindEating(SyncedConfiguration config) { FoodSlots = config.Bind("0. Global Settings", "Food Slots", 3, "How many foods a player can have active at once. The game uses 3. More slots add a lot of health and stamina; the global multipliers above can balance that. Lowering it keeps extra foods until they run out.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<int>(1, 5)); AllowAutoEat = config.Bind("0. Global Settings", "Allow Auto Eat", false, "When on, a player whose food runs out automatically eats another of the same food from their inventory, if they have one. Each player can still turn it off for themselves with Auto Eat in section 8.", true, null); AutoEat = config.Bind("8. Display", "Auto Eat", true, "Eat another of the same food when one runs out, while the server allows it (Allow Auto Eat in section 0). Per player, not synced from the server.", false, null); } private static void BindKitchen(SyncedConfiguration config) { BindFermenter(config); CookTimeMultiplier = config.Bind("9. Kitchen", "Cook Time Multiplier", 1f, "Multiplier on the cook time of every recipe on every cooking station and oven, on top of each recipe's own time in the station's section (one section per station, named after its prefab). 0.5 cooks twice as fast. Food on the fire when the value changes follows at once.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0.01f, 100f)); FoodCanBurn = config.Bind("9. Kitchen", "Food Can Burn", true, "On: cooked food left on a station burns as in the game, at twice its cook time. Off: cooked food never burns and waits on the station until it is taken.", true, null); FeastServings = config.Bind("9. Kitchen", "Feast Servings", 0, "Servings a placed feast holds. 0 keeps each feast's own count. Feasts nobody has eaten from follow a change at once; a feast already started keeps the servings it has left.", true, (AcceptableValueBase?)(object)new AcceptableValueRange<int>(0, 100)); } private static void BindFermenter(SyncedConfiguration config) { FermentationTime = BindMoved(config, "Fermentation Time", 0f, "Seconds a fermenter takes to brew a batch. 0 keeps each barrel's own time (the game's fermenter takes 2400, 40 minutes). Also applies to barrels already brewing: one that has brewed longer than the new time is ready at once.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 86400f)); BatchYield = BindMoved(config, "Batch Yield", 0, "Meads tapped from one batch. 0 keeps each recipe's own yield. Applies when a batch is tapped, so it also covers batches already brewing.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100)); } private static ConfigEntry<T> BindMoved<T>(SyncedConfiguration config, string key, T defaultValue, string description, AcceptableValueBase range) { string text = ConfigMigration.TakeOrphan(config.Config, "9. Fermenter", "9. Kitchen", key); ConfigEntry<T> obj = config.Bind("9. Kitchen", key, defaultValue, description, true, range); ConfigMigration.Apply((ConfigEntryBase)(object)obj, text); return obj; } private static void BindStaminaCosts(SyncedConfiguration config) { RunCost = BindCost(config, "Run Cost", "running", "the world Stamina Rate and Move Stamina Rate in section 7, Out Of Combat Run Cost and a mead's RunStaminaModifier"); JumpCost = BindCost(config, "Jump Cost", "jumping", "the world Stamina Rate and Move Stamina Rate in section 7, Out Of Combat Jump Cost, Skill Discount and a mead's JumpStaminaModifier"); DodgeCost = BindCost(config, "Dodge Cost", "dodging", "the world Stamina Rate in section 7, Out Of Combat Dodge Cost, Skill Discount and the game's own dodge skill discount"); BlockCost = BindCost(config, "Block Cost", "blocking (normal and perfect blocks)", "the world Stamina Rate in section 7 and Skill Discount"); AttackCost = BindCost(config, "Attack Cost", "attacking", "the world Stamina Rate in section 7"); SneakCost = BindCost(config, "Sneak Cost", "sneaking", "the world Stamina Rate in section 7 and Out Of Combat Sneak Cost; Free Sneaking Without Enemies overrides both"); SwimCost = BindCost(config, "Swim Cost", "swimming", "the world Stamina Rate and Move Stamina Rate in section 7"); EncumberedCost = BindCost(config, "Encumbered Cost", "walking while encumbered", "the world Stamina Rate in section 7"); ToolCost = BindCost(config, "Tool Cost", "placing and repairing with the hammer, hoe and cultivator", "the world Stamina Rate in section 7"); FishingPullCost = BindCost(config, "Fishing Pull Cost", "reeling in the fishing line", "the world Stamina Rate in section 7"); FishingHookedCost = BindCost(config, "Fishing Hooked Cost", "holding a hooked fish on the line", "the world Stamina Rate in section 7"); HarpoonCost = BindCost(config, "Harpoon Cost", "pulling a harpooned creature", "the world Stamina Rate in section 7"); DrowningDamage = config.Bind("4. Stamina Costs", "Drowning Damage", 1f, "Multiplier on the damage taken each second while swimming without stamina. 1 = untouched, 0 = harmless.", true, null); } private static ConfigEntry<float> BindCost(SyncedConfiguration config, string key, string action, string stacks) { return config.Bind("4. Stamina Costs", key, 1f, "Multiplier on the stamina drained by " + action + ". 1 = untouched, 0 = free, 2 = double. Multiplies with " + stacks + ".", true, null); } private static void BindOutOfCombat(SyncedConfiguration config) { OutOfCombatRunCost = config.Bind("4. Stamina Costs", "Out Of Combat Run Cost", 1f, string.Format("Extra multiplier on the {0} cost while out of combat: no enemy targets or has noticed the player and the player is not attacking. 1 = no change.", "run"), true, null); OutOfCombatJumpCost = config.Bind("4. Stamina Costs", "Out Of Combat Jump Cost", 1f, string.Format("Extra multiplier on the {0} cost while out of combat: no enemy targets or has noticed the player and the player is not attacking. 1 = no change.", "jump"), true, null); OutOfCombatDodgeCost = config.Bind("4. Stamina Costs", "Out Of Combat Dodge Cost", 1f, string.Format("Extra multiplier on the {0} cost while out of combat: no enemy targets or has noticed the player and the player is not attacking. 1 = no change.", "dodge"), true, null); OutOfCombatSneakCost = config.Bind("4. Stamina Costs", "Out Of Combat Sneak Cost", 1f, string.Format("Extra multiplier on the {0} cost while out of combat: no enemy targets or has noticed the player and the player is not attacking. 1 = no change.", "sneak"), true, null); FreeSneakingWithoutEnemies = config.Bind("4. Stamina Costs", "Free Sneaking Without Enemies", false, "Sneaking costs no stamina when no enemy is within stealth range and none has noticed the player.", true, null); SkillDiscount = config.Bind("4. Stamina Costs", "Skill Discount", 0f, "Percent taken off the block, dodge and jump costs at skill 100 of the matching skill (Blocking, Dodge, Jump). Scales with the skill and applies on top of the game's own discounts, so dodge at skill 100 gets both. Multiplies with the matching cost multipliers.", true, null); } private static void BindBaseValues(SyncedConfiguration config) { BaseHealth = config.Bind("5. Base Values", "Base Health", 25f, "Health a player has with no food. The game uses 25; at 25 FeastMaster leaves it alone, so another mod's value stands.", true, null); BaseStamina = config.Bind("5. Base Values", "Base Stamina", 75f, "Stamina a player has with no food. The game uses 75; at 75 FeastMaster leaves it alone, so another mod's value stands. Regen Per Extra Stamina Point counts stamina above this value, so raising it lowers that bonus.", true, null); RunSkillStamina = BindSkillStamina(config, "Run Skill Stamina", "Run"); JumpSkillStamina = BindSkillStamina(config, "Jump Skill Stamina", "Jump"); SneakSkillStamina = BindSkillStamina(config, "Sneak Skill Stamina", "Sneak"); SwimSkillStamina = BindSkillStamina(config, "Swim Skill Stamina", "Swim"); FishingSkillStamina = BindSkillStamina(config, "Fishing Skill Stamina", "Fishing"); } private static ConfigEntry<float> BindSkillStamina(SyncedConfiguration config, string key, string skill) { return config.Bind("5. Base Values", key, 0f, "Stamina added to Base Stamina at " + skill + " skill 100, scaling linearly with the skill level.", true, null); } private static void BindSkills(SyncedConfiguration config) { RunSkillGain = BindSkillGain(config, "Run Skill Gain", "Run"); JumpSkillGain = BindSkillGain(config, "Jump Skill Gain", "Jump"); SneakSkillGain = BindSkillGain(config, "Sneak Skill Gain", "Sneak"); SwimSkillGain = BindSkillGain(config, "Swim Skill Gain", "Swim"); FishingSkillGain = BindSkillGain(config, "Fishing Skill Gain", "Fishing"); } private static ConfigEntry<float> BindSkillGain(SyncedConfiguration config, string key, string skill) { return config.Bind("6. Skills", key, 1f, "Multiplier on the experience the " + skill + " skill earns. 1 = untouched. The world's skill gain rate still applies on top.", true, null); } private static void BindWorldRates(SyncedConfiguration config) { FoodRate = config.Bind("7. World Rates", "Food Rate", 0f, string.Format("Overrides the world modifier '{0}' while above 0. 0 leaves the world's value.", "food rate: how fast food runs out") + " Applies together with Duration Modifier; Eat Again At is a fraction of the resulting duration.", true, null); StaminaRate = config.Bind("7. World Rates", "Stamina Rate", 0f, string.Format("Overrides the world modifier '{0}' while above 0. 0 leaves the world's value.", "stamina rate: all stamina use") + " Multiplies with every cost multiplier in section 4.", true, null); MoveStaminaRate = config.Bind("7. World Rates", "Move Stamina Rate", 0f, string.Format("Overrides the world modifier '{0}' while above 0. 0 leaves the world's value.", "move stamina rate: running, jumping and swimming") + " Multiplies with Run, Jump and Swim Cost in section 4 and with Stamina Rate.", true,