// Consolidated decompiled source — Wubarrk-Fatty v1.1.2 // Generated by Hexium's decompiled-source browser. Best-effort concatenation of every type in this // version's manifest — decompiler output isn't guaranteed to compile as-is. using System; using System.Runtime.CompilerServices; using BepInEx.Configuration; using JetBrains.Annotations; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using HarmonyLib; using UnityEngine; using BepInEx; using TMPro; using BepInEx.Logging; using Fatty.Compat; using Fatty.Configuration; using Fatty.Patches; using Fatty.Progression; using System.Text.RegularExpressions; using BepInEx.Bootstrap; using System.Text; using Fatty.Synergies; using UnityEngine.UI; using Fatty.Regen; using Newtonsoft.Json; using ServerSync; // ---- plugins/Fatty.dll :: Microsoft.CodeAnalysis.EmbeddedAttribute ---- namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } // ---- plugins/Fatty.dll :: ServerSync.OwnConfigEntryBase ---- namespace ServerSync { [PublicAPI] public abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } } // ---- plugins/Fatty.dll :: ServerSync.SyncedConfigEntry`1 ---- namespace ServerSync { [PublicAPI] public class SyncedConfigEntry(ConfigEntry sourceConfig) : OwnConfigEntryBase { public readonly ConfigEntry SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } } // ---- plugins/Fatty.dll :: ServerSync.CustomSyncedValueBase ---- namespace ServerSync { public abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } } // ---- plugins/Fatty.dll :: ServerSync.CustomSyncedValue`1 ---- namespace ServerSync { [PublicAPI] public sealed class CustomSyncedValue : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } } // ---- plugins/Fatty.dll :: ServerSync.ConfigurationManagerAttributes ---- namespace ServerSync { internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } } // ---- plugins/Fatty.dll :: ServerSync.ConfigSync ---- namespace ServerSync { [PublicAPI] public class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register(configSync2.Name + " ConfigSync", configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections"); } } if (isServer) { __instance.StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ZNet.instance.StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List CurrentList = new List(adminList.GetList()); while (true) { yield return new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List(adminList.GetList()); List list = ZNet.instance.GetPeers().Where(delegate(ZNetPeer p) { string hostName = p.m_rpc.GetSocket().GetHostName(); return ((object)listContainsId != null) ? ((bool)listContainsId.Invoke(ZNet.instance, new object[2] { adminList, hostName })) : adminList.Contains(hostName); }).ToList(); SendAdmin(ZNet.instance.GetPeers().Except(list).ToList(), isAdmin: false); SendAdmin(list, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register(configSync.Name + " ConfigSync", configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary configValues = new Dictionary(); public readonly Dictionary customValues = new Dictionary(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket(ISocket original) : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List Package = new List(); public readonly ISocket Original = original; public new bool IsConnected() { return Original.IsConnected(); } public new ZPackage Recv() { return Original.Recv(); } public new int GetSendQueueSize() { return Original.GetSendQueueSize(); } public new int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public new bool IsHost() { return Original.IsHost(); } public new void Dispose() { Original.Dispose(); } public new bool GotNewData() { return Original.GotNewData(); } public new void Close() { Original.Close(); } public new string GetEndPointString() { return Original.GetEndPointString(); } public new void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(out totalSent, out totalRecv); } public new void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(out localQuality, out remoteQuality, out ping, out outByteSec, out inByteSec); } public new ISocket Accept() { return Original.Accept(); } public new int GetHostPort() { return Original.GetHostPort(); } public new bool Flush() { return Original.Flush(); } public new string GetHostName() { return Original.GetHostName(); } public new void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public new void Send(ZPackage pkg) { int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == "PeerInfo".GetStableHashCode() || num == "RoutedRPC".GetStableHashCode() || num == "ZDOData".GetStableHashCode()) && !finished) { ZPackage zPackage = new ZPackage(pkg.GetArray()); zPackage.SetPos(pos); Package.Add(zPackage); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary? __state, ZNet __instance, ZRpc rpc) { if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); if (AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }) is ZNetPeer obj && ZNet.m_onlineBackend != OnlineBackendType.Steamworks) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); if (fieldInfo.GetValue(obj) is ZPlayFabSocket zPlayFabSocket) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, zPlayFabSocket.m_remotePlayerId); } fieldInfo.SetValue(obj, bufferingSocket); } if (__state == null) { __state = new Dictionary(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }); peer = obj as ZNetPeer; if (peer == null) { SendBufferedData(); } else { __instance.StartCoroutine(sendAsync()); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List list = new List(); if (configSync.CurrentVersion != null) { list.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList syncedList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); list.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)methodInfo == null) ? ((object)syncedList.Contains(rpc.GetSocket().GetHostName())) : methodInfo.Invoke(ZNet.instance, new object[2] { syncedList, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, list, partial: false); yield return __instance.StartCoroutine(configSync.sendZPackage(new List { peer }, package)); } SendBufferedData(); } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); if (AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }) is ZNetPeer obj2) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(obj2, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } } } private class PackageEntry { public string section; public string key; public Type type; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] private static class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || ownConfigEntryBase.LocalBaseValue == null) { return true; } try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning($"Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}; Value: {value}"); } return false; } } private class InvalidDeserializationTypeException : Exception { public string expected; public string received; public string field = ""; } public static bool ProcessingServerUpdate; public readonly string Name; public string? DisplayName; public string? CurrentVersion; public string? MinimumRequiredVersion; public bool ModRequired; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet configSyncs; private readonly HashSet allConfigs = new HashSet(); private HashSet allCustomValues = new HashSet(); private static bool isServer; private static bool lockExempt; private OwnConfigEntryBase? lockedConfig; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary> configValueCache = new Dictionary>(); private readonly List> cacheExpirations = new List>(); private static long packageCounter; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0051; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag == true; } if (num) { return !lockExempt; } goto IL_0051; IL_0051: return false; } set { forceConfigLocking = value; } } public bool IsAdmin { get { if (!lockExempt) { return isSourceOfTruth; } return true; } } public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get; private set; } public event Action? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry) { OwnConfigEntryBase ownConfigEntryBase = configData(configEntry); SyncedConfigEntry syncedEntry = ownConfigEntryBase as SyncedConfigEntry; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry(configEntry); AccessTools.DeclaredField(typeof(ConfigDescription), "k__BackingField").SetValue(configEntry.Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(configEntry.Description.Tags ?? Array.Empty()).Concat(new SyncedConfigEntry[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(ZRoutedRpc.Everybody, configEntry); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry AddLockingConfigEntry(ConfigEntry lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[1] { "serverversion" }).Contains(customValue.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(ZRoutedRpc.Everybody, customValue); } }; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { try { if (isServer && IsLocked) { string text = SnatchCurrentlyHandlingRPC.currentRpc?.GetSocket()?.GetHostName(); if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList syncedList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? syncedList.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { syncedList, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair kv) { if (kv.Key < DateTimeOffset.Now.Ticks) { configValueCache.Remove(kv.Value); return true; } return false; }); byte b = package.ReadByte(); if ((b & 2) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out var value)) { value = new SortedDictionary(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value.Add(key, package.ReadByteArray()); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4) != 0) { MemoryStream stream = new MemoryStream(package.ReadByteArray()); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); ConfigFile configFile = null; bool saveOnConfigSet = false; foreach (KeyValuePair configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (configFile == null) { configFile = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (configFile != null) { configFile.SaveOnConfigSet = saveOnConfigSet; configFile.Save(); } foreach (KeyValuePair customValue in parsedConfigs.customValues) { if (!isServer) { CustomSyncedValueBase key2 = customValue.Key; if (key2.LocalBaseValue == null) { key2.LocalBaseValue = customValue.Key.BoxedValue; } } customValue.Key.BoxedValue = customValue.Value; } Debug.Log(string.Format("Received {0} configs and {1} custom values from {2} for mod {3}", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "the server", DisplayName ?? Name)); if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = package.ReadInt(); for (int num2 = 0; num2 < num; num2++) { string text = package.ReadString(); string text2 = package.ReadString(); string text3 = package.ReadString(); Type type = Type.GetType(text3); if (text3 == "" || type != null) { object obj; try { obj = ((text3 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning("Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected); continue; } OwnConfigEntryBase value2; if (text == "Internal") { CustomSyncedValueBase value; if (text2 == "serverversion") { if (obj?.ToString() != CurrentVersion) { Debug.LogWarning("Received server version is not equal: server version = " + (obj?.ToString() ?? "null") + "; local version = " + (CurrentVersion ?? "unknown")); } } else if (text2 == "lockexempt") { if (obj is bool flag) { lockExempt = flag; } } else if (dictionary2.TryGetValue(text2, out value)) { if ((text3 == "" && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null)) || GetZPackageTypeString(value.Type) == text3) { parsedConfigs.customValues[value] = obj; continue; } Debug.LogWarning("Got unexpected type " + text3 + " for internal value " + text2 + " for mod " + (DisplayName ?? Name) + ", expecting " + value.Type.AssemblyQualifiedName); } } else if (dictionary.TryGetValue(text + "_" + text2, out value2)) { Type type2 = configType(value2.BaseConfig); if ((text3 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text3) { parsedConfigs.configValues[value2] = obj; continue; } Debug.LogWarning("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName); } else { Debug.LogWarning("Received unknown config entry " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ". This may happen if client and server versions of the mod do not match."); } continue; } Debug.LogWarning("Got invalid type " + text3 + ", abort reading of received configs"); return new ParsedConfigs(); } return parsedConfigs; } private static bool isWritableConfig(OwnConfigEntryBase config) { ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config)); if (configSync == null) { return true; } if (!configSync.IsSourceOfTruth && config.SynchronizedConfig && config.LocalBaseValue != null) { if (!configSync.IsLocked) { if (config == configSync.lockedConfig) { return lockExempt; } return true; } return false; } return true; } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { configAttribute(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { ConfigFile configFile = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { if (configFile == null) { configFile = item.BaseConfig.ConfigFile; saveOnConfigSet = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (configFile != null) { configFile.SaveOnConfigSet = saveOnConfigSet; } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase config) => config.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private IEnumerator distributeConfigToPeers(ZNetPeer peer, ZPackage package) { ZRoutedRpc rpc = ZRoutedRpc.instance; if (rpc == null) { yield break; } byte[] data = package.GetArray(); if (data != null && data.LongLength > 250000) { int fragments = (int)(1 + (data.LongLength - 1) / 250000); long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } if (peer.m_socket.IsConnected()) { ZPackage zPackage = new ZPackage(); zPackage.Write((byte)2); zPackage.Write(packageIdentifier); zPackage.Write(fragment); zPackage.Write(fragments); zPackage.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(zPackage); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string text = Name + " ConfigSync"; if (isServer) { peer.m_rpc.Invoke(text, pkg); } else { rpc.InvokeRoutedRPC(peer.m_server ? 0 : peer.m_uid, text, pkg); } } IEnumerable waitForQueue() { float timeout = Time.time + 30f; while (peer.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log($"Disconnecting {peer.m_uid} after 30 seconds config sending timeout"); peer.m_rpc.Invoke("Error", ZNet.ConnectionStatus.ErrorConnectFailed); ZNet.instance.Disconnect(peer); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!ZNet.instance) { return Enumerable.Empty().GetEnumerator(); } List list = (List)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != ZRoutedRpc.Everybody) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List peers, ZPackage package) { if (!ZNet.instance) { yield break; } byte[] array = package.GetArray(); if (array != null && array.LongLength > 10000) { ZPackage zPackage = new ZPackage(); zPackage.Write((byte)4); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, System.IO.Compression.CompressionLevel.Optimal)) { deflateStream.Write(array, 0, array.Length); } zPackage.Write(memoryStream.ToArray()); package = zPackage; } List> writers = (from p in peers where p.IsReady() select distributeConfigToPeers(p, package)).ToList(); writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet.instance?.StartCoroutine(sendZPackage(target, package)); } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet.instance?.StartCoroutine(sendZPackage(target, package)); } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType().SingleOrDefault(); } public static SyncedConfigEntry? ConfigData(ConfigEntry config) { return config.Description.Tags?.OfType>().SingleOrDefault(); } private static T configAttribute(ConfigEntryBase config) { return config.Description.Tags.OfType().First(); } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { if (!type.IsEnum) { return type; } return Enum.GetUnderlyingType(type); } private static ZPackage ConfigsToPackage(IEnumerable? configs = null, IEnumerable? customValues = null, IEnumerable? packageEntries = null, bool partial = true) { List list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List(); List list2 = customValues?.ToList() ?? new List(); ZPackage zPackage = new ZPackage(); zPackage.Write(partial ? ((byte)1) : ((byte)0)); zPackage.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty()) { AddEntryToPackage(zPackage, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(zPackage, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(zPackage, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return zPackage; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { package.Write(entry.section); package.Write(entry.key); package.Write((entry.value == null) ? "" : GetZPackageTypeString(entry.type)); AddValueToZPackage(package, entry.value); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object? value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture); } else { if (value is ICollection collection) { package.Write(collection.Count); { foreach (object item in collection) { AddValueToZPackage(package, item); } return; } } if ((object)type != null && type.IsValueType && !type.IsPrimitive) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); package.Write(fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { package.Write(GetZPackageTypeString(fieldInfo.FieldType)); AddValueToZPackage(package, fieldInfo.GetValue(value)); } return; } } ZRpc.Serialize(new object[1] { value }, ref package); } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { if ((object)type != null && type.IsValueType && !type.IsPrimitive && !type.IsEnum) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = package.ReadInt(); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = package.ReadString(); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadValueWithTypeFromZPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = package.ReadInt(); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type); Type type2 = typeof(KeyValuePair<, >).MakeGenericType(type.GenericTypeArguments); FieldInfo field = type2.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type2.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj = ReadValueWithTypeFromZPackage(package, type2); dictionary.Add(field.GetValue(obj), field2.GetValue(obj)); } return dictionary; } if (type != typeof(List) && type.IsGenericType) { Type type3 = typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments[0]); if ((object)type3 != null && type3.IsAssignableFrom(type)) { int num3 = package.ReadInt(); object obj2 = Activator.CreateInstance(type); MethodInfo method = type3.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj2, new object[1] { ReadValueWithTypeFromZPackage(package, type.GenericTypeArguments[0]) }); } return obj2; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.DeclaredField(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type); List parameters = new List(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref parameters); return parameters.First(); } } } // ---- plugins/Fatty.dll :: ServerSync.VersionCheck ---- namespace ServerSync { [PublicAPI] [HarmonyPatch] public class VersionCheck { private static readonly HashSet versionChecks; private static readonly Dictionary notProcessedNames; public string Name; private string? displayName; private string? currentVersion; private string? minimumRequiredVersion; public bool ModRequired = true; private string? ReceivedCurrentVersion; private string? ReceivedMinimumRequiredVersion; private readonly List ValidatedClients = new List(); private ConfigSync? ConfigSync; public string DisplayName { get { return displayName ?? Name; } set { displayName = value; } } public string CurrentVersion { get { return currentVersion ?? "0.0.0"; } set { currentVersion = value; } } public string MinimumRequiredVersion { get { string text = minimumRequiredVersion; if (text == null) { if (!ModRequired) { return "0.0.0"; } text = CurrentVersion; } return text; } set { minimumRequiredVersion = value; } } private static void PatchServerSync() { Patches patchInfo = PatchProcessor.GetPatchInfo(AccessTools.DeclaredMethod(typeof(ZNet), "Awake")); if (patchInfo != null && patchInfo.Postfixes.Count((Patch p) => p.PatchMethod.DeclaringType == typeof(ConfigSync.RegisterRPCPatch)) > 0) { return; } Harmony harmony = new Harmony("org.bepinex.helpers.ServerSync"); foreach (Type item in from t in typeof(ConfigSync).GetNestedTypes(BindingFlags.NonPublic).Concat(new Type[1] { typeof(VersionCheck) }) where t.IsClass select t) { harmony.PatchAll(item); } } static VersionCheck() { versionChecks = new HashSet(); notProcessedNames = new Dictionary(); typeof(ThreadingHelper).GetMethod("StartSyncInvoke").Invoke(ThreadingHelper.Instance, new object[1] { new Action(PatchServerSync) }); } public VersionCheck(string name) { Name = name; ModRequired = true; versionChecks.Add(this); } public VersionCheck(ConfigSync configSync) { ConfigSync = configSync; Name = ConfigSync.Name; versionChecks.Add(this); } public void Initialize() { ReceivedCurrentVersion = null; ReceivedMinimumRequiredVersion = null; if (ConfigSync != null) { Name = ConfigSync.Name; DisplayName = ConfigSync.DisplayName; CurrentVersion = ConfigSync.CurrentVersion; MinimumRequiredVersion = ConfigSync.MinimumRequiredVersion; ModRequired = ConfigSync.ModRequired; } } private bool IsVersionOk() { if (ReceivedMinimumRequiredVersion == null || ReceivedCurrentVersion == null) { return !ModRequired; } bool num = new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion); bool flag = new Version(ReceivedCurrentVersion) >= new Version(MinimumRequiredVersion); return num && flag; } private string ErrorClient() { if (ReceivedMinimumRequiredVersion == null) { return DisplayName + " is not installed on the server."; } if (!(new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion))) { return DisplayName + " needs to be at least version " + ReceivedMinimumRequiredVersion + ". You have version " + CurrentVersion + "."; } return DisplayName + " may not be higher than version " + ReceivedCurrentVersion + ". You have version " + CurrentVersion + "."; } private string ErrorServer(ZRpc rpc) { return "Disconnect: The client (" + rpc.GetSocket().GetHostName() + ") doesn't have the correct " + DisplayName + " version " + MinimumRequiredVersion; } private string Error(ZRpc? rpc = null) { if (rpc != null) { return ErrorServer(rpc); } return ErrorClient(); } private static VersionCheck[] GetFailedClient() { return versionChecks.Where((VersionCheck check) => !check.IsVersionOk()).ToArray(); } private static VersionCheck[] GetFailedServer(ZRpc rpc) { return versionChecks.Where((VersionCheck check) => check.ModRequired && !check.ValidatedClients.Contains(rpc)).ToArray(); } private static void Logout() { Game.instance.Logout(); AccessTools.DeclaredField(typeof(ZNet), "m_connectionStatus").SetValue(null, ZNet.ConnectionStatus.ErrorVersion); } private static void DisconnectClient(ZRpc rpc) { rpc.Invoke("Error", 3); } private static void CheckVersion(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, null); } private static void CheckVersion(ZRpc rpc, ZPackage pkg, Action? original) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); string text3 = pkg.ReadString(); bool flag = false; foreach (VersionCheck versionCheck in versionChecks) { if (!(text != versionCheck.Name)) { Debug.Log("Received " + versionCheck.DisplayName + " version " + text3 + " and minimum version " + text2 + " from the " + (ZNet.instance.IsServer() ? "client" : "server") + "."); versionCheck.ReceivedMinimumRequiredVersion = text2; versionCheck.ReceivedCurrentVersion = text3; if (ZNet.instance.IsServer() && versionCheck.IsVersionOk()) { versionCheck.ValidatedClients.Add(rpc); } flag = true; } } if (flag) { return; } pkg.SetPos(0); if (original != null) { original(rpc, pkg); if (pkg.GetPos() == 0) { notProcessedNames.Add(text, text3); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPrefix] private static bool RPC_PeerInfo(ZRpc rpc, ZNet __instance) { VersionCheck[] array = (__instance.IsServer() ? GetFailedServer(rpc) : GetFailedClient()); if (array.Length == 0) { return true; } VersionCheck[] array2 = array; for (int i = 0; i < array2.Length; i++) { Debug.LogWarning(array2[i].Error(rpc)); } if (__instance.IsServer()) { DisconnectClient(rpc); } else { Logout(); } return false; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPrefix] private static void RegisterAndCheckVersion(ZNetPeer peer, ZNet __instance) { notProcessedNames.Clear(); IDictionary dictionary = (IDictionary)typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(peer.m_rpc); if (dictionary.Contains("ServerSync VersionCheck".GetStableHashCode())) { object obj = dictionary["ServerSync VersionCheck".GetStableHashCode()]; Action action = (Action)obj.GetType().GetField("m_action", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj); peer.m_rpc.Register("ServerSync VersionCheck", delegate(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, action); }); } else { peer.m_rpc.Register("ServerSync VersionCheck", CheckVersion); } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.Initialize(); if (versionCheck.ModRequired || __instance.IsServer()) { Debug.Log("Sending " + versionCheck.DisplayName + " version " + versionCheck.CurrentVersion + " and minimum version " + versionCheck.MinimumRequiredVersion + " to the " + (__instance.IsServer() ? "client" : "server") + "."); ZPackage zPackage = new ZPackage(); zPackage.Write(versionCheck.Name); zPackage.Write(versionCheck.MinimumRequiredVersion); zPackage.Write(versionCheck.CurrentVersion); peer.m_rpc.Invoke("ServerSync VersionCheck", zPackage); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] [HarmonyPrefix] private static void RemoveDisconnected(ZNetPeer peer, ZNet __instance) { if (!__instance.IsServer()) { return; } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.ValidatedClients.Remove(peer.m_rpc); } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] [HarmonyPostfix] private static void ShowConnectionError(FejdStartup __instance) { if (!__instance.m_connectionFailedPanel.activeSelf || ZNet.GetConnectionStatus() != ZNet.ConnectionStatus.ErrorVersion) { return; } bool flag = false; VersionCheck[] failedClient = GetFailedClient(); if (failedClient.Length != 0) { string text = string.Join("\n", failedClient.Select((VersionCheck check) => check.Error())); TMP_Text connectionFailedError = __instance.m_connectionFailedError; connectionFailedError.text = connectionFailedError.text + "\n" + text; flag = true; } foreach (KeyValuePair item in notProcessedNames.OrderBy((KeyValuePair kv) => kv.Key)) { if (!__instance.m_connectionFailedError.text.Contains(item.Key)) { TMP_Text connectionFailedError2 = __instance.m_connectionFailedError; connectionFailedError2.text = connectionFailedError2.text + "\nServer expects you to have " + item.Key + " (Version: " + item.Value + ") installed."; flag = true; } } if (flag) { RectTransform component = __instance.m_connectionFailedPanel.transform.Find("Image").GetComponent(); Vector2 sizeDelta = component.sizeDelta; sizeDelta.x = 675f; component.sizeDelta = sizeDelta; __instance.m_connectionFailedError.ForceMeshUpdate(); float num = __instance.m_connectionFailedError.renderedHeight + 105f; RectTransform component2 = component.transform.Find("ButtonOk").GetComponent(); component2.anchoredPosition = new Vector2(component2.anchoredPosition.x, component2.anchoredPosition.y - (num - component.sizeDelta.y) / 2f); sizeDelta = component.sizeDelta; sizeDelta.y = num; component.sizeDelta = sizeDelta; } } } } // ---- plugins/Fatty.dll :: Fatty.FattyPlugin ---- namespace Fatty { [BepInPlugin("wubarrk.fatty", "Fatty", "1.1.2")] public class FattyPlugin : BaseUnityPlugin { public const string PluginGUID = "wubarrk.fatty"; public const string PluginName = "Fatty"; public const string PluginVersion = "1.1.2"; private readonly Harmony _harmony = new Harmony("wubarrk.fatty"); public static FattyPlugin Instance { get; private set; } public ManualLogSource Log { get; private set; } private void Awake() { Instance = this; Log = base.Logger; FoodDurationMultiplierCompat.Initialize(); ConfigManager.Init(base.Config); CategoryManager.Init(); if (ConfigManager.rebuildFoodData.Value) { CategoryManager.RequestRebuild(); } _harmony.PatchAll(); Log.LogInfo("Fatty v1.1.2 loaded successfully! Time to get fat."); } private void OnGUI() { try { FeastLedgerGui.Draw(); } catch (Exception arg) { Log.LogError($"Fatty Mod: Feature Lost - Feast Ledger GUI. Reason: {arg}"); } } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.DrinkBuffs ---- namespace Fatty.Synergies { public static class DrinkBuffs { public const string TastyMeadPrefab = "MeadTasty"; private static bool _built; private static readonly Dictionary _byCategory = new Dictionary(); private static readonly Dictionary _hashByCategory = new Dictionary(); private static SE_Stats _drunk; public static int DrunkHash { get; private set; } public static IEnumerable AllHashes { get { foreach (int value in _hashByCategory.Values) { yield return value; } if (DrunkHash != 0) { yield return DrunkHash; } } } public static void Register(ObjectDB odb) { if (odb == null || odb.m_StatusEffects == null) { return; } if (!_built) { Build(); } ApplyConfigValues(); foreach (SE_Stats value in _byCategory.Values) { AddIfMissing(odb, value); } AddIfMissing(odb, _drunk); } private static void AddIfMissing(ObjectDB odb, StatusEffect se) { if (!(se == null) && odb.GetStatusEffect(se.NameHash()) == null) { odb.m_StatusEffects.Add(se); } } private static SE_Stats CreateBase(string internalName, string displayName, string tooltip) { SE_Stats sE_Stats = ScriptableObject.CreateInstance(); sE_Stats.name = internalName; sE_Stats.m_name = displayName; sE_Stats.m_tooltip = tooltip; sE_Stats.m_startMessage = displayName; sE_Stats.m_startMessageType = MessageHud.MessageType.TopLeft; sE_Stats.m_ttl = 0f; sE_Stats.m_healthRegenMultiplier = 1f; sE_Stats.m_staminaRegenMultiplier = 1f; sE_Stats.m_eitrRegenMultiplier = 1f; sE_Stats.m_damageModifier = 1f; return sE_Stats; } private static void Build() { SE_Stats sE_Stats = CreateBase("SE_Fatty_ArcaneDraught", "Arcane Draught", "The old magic runs in you.\nEitr regeneration and magic skills improved."); sE_Stats.m_icon = SynergyIcons.Resolve("ArcaneDraught", SynergyIcons.ArcaneDraught); Add("Eitr", sE_Stats); SE_Stats sE_Stats2 = CreateBase("SE_Fatty_WarriorsDraught", "Warrior's Draught", "Fed for the fight.\nSword and axe skill improved."); sE_Stats2.m_icon = SynergyIcons.Resolve("WarriorsDraught", SynergyIcons.WarriorsDraught); Add("Meat", sE_Stats2); SE_Stats sE_Stats3 = CreateBase("SE_Fatty_RevelersDraught", "Reveler's Draught", "The feast is on.\nStamina regeneration, running and jumping improved."); sE_Stats3.m_icon = SynergyIcons.Resolve("RevelersDraught", SynergyIcons.RevelersDraught); Add("Sweet", sE_Stats3); SE_Stats sE_Stats4 = CreateBase("SE_Fatty_SeafarersDraught", "Seafarer's Draught", "Salt in your blood.\nSwimming and fishing improved."); sE_Stats4.m_swimSpeedModifier = 0.1f; sE_Stats4.m_icon = SynergyIcons.Resolve("SeafarersDraught", SynergyIcons.SeafarersDraught); Add("Fish", sE_Stats4); _drunk = CreateBase("SE_Fatty_Drunk", "Drunk", "Skal!\nHarder hitting and harder to stagger - but your guard is a rumour."); _drunk.m_icon = SynergyIcons.Resolve("Drunk", SynergyIcons.Drunk); DrunkHash = _drunk.NameHash(); _built = true; } private static void Add(string category, SE_Stats se) { _byCategory[category] = se; _hashByCategory[category] = se.NameHash(); } private static void ApplyConfigValues() { float value = ConfigManager.drinkBuffSkillLevels.Value; if (_byCategory.TryGetValue("Eitr", out var value2)) { value2.m_eitrRegenMultiplier = Mathf.Max(0.01f, ConfigManager.drinkBuffEitrRegen.Value); value2.m_skillLevel = Skills.SkillType.ElementalMagic; value2.m_skillLevelModifier = value; value2.m_skillLevel2 = Skills.SkillType.BloodMagic; value2.m_skillLevelModifier2 = value; } if (_byCategory.TryGetValue("Meat", out var value3)) { value3.m_skillLevel = Skills.SkillType.Swords; value3.m_skillLevelModifier = value; value3.m_skillLevel2 = Skills.SkillType.Axes; value3.m_skillLevelModifier2 = value; } if (_byCategory.TryGetValue("Sweet", out var value4)) { value4.m_staminaRegenMultiplier = Mathf.Max(0.01f, ConfigManager.drinkBuffStaminaRegen.Value); value4.m_skillLevel = Skills.SkillType.Run; value4.m_skillLevelModifier = value; value4.m_skillLevel2 = Skills.SkillType.Jump; value4.m_skillLevelModifier2 = value; } if (_byCategory.TryGetValue("Fish", out var value5)) { value5.m_skillLevel = Skills.SkillType.Swim; value5.m_skillLevelModifier = value; value5.m_skillLevel2 = Skills.SkillType.Fishing; value5.m_skillLevelModifier2 = value; } if (_drunk != null) { _drunk.m_modifyAttackSkill = Skills.SkillType.All; _drunk.m_damageModifier = 1f + Mathf.Max(0f, ConfigManager.drunkDamageBonus.Value); _drunk.m_staggerModifier = 0f - Mathf.Clamp01(ConfigManager.drunkStaggerResist.Value); _drunk.m_skillLevel = Skills.SkillType.Blocking; _drunk.m_skillLevelModifier = 0f - Mathf.Abs(ConfigManager.drunkBlockingPenalty.Value); _drunk.m_skillLevel2 = Skills.SkillType.Blocking; _drunk.m_skillLevelModifier2 = 0f; _drunk.m_blockStaminaUseModifier = Mathf.Max(0f, ConfigManager.drunkBlockStaminaPenalty.Value); } } public static void Evaluate(Player player, SEMan seman, List foods) { if (seman == null) { return; } bool value = ConfigManager.enableDrinkBuffs.Value; string text = null; int num = 0; if (value && foods != null) { foreach (Player.Food food in foods) { if (food == null || food.m_item == null) { continue; } string text2 = PlayerPatches.ResolvePrefabName(food); if (!CategoryManager.IsDrinkPrefab(text2)) { continue; } if (text2 == "MeadTasty") { num++; } if (text == null) { string categoryForPrefab = CategoryManager.GetCategoryForPrefab(text2); if (_hashByCategory.ContainsKey(categoryForPrefab)) { text = categoryForPrefab; } } } } foreach (KeyValuePair item in _hashByCategory) { bool flag = value && item.Key == text; ApplyEffect(seman, item.Value, flag); if (flag) { string cat = item.Key; float remaining = SynergyManager.NthLargestRemaining(foods, (string prefab) => CategoryManager.IsDrinkPrefab(prefab) && CategoryManager.GetCategoryForPrefab(prefab) == cat, 1); SynergyManager.PublishRemaining(seman, item.Value, remaining); } } int num2 = Mathf.Clamp(ConfigManager.drunkStacksRequired.Value, 1, Mathf.Max(1, ConfigManager.maxFoodStacks.Value)); bool flag2 = value && ConfigManager.enableDrunk.Value && num >= num2; ApplyEffect(seman, DrunkHash, flag2); if (flag2) { float remaining2 = SynergyManager.NthLargestRemaining(foods, (string prefab) => prefab == "MeadTasty", num2); SynergyManager.PublishRemaining(seman, DrunkHash, remaining2); } } private static void ApplyEffect(SEMan seman, int nameHash, bool qualified) { if (nameHash != 0) { bool flag = seman.HaveStatusEffect(nameHash); if (qualified && !flag) { seman.AddStatusEffect(nameHash, resetTime: true); } else if (!qualified && flag) { seman.RemoveStatusEffect(nameHash); } } } public static float GetMaxHealthBonus(string prefabName, float stackMultiplier) { if (!ConfigManager.enableDrinkBuffs.Value) { return 0f; } if (!CategoryManager.IsDrinkPrefab(prefabName)) { return 0f; } string categoryForPrefab = CategoryManager.GetCategoryForPrefab(prefabName); if (!_hashByCategory.ContainsKey(categoryForPrefab)) { return 0f; } return ConfigManager.drinkBuffMaxHealth.Value * stackMultiplier; } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.SynergyEffects ---- namespace Fatty.Synergies { public static class SynergyEffects { private static bool _built; private static SE_Stats _balanced; private static SE_Stats _sugar; private static SE_Stats _fisher; private static SE_Stats _lumber; public static int BalancedDietHash { get; private set; } public static int SugarRushHash { get; private set; } public static int FishermanHash { get; private set; } public static int LumberjackHash { get; private set; } public static void Register(ObjectDB odb) { if (!(odb == null) && odb.m_StatusEffects != null) { if (!_built) { BuildEffects(); } ApplyConfigValues(); AddIfMissing(odb, _balanced); AddIfMissing(odb, _sugar); AddIfMissing(odb, _fisher); AddIfMissing(odb, _lumber); } } private static void AddIfMissing(ObjectDB odb, StatusEffect se) { if (!(se == null) && odb.GetStatusEffect(se.NameHash()) == null) { odb.m_StatusEffects.Add(se); } } private static SE_Stats CreateBase(string internalName, string displayName, string tooltip) { SE_Stats sE_Stats = ScriptableObject.CreateInstance(); sE_Stats.name = internalName; sE_Stats.m_name = displayName; sE_Stats.m_tooltip = tooltip; sE_Stats.m_startMessage = displayName; sE_Stats.m_startMessageType = MessageHud.MessageType.TopLeft; sE_Stats.m_ttl = 0f; sE_Stats.m_healthRegenMultiplier = 1f; sE_Stats.m_staminaRegenMultiplier = 1f; sE_Stats.m_eitrRegenMultiplier = 1f; sE_Stats.m_damageModifier = 1f; return sE_Stats; } private static void BuildEffects() { _balanced = CreateBase("SE_Fatty_BalancedDiet", "Balanced Diet", "A well-rounded meal.\nHealth & Stamina regeneration greatly increased."); _balanced.m_icon = SynergyIcons.Resolve("BalancedDiet", SynergyIcons.BalancedDiet); BalancedDietHash = _balanced.NameHash(); _sugar = CreateBase("SE_Fatty_SugarRush", "Sugar Rush", "Buzzing with energy!\nMovement speed greatly increased."); _sugar.m_icon = SynergyIcons.Resolve("SugarRush", SynergyIcons.SugarRush); SugarRushHash = _sugar.NameHash(); _fisher = CreateBase("SE_Fatty_Fisherman", "Fisherman's Friend", "One with the water.\nImproved swim speed and reduced swim stamina use."); _fisher.m_swimSpeedModifier = 0.1f; _fisher.m_icon = SynergyIcons.Resolve("Fisherman", SynergyIcons.Fisherman); FishermanHash = _fisher.NameHash(); _lumber = CreateBase("SE_Fatty_Lumberjack", "Lumberjack's Feast", "Fed and ready to fell forests.\nBonus carry weight and woodcutting damage."); _lumber.m_modifyAttackSkill = Skills.SkillType.WoodCutting; _lumber.m_icon = SynergyIcons.Resolve("Lumberjack", SynergyIcons.Lumberjack); LumberjackHash = _lumber.NameHash(); _built = true; } private static void ApplyConfigValues() { if (_balanced != null) { float value = ConfigManager.balancedDietRegen.Value; _balanced.m_healthRegenMultiplier = value; _balanced.m_staminaRegenMultiplier = value; } if (_sugar != null) { _sugar.m_speedModifier = Mathf.Max(0f, ConfigManager.sugarRushSpeedBonus.Value - 1f); } if (_fisher != null) { _fisher.m_swimStaminaUseModifier = 0f - Mathf.Clamp01(ConfigManager.fishermanSwimStaminaReduction.Value); } if (_lumber != null) { _lumber.m_addMaxCarryWeight = ConfigManager.lumberjackCarryWeight.Value; _lumber.m_damageModifier = 1f + ConfigManager.lumberjackWoodcuttingDamage.Value; } } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.ObjectDB_Awake_Patch ---- namespace Fatty.Synergies { [HarmonyPatch(typeof(ObjectDB), "Awake")] public static class ObjectDB_Awake_Patch { [HarmonyPostfix] public static void Postfix(ObjectDB __instance) { try { SynergyEffects.Register(__instance); DrinkBuffs.Register(__instance); MilestonePerks.Register(__instance); } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Synergy Registration (ObjectDB.Awake). Reason: {arg}"); } } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.ObjectDB_CopyOtherDB_Patch ---- namespace Fatty.Synergies { [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] public static class ObjectDB_CopyOtherDB_Patch { [HarmonyPostfix] public static void Postfix(ObjectDB __instance) { try { SynergyEffects.Register(__instance); DrinkBuffs.Register(__instance); MilestonePerks.Register(__instance); } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Synergy Registration (ObjectDB.CopyOtherDB). Reason: {arg}"); } } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.SynergyIcons ---- namespace Fatty.Synergies { public static class SynergyIcons { private enum Emblem { Equals, Star, Wave, Tree, Rune, Blade, Horn, Hook, Bubbles } private const int Size = 64; private static readonly Color BackgroundColor = new Color(0.05f, 0.05f, 0.06f, 1f); private static MethodInfo _loadImage; private static readonly Color Gold = new Color(0.82f, 0.66f, 0.22f, 1f); private static readonly Color GoldLight = new Color(1f, 0.9f, 0.45f, 1f); public static Sprite Resolve(string baseName, Func proceduralFallback) { try { Sprite sprite = LoadEmbedded(baseName); if (sprite != null) { return sprite; } } catch (Exception ex) { FattyPlugin.Instance.Log.LogWarning("Fatty: could not load embedded icon '" + baseName + "': " + ex.Message); } try { return proceduralFallback?.Invoke(); } catch (Exception ex2) { FattyPlugin.Instance.Log.LogWarning("Fatty: could not draw fallback icon '" + baseName + "': " + ex2.Message); return null; } } private static Sprite LoadEmbedded(string baseName) { Assembly assembly = typeof(SynergyIcons).Assembly; string value = "." + baseName + ".png"; string[] manifestResourceNames = assembly.GetManifestResourceNames(); foreach (string text in manifestResourceNames) { if (!text.EndsWith(value, StringComparison.OrdinalIgnoreCase)) { continue; } using Stream stream = assembly.GetManifestResourceStream(text); if (stream == null) { continue; } using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); byte[] data = memoryStream.ToArray(); Texture2D texture2D = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, name = "Fatty_" + baseName }; if (!LoadPng(texture2D, data)) { UnityEngine.Object.Destroy(texture2D); return null; } FlattenAlpha(texture2D, BackgroundColor); return Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 100f); } return null; } private static bool LoadPng(Texture2D tex, byte[] data) { if (_loadImage == null) { Type type = AccessTools.TypeByName("UnityEngine.ImageConversion"); _loadImage = ((type != null) ? AccessTools.Method(type, "LoadImage", new Type[2] { typeof(Texture2D), typeof(byte[]) }) : null); } if (_loadImage == null) { return false; } object obj = _loadImage.Invoke(null, new object[2] { tex, data }); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } private static void FlattenAlpha(Texture2D tex, Color bg) { Color[] pixels = tex.GetPixels(); for (int i = 0; i < pixels.Length; i++) { Color color = pixels[i]; float a = color.a; pixels[i] = new Color(color.r * a + bg.r * (1f - a), color.g * a + bg.g * (1f - a), color.b * a + bg.b * (1f - a), 1f); } tex.SetPixels(pixels); tex.Apply(updateMipmaps: false, makeNoLongerReadable: false); } public static Sprite BalancedDiet() { return Build(new Color(0.34f, 0.68f, 0.32f), Emblem.Equals); } public static Sprite SugarRush() { return Build(new Color(0.88f, 0.36f, 0.66f), Emblem.Star); } public static Sprite Fisherman() { return Build(new Color(0.24f, 0.55f, 0.85f), Emblem.Wave); } public static Sprite Lumberjack() { return Build(new Color(0.7f, 0.45f, 0.22f), Emblem.Tree); } public static Sprite ArcaneDraught() { return Build(new Color(0.45f, 0.35f, 0.78f), Emblem.Rune); } public static Sprite WarriorsDraught() { return Build(new Color(0.72f, 0.24f, 0.22f), Emblem.Blade); } public static Sprite RevelersDraught() { return Build(new Color(0.85f, 0.62f, 0.2f), Emblem.Horn); } public static Sprite SeafarersDraught() { return Build(new Color(0.2f, 0.62f, 0.62f), Emblem.Hook); } public static Sprite Drunk() { return Build(new Color(0.58f, 0.44f, 0.68f), Emblem.Bubbles); } private static Sprite Build(Color baseColor, Emblem emblem) { Texture2D texture2D = new Texture2D(64, 64, TextureFormat.RGBA32, mipChain: false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, name = "Fatty_SynergyIcon" }; Color[] array = new Color[4096]; float num = 31.5f; float num2 = 30.08f; float num3 = 25.6f; Color a = Color.Lerp(baseColor, Color.white, 0.3f); for (int i = 0; i < 64; i++) { for (int j = 0; j < 64; j++) { float num4 = (float)j - num; float num5 = (float)i - num; float num6 = Mathf.Sqrt(num4 * num4 + num5 * num5); Color color; if (num6 > num2) { color = BackgroundColor; } else if (num6 > num3) { color = Gold; } else { float t = Mathf.Clamp01(num6 / num3); color = Color.Lerp(a, baseColor, t); float nx = num4 / num3; float ny = num5 / num3; if (InEmblem(emblem, nx, ny)) { color = GoldLight; } } if (num6 > num2 - 1f && num6 <= num2) { color = Color.Lerp(BackgroundColor, color, Mathf.Clamp01(num2 - num6)); } color.a = 1f; array[i * 64 + j] = color; } } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false, makeNoLongerReadable: false); return Sprite.Create(texture2D, new Rect(0f, 0f, 64f, 64f), new Vector2(0.5f, 0.5f), 100f); } private static bool InEmblem(Emblem emblem, float nx, float ny) { switch (emblem) { case Emblem.Equals: if (Mathf.Abs(nx) < 0.52f) { if (!Between(ny, 0.14f, 0.32f)) { return Between(ny, -0.32f, -0.14f); } return true; } return false; case Emblem.Star: if (!(Mathf.Abs(nx) < 0.14f) || !(Mathf.Abs(ny) < 0.58f)) { if (Mathf.Abs(ny) < 0.14f) { return Mathf.Abs(nx) < 0.58f; } return false; } return true; case Emblem.Wave: { if (Mathf.Abs(nx) > 0.62f) { return false; } float num6 = 0.26f * Mathf.Sin(nx * MathF.PI * 2f); return Mathf.Abs(ny - num6) < 0.11f; } case Emblem.Tree: { float num3 = -0.5f; float num4 = 0.3f; if (Between(ny, num3, num4)) { float num5 = 0.55f * ((ny - num3) / (num4 - num3)); if (Mathf.Abs(nx) <= num5) { return true; } } if (Mathf.Abs(nx) < 0.1f) { return Between(ny, 0.3f, 0.52f); } return false; } case Emblem.Rune: if (Mathf.Abs(nx) < 0.1f && Mathf.Abs(ny) < 0.55f) { return true; } if (Between(ny, -0.42f, -0.02f) && Mathf.Abs(nx - (ny + 0.42f) * 0.95f) < 0.11f) { return true; } if (Between(ny, 0.02f, 0.42f) && Mathf.Abs(nx + (0.42f - ny) * 0.95f) < 0.11f) { return true; } return false; case Emblem.Blade: if (Mathf.Abs(nx) < 0.11f && Between(ny, -0.55f, 0.3f)) { return true; } if (Mathf.Abs(nx) < 0.36f && Between(ny, 0.3f, 0.4f)) { return true; } if (Mathf.Abs(nx) < 0.08f) { return Between(ny, 0.4f, 0.56f); } return false; case Emblem.Horn: { if (!Between(nx, -0.55f, 0.45f)) { return false; } float t = (nx + 0.55f) / 1f; float num = Mathf.Lerp(0.04f, 0.3f, t); float num2 = Mathf.Lerp(0.28f, -0.1f, t); return Mathf.Abs(ny - num2) < num; } case Emblem.Hook: if (Mathf.Abs(nx - 0.18f) < 0.1f && Between(ny, -0.52f, 0.12f)) { return true; } if (Mathf.Abs(Mathf.Sqrt((nx - 0.02f) * (nx - 0.02f) + (ny - 0.2f) * (ny - 0.2f)) - 0.24f) < 0.1f) { return ny > 0.06f; } return false; case Emblem.Bubbles: if (InCircle(nx, ny, -0.2f, 0.22f, 0.2f)) { return true; } if (InCircle(nx, ny, 0.18f, -0.02f, 0.16f)) { return true; } return InCircle(nx, ny, -0.06f, -0.34f, 0.12f); default: return false; } } private static bool InCircle(float nx, float ny, float cx, float cy, float r) { float num = nx - cx; float num2 = ny - cy; return num * num + num2 * num2 < r * r; } private static bool Between(float v, float lo, float hi) { if (v >= lo) { return v <= hi; } return false; } } } // ---- plugins/Fatty.dll :: Fatty.Synergies.SynergyManager ---- namespace Fatty.Synergies { [HarmonyPatch] public static class SynergyManager { public const string DrinkKey = "Drink"; [HarmonyPatch(typeof(Player), "UpdateFood")] [HarmonyPostfix] public static void Player_UpdateFood_Postfix(Player __instance, ref List ___m_foods) { try { if (!(__instance == null)) { SEMan sEMan = __instance.GetSEMan(); if (sEMan != null) { bool value = ConfigManager.enableSynergies.Value; Dictionary dictionary = CountCategories(___m_foods); bool qualified = value && dictionary["Meat"] >= 1 && dictionary["Vegetable"] >= 1; bool qualified2 = value && dictionary["Sweet"] >= 3 && dictionary["Drink"] >= 1; bool qualified3 = value && dictionary["Fish"] >= 2 && dictionary["Vegetable"] >= 1; bool qualified4 = value && dictionary["Meat"] >= 3 && dictionary["Drink"] >= 1; ApplySynergy(sEMan, SynergyEffects.BalancedDietHash, qualified, Mathf.Min(CategoryRemaining(___m_foods, "Meat", 1), CategoryRemaining(___m_foods, "Vegetable", 1))); ApplySynergy(sEMan, SynergyEffects.SugarRushHash, qualified2, Mathf.Min(CategoryRemaining(___m_foods, "Sweet", 3), DrinkRemaining(___m_foods, 1))); ApplySynergy(sEMan, SynergyEffects.FishermanHash, qualified3, Mathf.Min(CategoryRemaining(___m_foods, "Fish", 2), CategoryRemaining(___m_foods, "Vegetable", 1))); ApplySynergy(sEMan, SynergyEffects.LumberjackHash, qualified4, Mathf.Min(CategoryRemaining(___m_foods, "Meat", 3), DrinkRemaining(___m_foods, 1))); DrinkBuffs.Evaluate(__instance, sEMan, ___m_foods); } } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Dietary Synergies. Reason: {arg}"); } } public static Dictionary CountCategories(List foods) { Dictionary dictionary = new Dictionary(); string[] baseCategories = CategoryManager.BaseCategories; foreach (string key in baseCategories) { dictionary[key] = 0; } dictionary["Drink"] = 0; if (foods == null) { return dictionary; } foreach (Player.Food food in foods) { if (food != null && food.m_item != null) { string prefabName = PlayerPatches.ResolvePrefabName(food); string categoryForPrefab = CategoryManager.GetCategoryForPrefab(prefabName); if (dictionary.ContainsKey(categoryForPrefab)) { dictionary[categoryForPrefab]++; } if (CategoryManager.IsDrinkPrefab(prefabName)) { dictionary["Drink"]++; } } } return dictionary; } private static void ApplySynergy(SEMan seman, int nameHash, bool qualified, float remaining) { if (nameHash != 0) { bool flag = seman.HaveStatusEffect(nameHash); if (qualified && !flag) { seman.AddStatusEffect(nameHash, resetTime: true); } else if (!qualified && flag) { seman.RemoveStatusEffect(nameHash); } if (qualified) { PublishRemaining(seman, nameHash, remaining); } } } internal static void PublishRemaining(SEMan seman, int nameHash, float remaining) { if (!(remaining <= 0f)) { StatusEffect statusEffect = seman.GetStatusEffect(nameHash); if (!(statusEffect == null)) { statusEffect.m_ttl = remaining + statusEffect.GetDuration(); } } } internal static float CategoryRemaining(List foods, string category, int need) { return NthLargestRemaining(foods, (string prefab) => CategoryManager.GetCategoryForPrefab(prefab) == category, need); } internal static float DrinkRemaining(List foods, int need) { return NthLargestRemaining(foods, CategoryManager.IsDrinkPrefab, need); } internal static float NthLargestRemaining(List foods, Func match, int need) { if (foods == null || need < 1) { return 0f; } List list = new List(); foreach (Player.Food food in foods) { if (food != null && food.m_item != null && match(PlayerPatches.ResolvePrefabName(food))) { list.Add(food.m_time); } } if (list.Count < need) { return 0f; } list.Sort(); list.Reverse(); return list[need - 1]; } } } // ---- plugins/Fatty.dll :: Fatty.Scanners.FoodScanner ---- namespace Fatty.Scanners { [HarmonyPatch] public static class FoodScanner { private static bool _hasScanned = false; private static readonly Regex WordSplitter = new Regex("[A-Z]?[a-z0-9]+|[A-Z0-9]+(?![a-z])", RegexOptions.Compiled); private static readonly HashSet DrinkWords = new HashSet { "tea", "coffee", "mead", "ale", "drink", "meadnog", "alenog", "shake", "smoothie", "shroomshake", "beer", "brew", "booze", "perry", "blaand", "nog", "cider", "juice", "infusion", "wine", "mjod", "grog", "elixir", "tonic", "draught", "wort" }; private static readonly HashSet MeatWords = new HashSet { "meat", "steak", "pork", "sausage", "sausages", "meatbug", "boar", "deer", "wolf", "lox", "neck", "serpent", "troll", "bear", "hare", "chicken", "seeker", "asksvin", "volture", "bonemaw", "drake", "kraken", "ham", "jerky", "salami", "bacon", "entrails", "heart", "morgen", "ulv", "hakarl", "tentacle" }; private static readonly HashSet SweetWords = new HashSet { "berry", "berries", "cake", "sweet", "honey", "vineberry", "cookies", "cookie", "jam", "pie", "krem", "candy", "fruit" }; private static readonly HashSet FishWords = new HashSet { "fish", "salmon", "magmafish", "perch", "pike", "trout", "herring", "lutefisk", "rakfisk", "stockfish", "gravlaks", "puffers", "grouper", "tetra", "hafgufa" }; private static readonly HashSet VegetableWords = new HashSet { "turnip", "carrot", "onion", "mushroom", "shroom", "magecap", "vegetable", "veggies", "greens", "root", "roots", "barley", "flax", "seed", "seeds", "salad", "cabbage", "leek", "herb", "herbal", "bread", "porridge", "dumplings", "omelette", "potato", "pickled", "sap" }; private static readonly Dictionary KnownDrinkDiets = new Dictionary { { "MeadTasty", "Sweet" } }; private const int ItemsPerFrame = 100; private static void AddWords(string s, HashSet into) { if (string.IsNullOrEmpty(s)) { return; } foreach (Match item in WordSplitter.Matches(s)) { into.Add(item.Value.ToLowerInvariant()); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] [HarmonyPostfix] public static void ZNetScene_Awake_Postfix(ZNetScene __instance) { try { if (!_hasScanned && !(__instance == null)) { __instance.StartCoroutine(ScanRoutine()); } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Dynamic Food Scanner (start). Reason: {arg}"); } } [HarmonyPatch(typeof(Player), "OnSpawned")] [HarmonyPostfix] public static void Player_OnSpawned_Postfix(Player __instance) { try { FoodMilestones.OnSpawned(__instance); } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Milestones (spawn). Reason: {arg}"); } try { if (!_hasScanned && !(__instance == null)) { __instance.StartCoroutine(ScanRoutine()); } } catch (Exception arg2) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Dynamic Food Scanner (spawn). Reason: {arg2}"); } } private static IEnumerator ScanRoutine() { if (_hasScanned) { yield break; } _hasScanned = true; List list = null; bool flag = false; try { FattyPlugin.Instance.Log.LogInfo("Scanning ObjectDB for custom food items..."); ObjectDB instance = ObjectDB.instance; if (instance == null || instance.m_items == null) { FattyPlugin.Instance.Log.LogInfo("Fatty: ObjectDB not populated yet; deferring food scan until player spawn."); _hasScanned = false; } else { CategoryManager.EnsureBaseCategories(); Integrations.MapHardcodedIntegrations(CategoryManager.Categories); list = new List(instance.m_items); flag = true; } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Dynamic Food Scanner (setup). Reason: {arg}"); _hasScanned = false; } if (!flag || list == null) { yield break; } bool categoryUpdated = false; int processed = 0; int foodFound = 0; foreach (GameObject item in list) { try { if (ProcessItem(item, ref foodFound)) { categoryUpdated = true; } } catch (Exception arg2) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Skipped a food item during scan. Reason: {arg2}"); } int num = processed + 1; processed = num; if (num % 100 == 0) { yield return null; } } bool needsReclassify = CategoryManager.NeedsReclassify; if (categoryUpdated || needsReclassify) { try { CategoryManager.Save(); } catch (Exception arg3) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Data Save. Reason: {arg3}"); } } CategoryManager.NeedsReclassify = false; bool value = ConfigManager.rebuildFoodData.Value; if (value) { ConfigManager.rebuildFoodData.Value = false; } string arg4 = ((categoryUpdated || needsReclassify) ? "data saved" : "no change"); string arg5 = (value ? ", rebuilt on request" : (needsReclassify ? ", migrated to new categories" : string.Empty)); FattyPlugin.Instance.Log.LogInfo($"Fatty food scan complete: {foodFound} food item(s) catalogued ({arg4}{arg5})."); } private static bool ProcessItem(GameObject item, ref int foodFound) { if (item == null) { return false; } ItemDrop component = item.GetComponent(); if (component == null || component.m_itemData == null || component.m_itemData.m_shared == null) { return false; } ItemDrop.ItemData.SharedData shared = component.m_itemData.m_shared; bool num = shared.m_food > 0f || shared.m_foodStamina > 0f || shared.m_foodEitr > 0f; bool flag = shared.m_itemType == ItemDrop.ItemData.ItemType.Consumable; if (!num && !flag) { return false; } string prefabName = item.name; if (string.IsNullOrEmpty(prefabName)) { return false; } foodFound++; bool result = false; string text = CategoryManager.GetCategoryForPrefab(prefabName); if (text == "Unknown" || CategoryManager.NeedsReclassify) { HashSet hashSet = new HashSet(); AddWords(shared.m_name, hashSet); AddWords(prefabName, hashSet); bool flag2 = IsDrink(shared, hashSet) || CategoryManager.IsDrinkPrefab(prefabName); string text2 = (CategoryManager.IsHardcoded(prefabName) ? text : ClassifyDiet(shared, hashSet, flag2, prefabName)); CategoryManager.Classify(prefabName, text2, flag2); text = text2; if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo("Categorized '" + prefabName + "' as " + text2 + (flag2 ? " (drink)" : "")); } } CategoryManager.FoodData foodData = CategoryManager.AllFoodData.Find((CategoryManager.FoodData f) => f != null && f.PrefabName == prefabName); if (foodData == null) { foodData = new CategoryManager.FoodData { PrefabName = prefabName }; CategoryManager.AllFoodData.Add(foodData); result = true; } else if (foodData.Category != text || foodData.IsDrink != CategoryManager.IsDrinkPrefab(prefabName)) { result = true; } CategoryManager.MapInGameName(shared.m_name, prefabName); foodData.InGameName = shared.m_name; foodData.Health = shared.m_food; foodData.Stamina = shared.m_foodStamina; foodData.Eitr = shared.m_foodEitr; foodData.Regen = shared.m_foodRegen; foodData.BurnTime = shared.m_foodBurnTime; foodData.Category = text; foodData.IsDrink = CategoryManager.IsDrinkPrefab(prefabName); return result; } private static string ClassifyDiet(ItemDrop.ItemData.SharedData shared, HashSet words, bool isDrink, string prefabName) { if (shared.m_foodEitr > 0f && (shared.m_foodEitr > shared.m_food || shared.m_foodEitr > shared.m_foodStamina)) { return "Eitr"; } if (isDrink) { string text = ClassifyDrinkByEffect(shared, prefabName); if (text != null) { return text; } } if (words.Overlaps(MeatWords)) { return "Meat"; } if (words.Overlaps(FishWords)) { return "Fish"; } if (words.Overlaps(SweetWords)) { return "Sweet"; } if (words.Overlaps(VegetableWords)) { return "Vegetable"; } if (isDrink) { return "Meat"; } return "Unknown"; } private static string ClassifyDrinkByEffect(ItemDrop.ItemData.SharedData shared, string prefabName) { if (prefabName != null && KnownDrinkDiets.TryGetValue(prefabName, out var value)) { return value; } SE_Stats sE_Stats = shared.m_consumeStatusEffect as SE_Stats; if (sE_Stats == null) { return null; } if (sE_Stats.m_eitrOverTime > 0f || sE_Stats.m_eitrUpFront > 0f || sE_Stats.m_eitrRegenMultiplier > 1f) { return "Eitr"; } if (sE_Stats.m_swimSpeedModifier > 0f || sE_Stats.m_swimStaminaUseModifier < 0f) { return "Fish"; } if (sE_Stats.m_staminaOverTime > 0f || sE_Stats.m_staminaUpFront > 0f || sE_Stats.m_staminaRegenMultiplier > 1f || sE_Stats.m_runStaminaDrainModifier < 0f || sE_Stats.m_runStaminaUseModifier < 0f || sE_Stats.m_jumpStaminaUseModifier < 0f || sE_Stats.m_speedModifier > 0f) { return "Sweet"; } if (sE_Stats.m_healthOverTime > 0f || sE_Stats.m_healthUpFront > 0f || sE_Stats.m_healthRegenMultiplier > 1f || sE_Stats.m_damageModifier > 1f || sE_Stats.m_addArmor > 0f || sE_Stats.m_armorMultiplier > 1f || sE_Stats.m_staggerModifier < 0f || sE_Stats.m_addMaxCarryWeight > 0f) { return "Meat"; } return null; } private static bool IsDrink(ItemDrop.ItemData.SharedData shared, HashSet words) { return words.Overlaps(DrinkWords); } } } // ---- plugins/Fatty.dll :: Fatty.Scanners.Integrations ---- namespace Fatty.Scanners { public static class Integrations { public static bool IsValheimCuisineInstalled => Chainloader.PluginInfos.ContainsKey("XutzBR.ValheimCuisine"); public static void MapHardcodedIntegrations(Dictionary> categories) { if (IsValheimCuisineInstalled) { FattyPlugin.Instance.Log.LogInfo("Valheim Cuisine detected! Injecting hardcoded categories..."); CategoryManager.AddHardcodedCategory("Meat", "VC_NeckSoup"); CategoryManager.AddHardcodedCategory("Meat", "VC_ForestSkewer"); CategoryManager.AddHardcodedCategory("Meat", "VC_TrollStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_BoarStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_NeckStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_SmokedBearStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_FensalirSkause"); CategoryManager.AddHardcodedCategory("Meat", "VC_BloodyBroth"); CategoryManager.AddHardcodedCategory("Meat", "VC_TrollJerky"); CategoryManager.AddHardcodedCategory("Meat", "VC_ColdCuredNeck"); CategoryManager.AddHardcodedCategory("Meat", "VC_YdalirSkause"); CategoryManager.AddHardcodedCategory("Meat", "VC_UlvTailStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_HatchlingSoup"); CategoryManager.AddHardcodedCategory("Meat", "VC_LoxJerky"); CategoryManager.AddHardcodedCategory("Meat", "VC_NeckBarleyStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_AlfablotStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_JarnbjornStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_HareJerky"); CategoryManager.AddHardcodedCategory("Meat", "VC_TrollAspic"); CategoryManager.AddHardcodedCategory("Meat", "VC_ChickenStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_HareStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_DvergrMageBroth"); CategoryManager.AddHardcodedCategory("Meat", "VC_MorgenJerky"); CategoryManager.AddHardcodedCategory("Meat", "VC_Biksemad"); CategoryManager.AddHardcodedCategory("Meat", "VC_BonemawStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_VarangianStew"); CategoryManager.AddHardcodedCategory("Meat", "VC_BonemawChowder"); CategoryManager.AddHardcodedCategory("Meat", "VC_SerpentChowder"); CategoryManager.AddHardcodedCategory("Meat", "VC_LyngbakrChowder"); CategoryManager.AddHardcodedCategory("Meat", "VC_RegalPorridge"); CategoryManager.AddHardcodedCategory("Meat", "VC_FolkvangrNattmal"); CategoryManager.AddHardcodedCategory("Meat", "VC_WolfWraps"); CategoryManager.AddHardcodedCategory("Meat", "VC_DvergrDagmal"); CategoryManager.AddHardcodedCategory("Meat", "VC_Bacon"); CategoryManager.AddHardcodedCategory("Meat", "VC_SmokedBoarHam"); CategoryManager.AddHardcodedCategory("Meat", "VC_SmokedDrakeHeart"); CategoryManager.AddHardcodedCategory("Meat", "VC_BoarHam"); CategoryManager.AddHardcodedCategory("Meat", "VC_BonemawHakarl"); CategoryManager.AddHardcodedCategory("Meat", "VC_Hakarl"); CategoryManager.AddHardcodedCategory("Meat", "VC_UncuredWolfSalami"); CategoryManager.AddHardcodedCategory("Meat", "VC_PickledEntrails"); CategoryManager.AddHardcodedCategory("Meat", "VC_UnfermentedPickledEntrails"); CategoryManager.AddHardcodedCategory("Fish", "VC_PikeFillets"); CategoryManager.AddHardcodedCategory("Fish", "VC_PerchSteak"); CategoryManager.AddHardcodedCategory("Fish", "VC_BloodmoonStew"); CategoryManager.AddHardcodedCategory("Fish", "VC_GrouperPottage"); CategoryManager.AddHardcodedCategory("Fish", "VC_CrispyPuffers"); CategoryManager.AddHardcodedCategory("Fish", "VC_DeepNorthRagout"); CategoryManager.AddHardcodedCategory("Fish", "VC_MunarvagrSkause"); CategoryManager.AddHardcodedCategory("Fish", "VC_JomsvikingStew"); CategoryManager.AddHardcodedCategory("Fish", "VC_Gravlaks"); CategoryManager.AddHardcodedCategory("Fish", "VC_HighlanderDagmal"); CategoryManager.AddHardcodedCategory("Fish", "VC_Stockfish"); CategoryManager.AddHardcodedCategory("Fish", "VC_PickledHerring"); CategoryManager.AddHardcodedCategory("Fish", "VC_UnfermentedPickledHerring"); CategoryManager.AddHardcodedCategory("Fish", "VC_UnfermentedRakfisk"); CategoryManager.AddHardcodedCategory("Fish", "VC_UnfermentedLutefisk"); CategoryManager.AddHardcodedCategory("Sweet", "VC_EctoplasmSoup"); CategoryManager.AddHardcodedCategory("Sweet", "VC_Lefse"); CategoryManager.AddHardcodedCategory("Sweet", "VC_Multekrem"); CategoryManager.AddHardcodedCategory("Sweet", "VC_CreamBastarde"); CategoryManager.AddHardcodedCategory("Sweet", "VC_Blodplattar"); CategoryManager.AddHardcodedCategory("Sweet", "VC_FruitBowl"); CategoryManager.AddHardcodedCategory("Sweet", "VC_HerbalRemedy"); CategoryManager.MarkHardcodedDrink("VC_PerryBroth"); CategoryManager.MarkHardcodedDrink("VC_GlowingStew"); CategoryManager.MarkHardcodedDrink("VC_PerryPorridge"); CategoryManager.MarkHardcodedDrink("VC_MistyFondue"); CategoryManager.MarkHardcodedDrink("VC_Bragafull"); CategoryManager.MarkHardcodedDrink("VC_PeasantDagmal"); CategoryManager.MarkHardcodedDrink("VC_VarangianDagmal"); CategoryManager.MarkHardcodedDrink("VC_SpicedPerry"); CategoryManager.MarkHardcodedDrink("VC_ShroomBeer"); CategoryManager.MarkHardcodedDrink("VC_UnfermentedSpicedPerry"); CategoryManager.MarkHardcodedDrink("VC_Blaand"); CategoryManager.MarkHardcodedDrink("VC_FulingBeer"); CategoryManager.MarkHardcodedDrink("VC_FulingBlaand"); CategoryManager.MarkHardcodedDrink("VC_Booze"); CategoryManager.MarkHardcodedDrink("VC_Tunnelrumbler"); CategoryManager.MarkHardcodedDrink("VC_Nogginfog"); } } } } // ---- plugins/Fatty.dll :: Fatty.Regen.RegenScaling ---- namespace Fatty.Regen { public static class RegenScaling { private class Baseline { public bool Has; public float StaminaBase; public float EitrBase; public float StaminaWritten; public float EitrWritten; } private static readonly ConditionalWeakTable _state = new ConditionalWeakTable(); public static void Apply(Player player, float stamina, float refStamina, float eitr, float refEitr) { if (player == null) { return; } if (!ConfigManager.scaleRegenWithPool.Value) { Restore(player); return; } Baseline orCreateValue = _state.GetOrCreateValue(player); if (!orCreateValue.Has || !Mathf.Approximately(player.m_staminaRegen, orCreateValue.StaminaWritten)) { orCreateValue.StaminaBase = player.m_staminaRegen; } if (!orCreateValue.Has || !Mathf.Approximately(player.m_eiterRegen, orCreateValue.EitrWritten)) { orCreateValue.EitrBase = player.m_eiterRegen; } orCreateValue.Has = true; float cap = Mathf.Max(1f, ConfigManager.maxRegenScale.Value); player.m_staminaRegen = orCreateValue.StaminaBase * Scale(stamina, refStamina, cap); player.m_eiterRegen = orCreateValue.EitrBase * Scale(eitr, refEitr, cap); orCreateValue.StaminaWritten = player.m_staminaRegen; orCreateValue.EitrWritten = player.m_eiterRegen; } private static float Scale(float pool, float reference, float cap) { if (reference <= 0f || pool <= 0f) { return 1f; } float num = pool / reference; if (float.IsNaN(num) || float.IsInfinity(num)) { return 1f; } return Mathf.Clamp(num, 1f, cap); } private static void Restore(Player player) { if (_state.TryGetValue(player, out var value) && value.Has) { if (Mathf.Approximately(player.m_staminaRegen, value.StaminaWritten)) { player.m_staminaRegen = value.StaminaBase; } if (Mathf.Approximately(player.m_eiterRegen, value.EitrWritten)) { player.m_eiterRegen = value.EitrBase; } _state.Remove(player); } } } } // ---- plugins/Fatty.dll :: Fatty.Progression.FoodLifetimeLog ---- namespace Fatty.Progression { public static class FoodLifetimeLog { private const string CountsKey = "Fatty.FoodLifetimeCounts"; public static Dictionary Load(Player player) { Dictionary dictionary = new Dictionary(); if (player == null || player.m_customData == null) { return dictionary; } if (!player.m_customData.TryGetValue("Fatty.FoodLifetimeCounts", out var value) || string.IsNullOrEmpty(value)) { return dictionary; } string[] array = value.Split(new char[1] { ',' }); foreach (string text in array) { int num = text.IndexOf('='); if (num > 0 && num != text.Length - 1) { string key = text.Substring(0, num); if (int.TryParse(text.Substring(num + 1), out var result) && result > 0) { dictionary[key] = result; } } } return dictionary; } public static void Save(Player player, Dictionary counts) { if (player == null || player.m_customData == null || counts == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair count in counts) { if (count.Value > 0) { if (stringBuilder.Length > 0) { stringBuilder.Append(','); } stringBuilder.Append(count.Key).Append('=').Append(count.Value); } } if (stringBuilder.Length == 0) { player.m_customData.Remove("Fatty.FoodLifetimeCounts"); } else { player.m_customData["Fatty.FoodLifetimeCounts"] = stringBuilder.ToString(); } } public static void RecordEaten(Player player, string prefabName) { if (!(player == null) && !string.IsNullOrEmpty(prefabName)) { Dictionary dictionary = Load(player); dictionary.TryGetValue(prefabName, out var value); dictionary[prefabName] = value + 1; Save(player, dictionary); } } } } // ---- plugins/Fatty.dll :: Fatty.Progression.FoodMilestones ---- namespace Fatty.Progression { public static class FoodMilestones { private const string CategoryMeat = "Meat"; private const string CategorySweet = "Sweet"; private const string CategoryEitr = "Eitr"; private const string CategoryFish = "Fish"; private const string CategoryVegetable = "Vegetable"; private static float _pristineBaseHp = 25f; private static float _pristineBaseStamina = 75f; private static float _pristineMaxCarryWeight = 300f; private static bool _capturedBase; public static void OnSpawned(Player player) { if (!(player == null)) { CaptureBaseIfNeeded(player); Apply(player); } } public static void RecordEaten(Player player, string category) { if (player == null || !ConfigManager.enableMilestones.Value || string.IsNullOrEmpty(category)) { return; } Dictionary dictionary = MilestoneStore.Load(player); int num = MilestoneStore.Get(dictionary, category); int num2 = SumAll(dictionary); dictionary[category] = num + 1; MilestoneStore.Save(player, dictionary); Apply(player); if (MilestoneStore.EarnsMilestones(category)) { int num3 = TiersEarned(num); int num4 = TiersEarned(num + 1); if (num4 > num3) { AnnounceCategoryTier(player, category, num4); } } int num5 = FatnessTiersEarned(num2); int num6 = FatnessTiersEarned(num2 + 1); if (num6 > num5) { AnnounceFatnessTier(player, num6); } } public static void Apply(Player player) { if (!(player == null)) { CaptureBaseIfNeeded(player); bool value = ConfigManager.enableMilestones.Value; Dictionary counts = MilestoneStore.Load(player); player.m_baseHP = _pristineBaseHp + (value ? FatnessBonus(counts) : 0f); player.m_baseStamina = _pristineBaseStamina + (value ? StaminaBonus(counts) : 0f); player.m_maxCarryWeight = _pristineMaxCarryWeight + (value ? CarryWeightBonus(counts) : 0f); int num = (value ? TiersEarned(MilestoneStore.Get(counts, "Meat")) : 0); int num2 = (value ? TiersEarned(MilestoneStore.Get(counts, "Sweet")) : 0); int num3 = (value ? TiersEarned(MilestoneStore.Get(counts, "Eitr")) : 0); int fishTiers = (value ? TiersEarned(MilestoneStore.Get(counts, "Fish")) : 0); int vegTiers = (value ? TiersEarned(MilestoneStore.Get(counts, "Vegetable")) : 0); int num4 = (value ? FatnessTiersEarned(SumAll(counts)) : 0); MilestonePerks.Sync(player, fishTiers, vegTiers, num >= 2, num2 >= 2, num3 >= 2, num4 >= 2); } } public static float GetEitrBonus(Player player) { if (player == null || !ConfigManager.enableMilestones.Value) { return 0f; } return EitrBonus(MilestoneStore.Load(player)); } private static void CaptureBaseIfNeeded(Player player) { if (!_capturedBase) { _pristineBaseHp = player.m_baseHP; _pristineBaseStamina = player.m_baseStamina; _pristineMaxCarryWeight = player.m_maxCarryWeight; _capturedBase = true; } } private static void AnnounceCategoryTier(Player player, string category, int tier) { string arg = DescribeTierAward(category, tier); player.Message(MessageHud.MessageType.Center, $"You're getting fat! {category} {tier} - {arg}"); if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo($"[Fatty] Milestone: {category} tier {tier} -> {arg}."); } } private static void AnnounceFatnessTier(Player player, int tier) { string arg = DescribeTierAward("Fatness", tier); player.Message(MessageHud.MessageType.Center, $"You're getting fat! Fatness {tier} - {arg}"); if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo($"[Fatty] Milestone: Fatness tier {tier} -> {arg}."); } } internal static string DescribeTierAward(string category, int tier) { switch (category) { case "Meat": if (tier != 2) { return $"+{CarryWeightForTier(tier):0.#} carry weight"; } return $"+{ConfigManager.milestoneTier2MeatSkill.Value:0.#} Sword & Axe skill"; case "Sweet": if (tier != 2) { return $"+{StaminaForTier(tier):0.#} stamina"; } return $"+{ConfigManager.milestoneTier2StaminaRegen.Value * 100f:0.#}% stamina regen"; case "Eitr": if (tier != 2) { return $"+{EitrForTier(tier):0.#} eitr"; } return $"+{ConfigManager.milestoneTier2EitrRegen.Value * 100f:0.#}% eitr regen"; case "Fish": if (tier != 2) { return $"+{SwimSpeedForTier(tier) * 100f:0.#}% swim speed"; } return $"-{ConfigManager.milestoneTier2FishSwimStamina.Value * 100f:0.#}% swim stamina cost"; case "Vegetable": return $"+{(RegenForTier(tier) - 1f) * 100f:0.#}% regen"; case "Fatness": if (tier != 2) { return $"+{HpForTier(tier):0.#} max health"; } return $"+{ConfigManager.milestoneTier2HpRegen.Value * 100f:0.#}% health regen"; default: return ""; } } public static float CarryWeightBonus(Dictionary counts) { int num = TiersEarned(MilestoneStore.Get(counts, "Meat")); return Mathf.Min(((num >= 1) ? CarryWeightForTier(1) : 0f) + ((num >= 3) ? CarryWeightForTier(3) : 0f), Mathf.Max(0f, ConfigManager.milestoneCarryWeightCap.Value)); } public static float StaminaBonus(Dictionary counts) { int num = TiersEarned(MilestoneStore.Get(counts, "Sweet")); return Mathf.Min(((num >= 1) ? StaminaForTier(1) : 0f) + ((num >= 3) ? StaminaForTier(3) : 0f), Mathf.Max(0f, ConfigManager.milestoneStaminaCap.Value)); } public static float EitrBonus(Dictionary counts) { int num = TiersEarned(MilestoneStore.Get(counts, "Eitr")); return Mathf.Min(((num >= 1) ? EitrForTier(1) : 0f) + ((num >= 3) ? EitrForTier(3) : 0f), Mathf.Max(0f, ConfigManager.milestoneEitrCap.Value)); } public static float FatnessBonus(Dictionary counts) { int num = FatnessTiersEarned(SumAll(counts)); return Mathf.Min(((num >= 1) ? HpForTier(1) : 0f) + ((num >= 3) ? HpForTier(3) : 0f), Mathf.Max(0f, ConfigManager.milestoneTotalCap.Value)); } private static int SumAll(Dictionary counts) { int num = 0; foreach (KeyValuePair count in counts) { num += count.Value; } return num; } internal static int TiersEarned(int eaten) { int a = ConfigManager.milestoneTier1Count.Value; int b = ConfigManager.milestoneTier2Count.Value; int b2 = ConfigManager.milestoneTier3Count.Value; SortAscending(ref a, ref b); SortAscending(ref b, ref b2); SortAscending(ref a, ref b); if (eaten >= b2) { return 3; } if (eaten >= b) { return 2; } if (eaten >= a) { return 1; } return 0; } internal static int FatnessTiersEarned(int totalEaten) { int a = ConfigManager.fatnessTier1Count.Value; int b = ConfigManager.fatnessTier2Count.Value; int b2 = ConfigManager.fatnessTier3Count.Value; SortAscending(ref a, ref b); SortAscending(ref b, ref b2); SortAscending(ref a, ref b); if (totalEaten >= b2) { return 3; } if (totalEaten >= b) { return 2; } if (totalEaten >= a) { return 1; } return 0; } private static void SortAscending(ref int a, ref int b) { if (a > b) { int num = a; a = b; b = num; } } internal static float HpForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1Hp.Value, 3 => ConfigManager.milestoneTier3Hp.Value, _ => 0f, }; } internal static float CarryWeightForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1CarryWeight.Value, 3 => ConfigManager.milestoneTier3CarryWeight.Value, _ => 0f, }; } internal static float StaminaForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1Stamina.Value, 3 => ConfigManager.milestoneTier3Stamina.Value, _ => 0f, }; } internal static float EitrForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1Eitr.Value, 3 => ConfigManager.milestoneTier3Eitr.Value, _ => 0f, }; } internal static float SwimSpeedForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1SwimSpeed.Value, 3 => ConfigManager.milestoneTier3SwimSpeed.Value, _ => 0f, }; } internal static float RegenForTier(int tier) { return tier switch { 1 => ConfigManager.milestoneTier1Regen.Value, 2 => ConfigManager.milestoneTier2Regen.Value, 3 => ConfigManager.milestoneTier3Regen.Value, _ => 1f, }; } public static string Describe(Player player) { if (player == null) { return "no player"; } Dictionary counts = MilestoneStore.Load(player); List list = new List(); AppendIfEaten(list, counts, "Meat", "carry weight", CarryWeightBonus(counts)); AppendIfEaten(list, counts, "Sweet", "stamina", StaminaBonus(counts)); AppendIfEaten(list, counts, "Eitr", "eitr", EitrBonus(counts)); int num = MilestoneStore.Get(counts, "Fish"); if (num > 0) { int num2 = TiersEarned(num); string text = ((num2 >= 2) ? ", swim stamina reduced" : ""); list.Add(string.Format("{0}={1}({2} tier(s)) -> swim speed{3}", "Fish", num, num2, text)); } int num3 = MilestoneStore.Get(counts, "Vegetable"); if (num3 > 0) { list.Add(string.Format("{0}={1}({2} tier(s)) -> regen", "Vegetable", num3, TiersEarned(num3))); } int num4 = SumAll(counts); if (num4 > 0) { int num5 = FatnessTiersEarned(num4); string text2 = ((num5 >= 2) ? ", health regen boosted" : ""); list.Add($"Fatness={num4}({num5} tier(s)) -> +{FatnessBonus(counts):0.#} HP{text2}"); } if (list.Count != 0) { return string.Join(", ", list); } return "nothing eaten yet"; } private static void AppendIfEaten(List parts, Dictionary counts, string category, string statName, float bonus) { int num = MilestoneStore.Get(counts, category); if (num > 0) { int num2 = TiersEarned(num); string text = ((num2 >= 2) ? (", tier2 alt: " + DescribeTierAward(category, 2)) : ""); parts.Add($"{category}={num}({num2} tier(s)) -> +{bonus:0.#} {statName}{text}"); } } } } // ---- plugins/Fatty.dll :: Fatty.Progression.MilestonePerks ---- namespace Fatty.Progression { public static class MilestonePerks { private static bool _built; private static SE_Stats _fish1; private static SE_Stats _fish2; private static SE_Stats _fish3; private static SE_Stats _veg1; private static SE_Stats _veg2; private static SE_Stats _veg3; private static SE_Stats _meatSkill; private static SE_Stats _staminaRegen; private static SE_Stats _eitrRegen; private static SE_Stats _hpRegen; public static void Register(ObjectDB odb) { if (!(odb == null) && odb.m_StatusEffects != null) { if (!_built) { Build(); } ApplyConfigValues(); AddIfMissing(odb, _fish1); AddIfMissing(odb, _fish2); AddIfMissing(odb, _fish3); AddIfMissing(odb, _veg1); AddIfMissing(odb, _veg2); AddIfMissing(odb, _veg3); AddIfMissing(odb, _meatSkill); AddIfMissing(odb, _staminaRegen); AddIfMissing(odb, _eitrRegen); AddIfMissing(odb, _hpRegen); } } private static void AddIfMissing(ObjectDB odb, StatusEffect se) { if (!(se == null) && odb.GetStatusEffect(se.NameHash()) == null) { odb.m_StatusEffects.Add(se); } } private static SE_Stats CreateBase(string internalName, string displayName, string tooltip) { SE_Stats sE_Stats = ScriptableObject.CreateInstance(); sE_Stats.name = internalName; sE_Stats.m_name = displayName; sE_Stats.m_tooltip = tooltip; sE_Stats.m_startMessageType = MessageHud.MessageType.TopLeft; sE_Stats.m_ttl = 0f; sE_Stats.m_healthRegenMultiplier = 1f; sE_Stats.m_staminaRegenMultiplier = 1f; sE_Stats.m_eitrRegenMultiplier = 1f; sE_Stats.m_damageModifier = 1f; return sE_Stats; } private static void Build() { _fish1 = CreateBase("SE_Fatty_MilestoneFish1", "Sea Legs I", "A lifetime of fish.\nSwim speed permanently improved."); _fish1.m_icon = SynergyIcons.Resolve("MilestoneFish", SynergyIcons.Fisherman); _fish2 = CreateBase("SE_Fatty_MilestoneFish2", "Sea Legs II", "Practically part fish by now.\nSwim stamina cost permanently reduced."); _fish2.m_icon = SynergyIcons.Resolve("MilestoneFish", SynergyIcons.Fisherman); _fish3 = CreateBase("SE_Fatty_MilestoneFish3", "Sea Legs III", "The sea knows your name.\nSwim speed permanently improved further."); _fish3.m_icon = SynergyIcons.Resolve("MilestoneFish", SynergyIcons.Fisherman); _veg1 = CreateBase("SE_Fatty_MilestoneVeg1", "Well Nourished I", "A lifetime of greens.\nHealth, stamina and eitr regeneration permanently improved."); _veg1.m_icon = SynergyIcons.Resolve("MilestoneVeg", SynergyIcons.BalancedDiet); _veg2 = CreateBase("SE_Fatty_MilestoneVeg2", "Well Nourished II", "Your diet is the envy of the village.\nRegeneration permanently improved further."); _veg2.m_icon = SynergyIcons.Resolve("MilestoneVeg", SynergyIcons.BalancedDiet); _veg3 = CreateBase("SE_Fatty_MilestoneVeg3", "Well Nourished III", "A truly balanced life.\nRegeneration permanently improved further still."); _veg3.m_icon = SynergyIcons.Resolve("MilestoneVeg", SynergyIcons.BalancedDiet); _meatSkill = CreateBase("SE_Fatty_MilestoneMeatSkill", "Warrior's Build", "A lifetime of meat.\nSword and axe skill permanently improved."); _meatSkill.m_icon = SynergyIcons.Resolve("MilestoneMeat", SynergyIcons.WarriorsDraught); _staminaRegen = CreateBase("SE_Fatty_MilestoneStaminaRegen", "Boundless Energy", "A lifetime of sweets.\nStamina regeneration permanently improved."); _staminaRegen.m_icon = SynergyIcons.Resolve("MilestoneSweet", SynergyIcons.RevelersDraught); _eitrRegen = CreateBase("SE_Fatty_MilestoneEitrRegen", "Arcane Constitution", "A lifetime of eitr food.\nEitr regeneration permanently improved."); _eitrRegen.m_icon = SynergyIcons.Resolve("MilestoneEitr", SynergyIcons.ArcaneDraught); _hpRegen = CreateBase("SE_Fatty_MilestoneHpRegen", "Iron Constitution", "A lifetime of eating well.\nHealth regeneration permanently improved."); _hpRegen.m_icon = SynergyIcons.Resolve("MilestoneFatness", SynergyIcons.BalancedDiet); _built = true; } private static void ApplyConfigValues() { if (_fish1 != null) { _fish1.m_swimSpeedModifier = Mathf.Max(0f, ConfigManager.milestoneTier1SwimSpeed.Value); } if (_fish2 != null) { _fish2.m_swimStaminaUseModifier = 0f - Mathf.Clamp01(ConfigManager.milestoneTier2FishSwimStamina.Value); } if (_fish3 != null) { _fish3.m_swimSpeedModifier = Mathf.Max(0f, ConfigManager.milestoneTier3SwimSpeed.Value); } if (_veg1 != null) { SetRegen(_veg1, ConfigManager.milestoneTier1Regen.Value); } if (_veg2 != null) { SetRegen(_veg2, ConfigManager.milestoneTier2Regen.Value); } if (_veg3 != null) { SetRegen(_veg3, ConfigManager.milestoneTier3Regen.Value); } if (_meatSkill != null) { float value = ConfigManager.milestoneTier2MeatSkill.Value; _meatSkill.m_skillLevel = Skills.SkillType.Swords; _meatSkill.m_skillLevelModifier = value; _meatSkill.m_skillLevel2 = Skills.SkillType.Axes; _meatSkill.m_skillLevelModifier2 = value; } if (_staminaRegen != null) { _staminaRegen.m_staminaRegenMultiplier = Mathf.Max(0.01f, 1f + ConfigManager.milestoneTier2StaminaRegen.Value); } if (_eitrRegen != null) { _eitrRegen.m_eitrRegenMultiplier = Mathf.Max(0.01f, 1f + ConfigManager.milestoneTier2EitrRegen.Value); } if (_hpRegen != null) { _hpRegen.m_healthRegenMultiplier = Mathf.Max(0.01f, 1f + ConfigManager.milestoneTier2HpRegen.Value); } } private static void SetRegen(SE_Stats se, float multiplier) { se.m_eitrRegenMultiplier = (se.m_staminaRegenMultiplier = (se.m_healthRegenMultiplier = Mathf.Max(0.01f, multiplier))); } public static void Sync(Player player, int fishTiers, int vegTiers, bool meatSkillActive, bool staminaRegenActive, bool eitrRegenActive, bool hpRegenActive) { SEMan sEMan = player?.GetSEMan(); if (sEMan != null) { ApplyEffect(sEMan, _fish1, fishTiers >= 1); ApplyEffect(sEMan, _fish2, fishTiers >= 2); ApplyEffect(sEMan, _fish3, fishTiers >= 3); ApplyEffect(sEMan, _veg1, vegTiers >= 1); ApplyEffect(sEMan, _veg2, vegTiers >= 2); ApplyEffect(sEMan, _veg3, vegTiers >= 3); ApplyEffect(sEMan, _meatSkill, meatSkillActive); ApplyEffect(sEMan, _staminaRegen, staminaRegenActive); ApplyEffect(sEMan, _eitrRegen, eitrRegenActive); ApplyEffect(sEMan, _hpRegen, hpRegenActive); } } private static void ApplyEffect(SEMan seman, SE_Stats se, bool qualified) { if (!(se == null)) { int nameHash = se.NameHash(); bool flag = seman.HaveStatusEffect(nameHash); if (qualified && !flag) { seman.AddStatusEffect(nameHash, resetTime: true); } else if (!qualified && flag) { seman.RemoveStatusEffect(nameHash); } } } } } // ---- plugins/Fatty.dll :: Fatty.Progression.MilestoneStore ---- namespace Fatty.Progression { public static class MilestoneStore { private const string CountsKey = "Fatty.FoodCounts"; public static Dictionary Load(Player player) { Dictionary dictionary = new Dictionary(); if (player == null || player.m_customData == null) { return dictionary; } if (!player.m_customData.TryGetValue("Fatty.FoodCounts", out var value) || string.IsNullOrEmpty(value)) { return dictionary; } string[] array = value.Split(new char[1] { ',' }); foreach (string text in array) { int num = text.IndexOf('='); if (num > 0 && num != text.Length - 1) { string key = text.Substring(0, num); if (int.TryParse(text.Substring(num + 1), out var result) && result > 0) { dictionary[key] = result; } } } return dictionary; } public static void Save(Player player, Dictionary counts) { if (player == null || player.m_customData == null || counts == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair count in counts) { if (count.Value > 0) { if (stringBuilder.Length > 0) { stringBuilder.Append(','); } stringBuilder.Append(count.Key).Append('=').Append(count.Value); } } if (stringBuilder.Length == 0) { player.m_customData.Remove("Fatty.FoodCounts"); } else { player.m_customData["Fatty.FoodCounts"] = stringBuilder.ToString(); } } public static int Get(Dictionary counts, string category) { if (counts == null || string.IsNullOrEmpty(category)) { return 0; } if (!counts.TryGetValue(category, out var value)) { return 0; } return value; } public static bool EarnsMilestones(string category) { if (!string.IsNullOrEmpty(category)) { return category != "Unknown"; } return false; } } } // ---- plugins/Fatty.dll :: Fatty.Patches.FeastLedgerGui ---- namespace Fatty.Patches { internal static class FeastLedgerGui { private const int WindowId = 4880273; private const float ToggleButtonWidth = 28f; private const float ClosedWidth = 110f; private const float ClosedHeight = 30f; private const float GripSize = 16f; private const float MinWidth = 340f; private const float MaxWidth = 900f; private const float MinHeight = 280f; private const float MaxHeight = 700f; private static bool _initialized; private static bool _open; private static Rect _rect; private static bool _resizing; private static Vector2 _resizeStartMouse; private static Vector2 _resizeStartSize; private static Vector2 _scroll; private static bool _milestonesOpen; private static bool _feastLogOpen; private static readonly Dictionary _stomachExpanded = new Dictionary(); private static bool _stylesReady; private static GUIStyle _windowStyle; private static GUIStyle _closedPillStyle; private static GUIStyle _toggleBtnStyle; private static GUIStyle _foldoutStyle; private static GUIStyle _smallLabelStyle; private static GUIStyle _gripStyle; private const float IconSize = 18f; private static readonly StringBuilder _sb = new StringBuilder(256); private static Dictionary _itemPrefabCache; internal static void Toggle() { EnsureInit(); _open = !_open; } internal static void Draw() { if (!(Player.m_localPlayer == null)) { EnsureInit(); EnsureStyles(); Rect clientRect = (_open ? _rect : new Rect(_rect.x, _rect.y, 110f, 30f)); Rect rect = GUI.Window(4880273, clientRect, DrawWindow, GUIContent.none, _windowStyle); _rect.x = rect.x; _rect.y = rect.y; if (_open) { _rect.width = rect.width; _rect.height = rect.height; HandleResize(); } } } private static void EnsureInit() { if (!_initialized) { _initialized = true; float width = Mathf.Clamp(ConfigManager.foodBreakdownPanelWidth.Value, 340f, 900f); float height = Mathf.Clamp(ConfigManager.foodBreakdownPanelHeight.Value, 280f, 700f); _rect = new Rect(ConfigManager.foodBreakdownPanelX.Value, ConfigManager.foodBreakdownPanelY.Value, width, height); } } private static void DrawWindow(int id) { if (!_open) { DrawClosedPill(); return; } float width = _rect.width; float height = _rect.height; Rect rect = new Rect(0f, 0f, width, height); GiltFrameTheme.DrawPanelFill(rect); GiltFrameTheme.DrawFrame(rect); GiltFrameTheme.DrawShadowed(new Rect(32f, 26f, width - 36f - 14f - 28f - 10f, 34f), "THE FEAST LEDGER", GiltFrameTheme.Title); GiltFrameTheme.DrawRule(new Rect(40f, 60f, width - 80f, 1f)); if (GUI.Button(new Rect(width - 18f - 28f - 6f, 28f, 28f, 34f), "▼", _toggleBtnStyle)) { _open = false; } GUI.DragWindow(new Rect(0f, 0f, width - 18f - 28f - 10f, 72f)); GUILayout.BeginArea(GiltFrameTheme.Body(rect)); _scroll = GUILayout.BeginScrollView(_scroll); DrawStomachSection(); GUILayout.Space(8f); DrawMilestoneSection(); GUILayout.Space(8f); DrawFeastLogSection(); GUILayout.Space(4f); GUILayout.EndScrollView(); GUILayout.EndArea(); } private static void DrawClosedPill() { Rect position = new Rect(0f, 0f, 110f, 30f); GUI.Box(position, GUIContent.none, _closedPillStyle); if (GUI.Button(position, "▲ FEAST", _closedPillStyle)) { _open = true; } GUI.DragWindow(position); } private static void DrawStomachSection() { GUILayout.Label("Stomach", GiltFrameTheme.Header); if (HudPatches.GroupCount == 0) { GUILayout.Label("An empty stomach is no way to live.", GiltFrameTheme.Note); return; } for (int i = 0; i < HudPatches.GroupCount; i++) { HudPatches.FoodGroup foodGroup = HudPatches.Groups[i]; if (foodGroup?.Rep == null || foodGroup.Members.Count == 0) { continue; } string text = FoodBreakdownUI.ResolveDisplayName(foodGroup.Rep); TryGetIcon(foodGroup.Rep, out var tex, out var uv); if (foodGroup.Count <= 1) { PlayerPatches.FoodContribution c = foodGroup.Members[0]; GUILayout.BeginHorizontal(); DrawIcon(tex, uv); GUILayout.Label(FormatMemberLine(text + " - ", c), GiltFrameTheme.Value); GUILayout.EndHorizontal(); continue; } string key = PlayerPatches.ResolvePrefabName(foodGroup.Rep) ?? text; bool value; bool flag = _stomachExpanded.TryGetValue(key, out value) && value; _sb.Clear(); _sb.Append(flag ? "▼ " : "▶ ").Append(text).Append(" x") .Append(foodGroup.Count) .Append(" - empties in ") .Append(FoodBreakdownUI.FormatTime(foodGroup.ActiveTime)) .Append(", ") .Append(FoodBreakdownUI.FormatTime(foodGroup.TotalTime)) .Append(" banked"); GUILayout.BeginHorizontal(); DrawIcon(tex, uv); if (GUILayout.Button(_sb.ToString(), _foldoutStyle)) { _stomachExpanded[key] = !flag; } GUILayout.EndHorizontal(); if (flag) { for (int j = 0; j < foodGroup.Members.Count; j++) { PlayerPatches.FoodContribution c2 = foodGroup.Members[j]; GUILayout.Label(FormatMemberLine(" " + (j + 1) + ". ", c2, c2.StackIndex, c2.Multiplier), _smallLabelStyle); } } } } private static bool TryGetIcon(Player.Food food, out Texture2D tex, out Rect uv) { tex = null; uv = default(Rect); Sprite sprite = food?.m_item?.GetIcon(); if (sprite == null || sprite.texture == null) { return false; } tex = sprite.texture; Rect rect = sprite.rect; uv = new Rect(rect.x / (float)tex.width, rect.y / (float)tex.height, rect.width / (float)tex.width, rect.height / (float)tex.height); return true; } private static void DrawIcon(Texture2D tex, Rect uv) { Rect rect = GUILayoutUtility.GetRect(18f, 18f, GUILayout.Width(18f), GUILayout.Height(18f)); if (tex != null) { GUI.DrawTextureWithTexCoords(rect, tex, uv); } } private static string FormatMemberLine(string prefix, PlayerPatches.FoodContribution c) { return FormatMemberLine(prefix, c, -1, 0f); } private static string FormatMemberLine(string prefix, PlayerPatches.FoodContribution c, int stackIndex, float multiplier) { _sb.Clear(); _sb.Append(prefix).Append(FoodBreakdownUI.FormatTime(c.Food.m_time)).Append(" left"); if (stackIndex >= 0) { _sb.Append(" (stack ").Append(stackIndex + 1).Append(" @ ") .Append(Mathf.RoundToInt(multiplier * 100f)) .Append("%)"); } if (c.Hp > 0.01f) { _sb.Append(" +").Append(c.Hp.ToString("0.#")).Append(" HP"); } if (c.Stamina > 0.01f) { _sb.Append(" +").Append(c.Stamina.ToString("0.#")).Append(" Sta"); } if (c.Eitr > 0.01f) { _sb.Append(" +").Append(c.Eitr.ToString("0.#")).Append(" Eitr"); } return _sb.ToString(); } private static void DrawMilestoneSection() { if (GUILayout.Button((_milestonesOpen ? "▼ " : "▶ ") + "Getting Fat", _foldoutStyle)) { _milestonesOpen = !_milestonesOpen; } if (!_milestonesOpen) { return; } Player localPlayer = Player.m_localPlayer; if (localPlayer == null) { GUILayout.Label("(no player)", GiltFrameTheme.Note); return; } Dictionary dictionary = MilestoneStore.Load(localPlayer); DrawAlternatingMilestoneLine("Meat", dictionary, "Meat"); DrawAlternatingMilestoneLine("Sweet", dictionary, "Sweet"); DrawAlternatingMilestoneLine("Eitr", dictionary, "Eitr"); DrawAlternatingMilestoneLine("Fish", dictionary, "Fish"); int num = MilestoneStore.Get(dictionary, "Vegetable"); int num2 = FoodMilestones.TiersEarned(num); _sb.Clear(); _sb.Append("Vegetable: ").Append(num).Append(" eaten"); if (num2 > 0) { float num3 = 1f; for (int i = 1; i <= num2; i++) { num3 *= FoodMilestones.RegenForTier(i); } _sb.Append(" - tier ").Append(num2).Append(", +") .Append(((num3 - 1f) * 100f).ToString("0.#")) .Append("% regen (health/stamina/eitr)"); } GUILayout.Label(_sb.ToString(), GiltFrameTheme.Value); GUILayout.Space(4f); int num4 = 0; foreach (KeyValuePair item in dictionary) { num4 += item.Value; } DrawAlternatingMilestoneLine("Fatness (all foods)", null, "Fatness", num4, FoodMilestones.FatnessTiersEarned(num4), bold: true); } private static void DrawAlternatingMilestoneLine(string label, Dictionary counts, string category, int totalEatenOverride = -1, int tiersOverride = -1, bool bold = false) { int num = ((totalEatenOverride >= 0) ? totalEatenOverride : MilestoneStore.Get(counts, category)); int num2 = ((tiersOverride >= 0) ? tiersOverride : FoodMilestones.TiersEarned(num)); _sb.Clear(); _sb.Append(label).Append(": ").Append(num) .Append(" eaten"); if (num2 > 0) { _sb.Append(" - tier ").Append(num2).Append(", "); for (int i = 1; i <= num2; i++) { if (i > 1) { _sb.Append(", "); } _sb.Append(FoodMilestones.DescribeTierAward(category, i)); } } GUILayout.Label(_sb.ToString(), bold ? GiltFrameTheme.Header : GiltFrameTheme.Value); } private static void DrawFeastLogSection() { if (GUILayout.Button((_feastLogOpen ? "▼ " : "▶ ") + "Lifetime Feast Log", _foldoutStyle)) { _feastLogOpen = !_feastLogOpen; } if (!_feastLogOpen) { return; } Player localPlayer = Player.m_localPlayer; if (localPlayer == null) { GUILayout.Label("(no player)", GiltFrameTheme.Note); return; } Dictionary dictionary = FoodLifetimeLog.Load(localPlayer); if (dictionary.Count == 0) { GUILayout.Label("Nothing eaten yet.", GiltFrameTheme.Note); return; } List> list = new List>(dictionary); list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); EnsureItemCache(); foreach (KeyValuePair item in list) { GUILayout.Label(ResolveLifetimeDisplayName(item.Key) + " x" + item.Value, _smallLabelStyle); } } private static string ResolveLifetimeDisplayName(string prefabName) { if (_itemPrefabCache != null && _itemPrefabCache.TryGetValue(prefabName, out var value) && value != null) { ItemDrop.ItemData itemData = value.GetComponent()?.m_itemData; if (itemData?.m_shared != null) { return FoodBreakdownUI.Localize(itemData.m_shared.m_name); } } return prefabName; } private static void EnsureItemCache() { if (_itemPrefabCache != null && _itemPrefabCache.Count > 0) { return; } ObjectDB instance = ObjectDB.instance; if (instance == null || instance.m_items == null) { return; } _itemPrefabCache = new Dictionary(); foreach (GameObject item in instance.m_items) { if (!(item == null)) { _itemPrefabCache[item.name] = item; } } } private static void HandleResize() { Rect position = new Rect(_rect.xMax - 18f - 16f, _rect.yMax - 18f - 16f, 16f, 16f); GUI.Box(position, GUIContent.none, _gripStyle); Event current = Event.current; if (current != null) { if (!_resizing && current.type == EventType.MouseDown && current.button == 0 && position.Contains(current.mousePosition)) { _resizing = true; _resizeStartMouse = current.mousePosition; _resizeStartSize = new Vector2(_rect.width, _rect.height); current.Use(); } else if (_resizing && current.type == EventType.MouseDrag && current.button == 0) { Vector2 vector = current.mousePosition - _resizeStartMouse; _rect.width = Mathf.Clamp(_resizeStartSize.x + vector.x, 340f, 900f); _rect.height = Mathf.Clamp(_resizeStartSize.y + vector.y, 280f, 700f); current.Use(); } else if (_resizing && (current.type == EventType.MouseUp || current.rawType == EventType.MouseUp)) { _resizing = false; ConfigManager.foodBreakdownPanelWidth.Value = _rect.width; ConfigManager.foodBreakdownPanelHeight.Value = _rect.height; current.Use(); } ConfigManager.foodBreakdownPanelX.Value = _rect.x; ConfigManager.foodBreakdownPanelY.Value = _rect.y; } } private static void EnsureStyles() { if (!_stylesReady) { _stylesReady = true; GiltFrameTheme.EnsureBuilt(); _windowStyle = new GUIStyle(); _closedPillStyle = new GUIStyle(GUI.skin.button) { fontSize = GiltFrameTheme.Header.fontSize, fontStyle = FontStyle.Bold, font = GiltFrameTheme.Header.font, alignment = TextAnchor.MiddleCenter }; _closedPillStyle.normal.textColor = GiltFrameTheme.Gold; _closedPillStyle.normal.background = MakeTex(new Color(0.05f, 0.045f, 0.03f, 0.95f)); _closedPillStyle.hover.textColor = GiltFrameTheme.GoldBright; _closedPillStyle.hover.background = _closedPillStyle.normal.background; _toggleBtnStyle = new GUIStyle(GUI.skin.button) { fontSize = GiltFrameTheme.Header.fontSize, fontStyle = FontStyle.Bold, font = GiltFrameTheme.Header.font }; _toggleBtnStyle.normal.textColor = GiltFrameTheme.Gold; _toggleBtnStyle.hover.textColor = GiltFrameTheme.GoldBright; _foldoutStyle = new GUIStyle(GiltFrameTheme.Header) { alignment = TextAnchor.MiddleLeft, padding = new RectOffset(2, 2, 2, 2) }; _foldoutStyle.normal.background = null; _foldoutStyle.hover.textColor = GiltFrameTheme.GoldBright; _foldoutStyle.hover.background = null; _foldoutStyle.active.textColor = GiltFrameTheme.GoldBright; _foldoutStyle.active.background = null; _smallLabelStyle = new GUIStyle(GiltFrameTheme.Key) { fontSize = Mathf.Max(6, GiltFrameTheme.Key.fontSize - 1) }; _gripStyle = new GUIStyle(GUI.skin.box) { border = new RectOffset(0, 0, 0, 0) }; _gripStyle.normal.background = MakeTex(GiltFrameTheme.Gold); } } private static Texture2D MakeTex(Color c) { Texture2D texture2D = new Texture2D(1, 1, TextureFormat.RGBA32, mipChain: false); texture2D.SetPixel(0, 0, c); texture2D.Apply(); return texture2D; } } } // ---- plugins/Fatty.dll :: Fatty.Patches.FoodBreakdownUI ---- namespace Fatty.Patches { internal static class FoodBreakdownUI { private const int MaxSegments = 8; private static Image[][] _segments; private static readonly Color DimSegment = new Color(0.35f, 0.3f, 0.18f, 0.9f); private static readonly Color BrightSegment = new Color(1f, 0.86f, 0.35f, 1f); internal static void BuildSlotExtras(Hud hud, Image[] icons, TMP_Text textTemplate) { try { if (hud == null || icons == null || icons.Length == 0) { return; } int num = icons.Length; _segments = new Image[num][]; for (int i = 0; i < num; i++) { if (!(icons[i] == null)) { _segments[i] = BuildSegmentBar(icons[i].transform); } } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Segmented Stack Bars. Reason: {arg}"); } } internal static void SyncFrame(int slotCount) { try { for (int i = 0; i < slotCount; i++) { SyncSegmentBar(i, (i < HudPatches.GroupCount) ? HudPatches.Groups[i] : null); } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Segmented Stack Bar Sync. Reason: {arg}"); _segments = null; } } private static void SyncSegmentBar(int slot, HudPatches.FoodGroup g) { Image[] array = ((_segments != null && slot < _segments.Length) ? _segments[slot] : null); if (array == null) { return; } int num = (ConfigManager.useSegmentedStackBar.Value ? (g?.Members?.Count).GetValueOrDefault() : 0); for (int i = 0; i < array.Length; i++) { Image image = array[i]; if (!(image == null)) { if (i >= num) { SetActive(image.transform, active: false); continue; } SetActive(image.transform, active: true); PlayerPatches.FoodContribution foodContribution = g.Members[i]; float t = ((foodContribution.BurnTime > 0f) ? Mathf.Clamp01(foodContribution.Food.m_time / foodContribution.BurnTime) : 0f); image.color = Color.Lerp(DimSegment, BrightSegment, t); } } } internal static string ResolveDisplayName(Player.Food food) { string text = food?.m_item?.m_shared?.m_name; if (string.IsNullOrEmpty(text)) { return PlayerPatches.ResolvePrefabName(food); } return Localize(text); } internal static string Localize(string token) { try { return Localization.instance.Localize(token); } catch { return token; } } internal static string FormatTime(float t) { if (t <= 0f) { return "0s"; } if (!(t >= 60f)) { return Mathf.FloorToInt(t) + "s"; } return Mathf.CeilToInt(t / 60f) + "m"; } private static Image[] BuildSegmentBar(Transform iconRoot) { GameObject gameObject = new GameObject("Fatty_SegmentBar", typeof(RectTransform)); gameObject.transform.SetParent(iconRoot, worldPositionStays: false); RectTransform obj = (RectTransform)gameObject.transform; obj.anchorMin = new Vector2(0f, 0f); obj.anchorMax = new Vector2(1f, 0f); obj.pivot = new Vector2(0.5f, 0f); obj.sizeDelta = new Vector2(0f, 4f); obj.anchoredPosition = new Vector2(0f, 1f); Image[] array = new Image[8]; for (int i = 0; i < 8; i++) { GameObject gameObject2 = new GameObject("Seg" + i, typeof(RectTransform)); gameObject2.transform.SetParent(gameObject.transform, worldPositionStays: false); RectTransform obj2 = (RectTransform)gameObject2.transform; float num = 0.015f; obj2.anchorMin = new Vector2((float)i / 8f + num, 0f); obj2.anchorMax = new Vector2((float)(i + 1) / 8f - num, 1f); obj2.offsetMin = Vector2.zero; obj2.offsetMax = Vector2.zero; Image image = gameObject2.AddComponent(); image.color = DimSegment; image.raycastTarget = false; gameObject2.SetActive(value: false); array[i] = image; } gameObject.transform.SetAsLastSibling(); return array; } private static void SetActive(Transform t, bool active) { if (t != null && t.gameObject.activeSelf != active) { t.gameObject.SetActive(active); } } } } // ---- plugins/Fatty.dll :: Fatty.Patches.GiltFrameTheme ---- namespace Fatty.Patches { internal static class GiltFrameTheme { private enum Edge { Top, Bottom, Left, Right } private sealed class Painter { public bool MirrorX; public bool MirrorY; public bool Transpose; private readonly int _w; private readonly int _h; private readonly float[] _a; private readonly float[] _z; public Painter(int w, int h) { _w = w; _h = h; _a = new float[w * h]; _z = new float[w * h]; } private void Put(float fx, float fy, float a, float z) { if (a <= 0f) { return; } if (Transpose) { float num = fx; fx = fy; fy = num; } int num2 = Mathf.RoundToInt(fx); int num3 = Mathf.RoundToInt(fy); if (MirrorX) { num2 = _w - 1 - num2; } if (MirrorY) { num3 = _h - 1 - num3; } if (num2 >= 0 && num3 >= 0 && num2 < _w && num3 < _h) { int num4 = num3 * _w + num2; if (a > _a[num4]) { _a[num4] = a; } if (z > _z[num4]) { _z[num4] = z; } } } public void Disc(float cx, float cy, float r) { if (r <= 0f) { return; } int num = Mathf.FloorToInt(cx - r - 1f); int num2 = Mathf.CeilToInt(cx + r + 1f); int num3 = Mathf.FloorToInt(cy - r - 1f); int num4 = Mathf.CeilToInt(cy + r + 1f); for (int i = num3; i <= num4; i++) { for (int j = num; j <= num2; j++) { float num5 = (float)j - cx; float num6 = (float)i - cy; float num7 = Mathf.Sqrt(num5 * num5 + num6 * num6); float num8 = Mathf.Clamp01(r + 0.5f - num7); if (!(num8 <= 0f)) { Put(j, i, num8, Mathf.Sqrt(Mathf.Max(0f, 1f - num7 / r * (num7 / r)))); } } } } public void Lozenge(float cx, float cy, float r) { int num = Mathf.FloorToInt(cx - r - 1f); int num2 = Mathf.CeilToInt(cx + r + 1f); int num3 = Mathf.FloorToInt(cy - r - 1f); int num4 = Mathf.CeilToInt(cy + r + 1f); for (int i = num3; i <= num4; i++) { for (int j = num; j <= num2; j++) { float num5 = Mathf.Abs((float)j - cx) + Mathf.Abs((float)i - cy); float num6 = Mathf.Clamp01(r + 0.5f - num5); if (!(num6 <= 0f)) { Put(j, i, num6, Mathf.Sqrt(Mathf.Max(0f, 1f - num5 / r * (num5 / r)))); } } } } public void Taper(Vector2 a, Vector2 b, float w0, float w1) { int num = Mathf.Max(8, Mathf.CeilToInt(Vector2.Distance(a, b) * 3f)); for (int i = 0; i <= num; i++) { float t = (float)i / (float)num; Vector2 vector = Vector2.Lerp(a, b, t); Disc(vector.x, vector.y, Mathf.Lerp(w0, w1, t)); } } public void Bezier(Vector2 a, Vector2 b, Vector2 c, float w0, float w1) { int num = Mathf.Max(16, Mathf.CeilToInt((Vector2.Distance(a, b) + Vector2.Distance(b, c)) * 3f)); for (int i = 0; i <= num; i++) { float num2 = (float)i / (float)num; float num3 = 1f - num2; Vector2 vector = num3 * num3 * a + 2f * num3 * num2 * b + num2 * num2 * c; Disc(vector.x, vector.y, Mathf.Lerp(w0, w1, num2)); } } public void Spiral(Vector2 eye, float r0, float growth, float t0, float t1, float phase, float w0, float w1, bool mirror = false) { int num = Mathf.Max(48, Mathf.CeilToInt((t1 - t0) * 40f)); for (int i = 0; i <= num; i++) { float t2 = (float)i / (float)num; float num2 = Mathf.Lerp(t0, t1, t2); float num3 = r0 * Mathf.Exp(growth * num2); float f = num2 + phase; float num4 = Mathf.Cos(f) * num3; float num5 = Mathf.Sin(f) * num3; if (mirror) { num4 = 0f - num4; } Disc(eye.x + num4, eye.y + num5, Mathf.Lerp(w0, w1, t2)); } } public void RailPixel(int x, int y, float d, float half) { float num = Mathf.Clamp01(half + 0.5f - d); if (!(num <= 0f)) { Put(x, y, num, Mathf.Sqrt(Mathf.Max(0f, 1f - d / half * (d / half)))); } } public void RailElbow(float mid, float half, float centre) { float num = centre - mid; for (int i = 0; i < _h; i++) { for (int j = 0; j < _w; j++) { float d; if (!((float)j <= centre) || !((float)i <= centre)) { d = ((!((float)i <= centre)) ? ((!((float)j <= centre)) ? Mathf.Min(Mathf.Abs((float)i - mid), Mathf.Abs((float)j - mid)) : Mathf.Abs((float)j - mid)) : Mathf.Abs((float)i - mid)); } else { float num2 = (float)j - centre; float num3 = (float)i - centre; d = Mathf.Abs(Mathf.Sqrt(num2 * num2 + num3 * num3) - num); } RailPixel(j, i, d, half); } } } public Texture2D Bake() { Texture2D texture2D = New(_w, _h, TextureWrapMode.Clamp, FilterMode.Bilinear); Color[] array = new Color[_w * _h]; for (int i = 0; i < _h; i++) { for (int j = 0; j < _w; j++) { int num = i * _w + j; float num2 = _a[num]; int num3 = (_h - 1 - i) * _w + j; if (num2 <= 0f) { array[num3] = Color.clear; continue; } float num4 = Z(j - 1, i) - Z(j + 1, i) + (Z(j, i - 1) - Z(j, i + 1)); float num5 = Mathf.Clamp01(0.42f + 0.5f * num4 + 0.18f * _z[num]); Color color = ((num5 < 0.5f) ? Color.Lerp(GoldDeep, Gold, num5 * 2f) : Color.Lerp(Gold, GoldBright, (num5 - 0.5f) * 2f)); array[num3] = new Color(color.r, color.g, color.b, num2); } } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false); return texture2D; } private float Z(int x, int y) { return _z[Mathf.Clamp(y, 0, _h - 1) * _w + Mathf.Clamp(x, 0, _w - 1)]; } } public static readonly Color GoldDeep = new Color(0.26f, 0.17f, 0.05f, 1f); public static readonly Color Gold = new Color(0.8f, 0.62f, 0.26f, 1f); public static readonly Color GoldBright = new Color(1f, 0.94f, 0.72f, 1f); public static readonly Color Parchment = new Color(0.9f, 0.86f, 0.75f, 1f); public static readonly Color Muted = new Color(0.6f, 0.56f, 0.48f, 1f); public const float Band = 18f; public const float TitleHeight = 54f; public const float FooterHeight = 30f; public const float Pad = 22f; private const int CornerTile = 84; private const float Overhang = 10f; private const float CornerExtent = 74f; private const int CrestW = 56; private const int CrestH = 34; private const float CrestOverhang = 8f; private const float OuterMid = 4.4f; private const float OuterHalf = 2.7f; private const float InnerMid = 12.4f; private const float InnerHalf = 1.6f; private const float BendCentre = 32f; private static int _fontSizeDelta; public static GUIStyle Title; public static GUIStyle SubTitle; public static GUIStyle Header; public static GUIStyle Key; public static GUIStyle Value; public static GUIStyle Note; public static GUIStyle Footer; public static GUIStyle Button; public static GUIStyle Primary; public static GUIStyle Row; public static GUIStyle Field; private static Texture2D _railTop; private static Texture2D _railBottom; private static Texture2D _railLeft; private static Texture2D _railRight; private static Texture2D _cornerTL; private static Texture2D _cornerTR; private static Texture2D _cornerBL; private static Texture2D _cornerBR; private static Texture2D _crestTop; private static Texture2D _crestBottom; private static Texture2D _diamond; private static Texture2D _panel; private static Texture2D _white; private static Texture2D _btnNormal; private static Texture2D _btnHover; private static Texture2D _btnActive; private static Texture2D _rowNormal; private static Texture2D _rowHover; private static Texture2D _rowActive; private static Texture2D _selection; private static Texture2D _fieldTex; public static void EnsureBuilt() { if (!(_railTop != null)) { _fontSizeDelta = ConfigManager.feastLedgerFontSizeDelta.Value; _railTop = BuildRail(Edge.Top); _railBottom = BuildRail(Edge.Bottom); _railLeft = BuildRail(Edge.Left); _railRight = BuildRail(Edge.Right); _cornerTL = BuildCorner(mirrorX: false, mirrorY: false); _cornerTR = BuildCorner(mirrorX: true, mirrorY: false); _cornerBL = BuildCorner(mirrorX: false, mirrorY: true); _cornerBR = BuildCorner(mirrorX: true, mirrorY: true); _crestTop = BuildCrest(flip: false); _crestBottom = BuildCrest(flip: true); _diamond = BuildDiamond(14); _panel = BuildPanel(64); _white = BuildSolid(Color.white); _btnNormal = BuildPatch(new Color(0.1f, 0.085f, 0.065f, 0.95f), GoldDeep); _btnHover = BuildPatch(new Color(0.19f, 0.15f, 0.09f, 0.97f), Gold); _btnActive = BuildPatch(new Color(0.3f, 0.23f, 0.11f, 0.98f), GoldBright); _rowNormal = BuildPatch(Color.clear, Color.clear); _rowHover = BuildPatch(new Color(0.8f, 0.62f, 0.26f, 0.1f), new Color(0.8f, 0.62f, 0.26f, 0.45f)); _rowActive = BuildPatch(new Color(0.8f, 0.62f, 0.26f, 0.2f), Gold); _selection = BuildSolid(new Color(0.8f, 0.62f, 0.26f, 0.15f)); _fieldTex = BuildPatch(new Color(0.03f, 0.03f, 0.025f, 0.95f), GoldDeep); BuildStyles(); } } public static void DrawWindow(Rect win, string title) { EnsureBuilt(); GUI.DrawTexture(new Rect(win.x + 1f, win.y + 1f, win.width - 2f, win.height - 2f), _panel, ScaleMode.StretchToFill); DrawFrame(win); DrawShadowed(new Rect(win.x + 74f, win.y + 18f + 8f, win.width - 148f, 34f), title.ToUpperInvariant(), Title); DrawRule(new Rect(win.x + 18f + 22f, win.y + 18f + 54f - 12f, win.width - 80f, 1f)); } public static Rect Body(Rect win) { return new Rect(win.x + 18f + 22f, win.y + 18f + 54f, win.width - 80f, win.height - 36f - 54f - 30f); } public static Rect FooterLine(Rect win) { return new Rect(win.x + 18f + 22f, win.yMax - 18f - 30f, win.width - 80f, 18f); } public static Rect TitleBar(Rect win) { return new Rect(win.x, win.y, win.width, 72f); } public static void DrawFrame(Rect r) { float width = r.width - 148f; float height = r.height - 148f; GUI.DrawTexture(new Rect(r.x + 74f, r.y, width, 18f), _railTop); GUI.DrawTexture(new Rect(r.x + 74f, r.yMax - 18f, width, 18f), _railBottom); GUI.DrawTexture(new Rect(r.x, r.y + 74f, 18f, height), _railLeft); GUI.DrawTexture(new Rect(r.xMax - 18f, r.y + 74f, 18f, height), _railRight); float num = 74f; GUI.DrawTexture(new Rect(r.x - 10f, r.y - 10f, 84f, 84f), _cornerTL); GUI.DrawTexture(new Rect(r.xMax - num, r.y - 10f, 84f, 84f), _cornerTR); GUI.DrawTexture(new Rect(r.x - 10f, r.yMax - num, 84f, 84f), _cornerBL); GUI.DrawTexture(new Rect(r.xMax - num, r.yMax - num, 84f, 84f), _cornerBR); float x = r.center.x - 28f; GUI.DrawTexture(new Rect(x, r.y - 8f, 56f, 34f), _crestTop); GUI.DrawTexture(new Rect(x, r.yMax + 8f - 34f, 56f, 34f), _crestBottom); DrawOutline(new Rect(r.x + 18f, r.y + 18f, r.width - 36f, r.height - 36f), new Color(Gold.r, Gold.g, Gold.b, 0.45f), 1f); } public static void DrawPanelFill(Rect win) { EnsureBuilt(); GUI.DrawTexture(new Rect(win.x + 1f, win.y + 1f, win.width - 2f, win.height - 2f), _panel, ScaleMode.StretchToFill); } public static void DrawOutline(Rect r, Color c, float t) { Color color = GUI.color; GUI.color = c; GUI.DrawTexture(new Rect(r.x, r.y, r.width, t), _white); GUI.DrawTexture(new Rect(r.x, r.yMax - t, r.width, t), _white); GUI.DrawTexture(new Rect(r.x, r.y, t, r.height), _white); GUI.DrawTexture(new Rect(r.xMax - t, r.y, t, r.height), _white); GUI.color = color; } public static void DrawFill(Rect r, Color c) { Color color = GUI.color; GUI.color = c; GUI.DrawTexture(r, _white); GUI.color = color; } public static void DrawInset(Rect r) { DrawFill(r, new Color(0f, 0f, 0f, 0.45f)); DrawOutline(r, new Color(Gold.r, Gold.g, Gold.b, 0.32f), 1f); } public static void DrawSelection(Rect r) { GUI.DrawTexture(r, _selection); } public static void DrawRule(Rect r) { DrawFill(r, new Color(Gold.r, Gold.g, Gold.b, 0.4f)); GUI.DrawTexture(new Rect(r.center.x - 7f, r.y - 7f + 0.5f, 14f, 14f), _diamond); } public static void DrawShadowed(Rect r, string text, GUIStyle style) { Color textColor = style.normal.textColor; style.normal.textColor = new Color(0f, 0f, 0f, 0.7f); GUI.Label(new Rect(r.x + 1.5f, r.y + 1.5f, r.width, r.height), text, style); style.normal.textColor = textColor; GUI.Label(r, text, style); } private static void BuildStyles() { Font font = FindFont(); Title = Text(font, 25, FontStyle.Bold, GoldBright, TextAnchor.MiddleCenter); SubTitle = Text(font, 18, FontStyle.Bold, Gold, TextAnchor.MiddleLeft); Header = Text(font, 13, FontStyle.Bold, Gold, TextAnchor.MiddleLeft); Key = Text(font, 13, FontStyle.Normal, Muted, TextAnchor.MiddleLeft); Value = Text(font, 13, FontStyle.Bold, Parchment, TextAnchor.MiddleLeft); Note = Text(font, 13, FontStyle.Italic, Muted, TextAnchor.MiddleCenter); Note.wordWrap = true; Footer = Text(font, 11, FontStyle.Normal, Muted, TextAnchor.MiddleCenter); Button = Patch(font, 13, FontStyle.Bold, TextAnchor.MiddleCenter, _btnNormal, _btnHover, _btnActive); Button.padding = new RectOffset(12, 12, 6, 6); Primary = Patch(font, 16, FontStyle.Bold, TextAnchor.MiddleCenter, _btnNormal, _btnHover, _btnActive); Primary.padding = new RectOffset(12, 12, 8, 8); Primary.normal.textColor = Gold; Row = Patch(font, 14, FontStyle.Normal, TextAnchor.MiddleLeft, _rowNormal, _rowHover, _rowActive); Row.padding = new RectOffset(12, 12, 4, 4); Row.normal.textColor = Parchment; Field = new GUIStyle { font = font, fontSize = Sized(13), alignment = TextAnchor.MiddleLeft, padding = new RectOffset(8, 8, 4, 4), border = new RectOffset(3, 3, 3, 3), clipping = TextClipping.Clip }; Field.normal.background = _fieldTex; Field.focused.background = _fieldTex; Field.hover.background = _fieldTex; Field.active.background = _fieldTex; Field.normal.textColor = Parchment; Field.focused.textColor = GoldBright; Field.hover.textColor = Parchment; Field.active.textColor = Parchment; } private static int Sized(int baseSize) { return Mathf.Max(6, baseSize + _fontSizeDelta); } private static GUIStyle Text(Font font, int size, FontStyle fs, Color colour, TextAnchor anchor) { return new GUIStyle { font = font, fontSize = Sized(size), fontStyle = fs, alignment = anchor, richText = true, wordWrap = false, clipping = TextClipping.Clip, normal = { textColor = colour } }; } private static GUIStyle Patch(Font font, int size, FontStyle fs, TextAnchor anchor, Texture2D normal, Texture2D hover, Texture2D active) { GUIStyle gUIStyle = new GUIStyle(); gUIStyle.font = font; gUIStyle.fontSize = Sized(size); gUIStyle.fontStyle = fs; gUIStyle.alignment = anchor; gUIStyle.richText = true; gUIStyle.wordWrap = false; gUIStyle.border = new RectOffset(3, 3, 3, 3); gUIStyle.clipping = TextClipping.Clip; gUIStyle.normal.background = normal; gUIStyle.hover.background = hover; gUIStyle.active.background = active; gUIStyle.focused.background = normal; gUIStyle.normal.textColor = Parchment; gUIStyle.hover.textColor = GoldBright; gUIStyle.active.textColor = GoldBright; gUIStyle.focused.textColor = Parchment; return gUIStyle; } private static Font FindFont() { Font[] array = Resources.FindObjectsOfTypeAll(); string[] array2 = new string[3] { "AveriaSerifLibre", "Norsebold", "Norse" }; foreach (string value in array2) { Font[] array3 = array; foreach (Font font in array3) { if (font != null && font.name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return font; } } } return null; } private static Texture2D BuildRail(Edge edge) { int num; int num2; if (edge != Edge.Top) { num = ((edge == Edge.Bottom) ? 1 : 0); if (num == 0) { num2 = 18; goto IL_0012; } } else { num = 1; } num2 = 4; goto IL_0012; IL_0012: int num3 = num2; int num4 = ((num != 0) ? 18 : 4); Painter painter = new Painter(num3, num4); for (int i = 0; i < num4; i++) { for (int j = 0; j < num3; j++) { float num5 = edge switch { Edge.Top => i, Edge.Bottom => num4 - 1 - i, Edge.Left => j, _ => num3 - 1 - j, }; painter.RailPixel(j, i, Mathf.Abs(num5 - 4.4f), 2.7f); painter.RailPixel(j, i, Mathf.Abs(num5 - 12.4f), 1.6f); } } return painter.Bake(); } private static Texture2D BuildCorner(bool mirrorX, bool mirrorY) { Painter painter = new Painter(84, 84) { MirrorX = mirrorX, MirrorY = mirrorY }; painter.RailElbow(14.4f, 2.7f, 32f); painter.RailElbow(22.4f, 1.6f, 32f); painter.Lozenge(11f, 11f, 4.5f); painter.Taper(new Vector2(13.5f, 13.5f), new Vector2(19.3f, 19.3f), 1.2f, 2.2f); painter.Lozenge(36f, 36f, 3.4f); for (int i = 0; i < 2; i++) { painter.Transpose = i == 1; painter.Taper(new Vector2(38.5f, 35f), new Vector2(47f, 32f), 1.4f, 0.45f); painter.Spiral(new Vector2(50f, 31f), 1.3f, 0.33f, 0f, 6.8f, 5.371f, 0.55f, 2.4f); painter.Bezier(new Vector2(56f, 36f), new Vector2(68f, 41f), new Vector2(78f, 30f), 2.2f, 0.35f); painter.Spiral(new Vector2(76f, 32f), 0.9f, 0.32f, 0f, 4.6f, 1f, 1.1f, 0.3f); painter.Disc(58f, 34f, 1.8f); } painter.Transpose = false; return painter.Bake(); } private static Texture2D BuildCrest(bool flip) { Painter painter = new Painter(56, 34); painter.MirrorY = flip; painter.Disc(28f, 2.5f, 1.8f); painter.Lozenge(28f, 9f, 6.5f); painter.Taper(new Vector2(28f, 14f), new Vector2(28f, 24f), 2.2f, 1.2f); painter.Spiral(new Vector2(17f, 25f), 1.2f, 0.33f, 0f, 5.4f, 2.6f, 2f, 0.35f); painter.Spiral(new Vector2(39f, 25f), 1.2f, 0.33f, 0f, 5.4f, 2.6f, 2f, 0.35f, mirror: true); painter.Bezier(new Vector2(23f, 19f), new Vector2(13f, 24f), new Vector2(4f, 16f), 1.6f, 0.3f); painter.Bezier(new Vector2(33f, 19f), new Vector2(43f, 24f), new Vector2(52f, 16f), 1.6f, 0.3f); painter.Disc(3f, 14f, 1.5f); painter.Disc(53f, 14f, 1.5f); return painter.Bake(); } private static Texture2D BuildDiamond(int size) { Painter painter = new Painter(size, size); float num = (float)(size - 1) * 0.5f; painter.Lozenge(num, num, (float)size * 0.36f); return painter.Bake(); } private static Texture2D BuildPanel(int size) { Texture2D texture2D = New(size, size, TextureWrapMode.Clamp, FilterMode.Bilinear); Color[] array = new Color[size * size]; float num = (float)(size - 1) * 0.5f; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { float num2 = ((float)j - num) / num; float num3 = ((float)i - num) / num; float num4 = Mathf.Clamp01(1f - 0.55f * Mathf.Sqrt(0.6f * (num2 * num2 + num3 * num3))); array[i * size + j] = new Color(0.055f * num4 + 0.02f, 0.048f * num4 + 0.017f, 0.038f * num4 + 0.013f, 0.955f); } } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false); return texture2D; } private static Texture2D BuildPatch(Color fill, Color border) { Texture2D texture2D = New(12, 12, TextureWrapMode.Clamp, FilterMode.Point); Color[] array = new Color[144]; for (int i = 0; i < 12; i++) { for (int j = 0; j < 12; j++) { int num = Mathf.Min(Mathf.Min(j, i), Mathf.Min(11 - j, 11 - i)); array[i * 12 + j] = ((num == 0) ? border : fill); } } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false); return texture2D; } private static Texture2D BuildSolid(Color c) { Texture2D texture2D = New(1, 1, TextureWrapMode.Clamp, FilterMode.Point); texture2D.SetPixel(0, 0, c); texture2D.Apply(updateMipmaps: false); return texture2D; } private static Texture2D New(int w, int h, TextureWrapMode wrap, FilterMode filter) { return new Texture2D(w, h, TextureFormat.RGBA32, mipChain: false) { wrapMode = wrap, filterMode = filter, hideFlags = HideFlags.HideAndDontSave }; } } } // ---- plugins/Fatty.dll :: Fatty.Patches.HudPatches ---- namespace Fatty.Patches { [HarmonyPatch] public static class HudPatches { internal class FoodGroup { public Player.Food Rep; public int Count; public float ActiveTime; public float TotalTime; public readonly List Members = new List(); } private const int TargetSlots = 5; internal static readonly Color GoldTrim = new Color(0.82f, 0.66f, 0.22f, 1f); internal static readonly Color GoldText = new Color(1f, 0.86f, 0.35f, 1f); private static bool _customDrawReady; private static TMP_Text[] _countLabels; internal static readonly List Groups = new List(); internal static int GroupCount; private static readonly Dictionary _groupIndex = new Dictionary(); private static readonly Dictionary _stackSeen = new Dictionary(); [HarmonyPatch(typeof(Hud), "Awake")] [HarmonyPostfix] public static void Hud_Awake_Postfix(Hud __instance, ref Image[] ___m_foodIcons, ref Image[] ___m_foodBars, ref TMP_Text[] ___m_foodTime, RectTransform ___m_foodBarRoot) { _customDrawReady = false; _countLabels = null; try { if (___m_foodIcons == null || ___m_foodBars == null || ___m_foodTime == null) { Bail("food icon/bar/time arrays are null"); return; } int num = ___m_foodIcons.Length; if (num < 1 || ___m_foodBars.Length != num || ___m_foodTime.Length != num) { Bail($"food arrays empty or mismatched (icons={___m_foodIcons.Length}, bars={___m_foodBars.Length}, time={___m_foodTime.Length})"); return; } if (num < 5) { TryExpand(ref ___m_foodIcons, ref ___m_foodBars, ref ___m_foodTime, num); } int num2 = ___m_foodIcons.Length; TMP_Text[] array = new TMP_Text[num2]; TMP_Text tMP_Text = FirstNonNull(___m_foodTime); for (int i = 0; i < num2; i++) { if (!(___m_foodIcons[i] == null)) { StyleIcon(___m_foodIcons[i]); array[i] = CreateCountLabel(___m_foodIcons[i].transform, tMP_Text); } } _countLabels = array; _customDrawReady = true; FattyPlugin.Instance.Log.LogInfo($"Fatty: food HUD ready with {num2} slot(s); custom steady draw active."); try { FoodBreakdownUI.BuildSlotExtras(__instance, ___m_foodIcons, tMP_Text); } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Breakdown/Feast Log UI. Reason: {arg}"); } } catch (Exception arg2) { _customDrawReady = false; _countLabels = null; FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food HUD Setup. Reason: {arg2}"); } } [HarmonyPatch(typeof(Hud), "UpdateFood")] [HarmonyPrefix] public static bool Hud_UpdateFood_Prefix(Hud __instance, Player player, Image[] ___m_foodIcons, Image[] ___m_foodBars, TMP_Text[] ___m_foodTime, RectTransform ___m_foodBarRoot, RectTransform ___m_foodBaseBar) { if (!_customDrawReady || player == null) { return true; } if (___m_foodIcons == null || ___m_foodBars == null || ___m_foodTime == null) { return true; } try { List foods = player.GetFoods(); GroupCount = 0; _groupIndex.Clear(); _stackSeen.Clear(); bool value = ConfigManager.noFoodDecay.Value; float multiplier = FoodDuration.GetMultiplier(); float healthBenefit = FoodDuration.GetHealthBenefit(); float staminaBenefit = FoodDuration.GetStaminaBenefit(); float eitrBenefit = FoodDuration.GetEitrBenefit(); if (foods != null) { foreach (Player.Food item2 in foods) { if (item2 == null || item2.m_item == null || item2.m_item.m_shared == null) { continue; } string text = PlayerPatches.ResolvePrefabName(item2); if (string.IsNullOrEmpty(text)) { text = "?"; } PlayerPatches.FoodContribution item = PlayerPatches.ComputeContribution(item2, _stackSeen, value, multiplier, healthBenefit, staminaBenefit, eitrBenefit); FoodGroup foodGroup; if (_groupIndex.TryGetValue(text, out var value2)) { foodGroup = Groups[value2]; foodGroup.Count++; foodGroup.TotalTime += item2.m_time; foodGroup.ActiveTime = item2.m_time; } else { value2 = GroupCount++; if (value2 < Groups.Count) { foodGroup = Groups[value2]; foodGroup.Members.Clear(); } else { foodGroup = new FoodGroup(); Groups.Add(foodGroup); } foodGroup.Rep = item2; foodGroup.Count = 1; foodGroup.TotalTime = item2.m_time; foodGroup.ActiveTime = item2.m_time; _groupIndex[text] = value2; } foodGroup.Members.Add(item); } } if (___m_foodBaseBar != null) { ___m_foodBaseBar.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, player.GetBaseFoodHP() / 25f * 32f); } int num = ___m_foodIcons.Length; for (int i = 0; i < num; i++) { Image image = ((i < ___m_foodIcons.Length) ? ___m_foodIcons[i] : null); Image image2 = ((i < ___m_foodBars.Length) ? ___m_foodBars[i] : null); TMP_Text tMP_Text = ((i < ___m_foodTime.Length) ? ___m_foodTime[i] : null); TMP_Text tMP_Text2 = ((_countLabels != null && i < _countLabels.Length) ? _countLabels[i] : null); bool flag = i < GroupCount; if (image != null) { SetActiveSafe(image.transform, flag); } if (image2 != null) { SetActiveSafe(image2.transform, flag); } if (tMP_Text != null) { SetActiveSafe(tMP_Text.transform, flag); } if (flag) { FoodGroup foodGroup2 = Groups[i]; if (image != null && foodGroup2.Rep != null && foodGroup2.Rep.m_item != null) { image.sprite = foodGroup2.Rep.m_item.GetIcon(); image.color = Color.white; } if (tMP_Text != null) { float activeTime = foodGroup2.ActiveTime; tMP_Text.text = ((activeTime >= 60f) ? (Mathf.CeilToInt(activeTime / 60f) + "m") : (Mathf.FloorToInt(activeTime) + "s")); tMP_Text.color = Color.white; } if (tMP_Text2 != null) { if (foodGroup2.Count > 1 && !ConfigManager.useSegmentedStackBar.Value) { tMP_Text2.text = "x" + foodGroup2.Count; SetActiveSafe(tMP_Text2.transform, active: true); } else { SetActiveSafe(tMP_Text2.transform, active: false); } } } else if (tMP_Text2 != null) { SetActiveSafe(tMP_Text2.transform, active: false); } } if (___m_foodBarRoot != null) { ___m_foodBarRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Ceil(player.GetMaxHealth() / 25f * 32f)); } if (ConfigManager.breakdownToggleKey.Value.IsDown() && CanAcceptHotkey()) { FeastLedgerGui.Toggle(); } FoodBreakdownUI.SyncFrame(num); return false; } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food HUD Draw. Reason: {arg}"); return true; } } [HarmonyPatch(typeof(Hud), "UpdateStatusEffects")] [HarmonyPostfix] public static void Hud_UpdateStatusEffects_Postfix(List statusEffects, List ___m_statusEffects) { try { if (statusEffects == null || ___m_statusEffects == null) { return; } int num = Mathf.Min(statusEffects.Count, ___m_statusEffects.Count); for (int i = 0; i < num; i++) { StatusEffect statusEffect = statusEffects[i]; if (statusEffect == null) { continue; } int num2 = statusEffect.NameHash(); bool flag = num2 == SynergyEffects.BalancedDietHash || num2 == SynergyEffects.SugarRushHash || num2 == SynergyEffects.FishermanHash || num2 == SynergyEffects.LumberjackHash; if (!flag) { foreach (int allHash in DrinkBuffs.AllHashes) { if (num2 == allHash) { flag = true; break; } } } if (!flag) { continue; } RectTransform rectTransform = ___m_statusEffects[i]; if (!(rectTransform == null)) { Transform transform = rectTransform.Find("TimeBar"); if (transform != null) { transform.gameObject.SetActive(value: false); } Transform transform2 = rectTransform.Find("Cooldown"); if (transform2 != null) { transform2.gameObject.SetActive(value: false); } } } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Synergy Status Icon Cleanup. Reason: {arg}"); } } private static void TryExpand(ref Image[] icons, ref Image[] bars, ref TMP_Text[] times, int existing) { if (existing < 2) { Bail("need >=2 existing slots to derive the layout spacing; keeping current count"); return; } if (icons[0] == null || icons[1] == null || bars[0] == null || bars[1] == null || times[0] == null || times[1] == null) { Bail("slot 0/1 has a null component; keeping current count"); return; } Image[] array = Grow(icons, 5); Image[] array2 = Grow(bars, 5); TMP_Text[] array3 = Grow(times, 5); List list = new List(); bool flag = ExpandArray(bars, array2, existing, list); bool flag2 = icons[0].transform.parent != icons[1].transform.parent; bool flag3 = (flag2 ? ExpandIconGroups(icons, times, array, array3, existing, list) : (ExpandArray(icons, array, existing, list) && ExpandArray(times, array3, existing, list))); if (!flag || !flag3) { DestroyClones(list); Bail($"could not build extra slots (bars ok: {flag}, icons ok: {flag3}); keeping {existing} slot(s)"); return; } icons = array; bars = array2; times = array3; FattyPlugin.Instance.Log.LogInfo(string.Format("Fatty: expanded food HUD {0} -> {1} slots (icons {2}).", existing, 5, flag2 ? "grouped" : "parallel")); } private static bool ExpandArray(T[] src, T[] dst, int existing, List created) where T : Component { for (int i = existing; i < 5; i++) { T val = CloneComponent(src[0], src[1], i, created); if (val == null) { return false; } dst[i] = val; } return true; } private static T CloneComponent(T template, T next, int slot, List created) where T : Component { if (template == null) { return null; } GameObject gameObject = UnityEngine.Object.Instantiate(template.gameObject, template.transform.parent); gameObject.name = template.gameObject.name + "_Fatty" + slot; created.Add(gameObject); T component = gameObject.GetComponent(); if (component == null) { return null; } if (template.transform is RectTransform rectTransform && next.transform is RectTransform rectTransform2 && gameObject.transform is RectTransform rectTransform3) { Vector2 vector = rectTransform2.anchoredPosition - rectTransform.anchoredPosition; rectTransform3.anchoredPosition = rectTransform.anchoredPosition + vector * slot; } return component; } private static bool ExpandIconGroups(Image[] icons, TMP_Text[] times, Image[] newIcons, TMP_Text[] newTimes, int existing, List created) { Transform parent = icons[0].transform.parent; Transform parent2 = icons[1].transform.parent; if (parent == null || parent2 == null) { return false; } string name = icons[0].name; string name2 = times[0].name; bool flag = times[0].transform.IsChildOf(parent); Vector2 vector = ((parent is RectTransform rectTransform) ? rectTransform.anchoredPosition : Vector2.zero); Vector2 vector2 = ((parent2 is RectTransform rectTransform2) ? (rectTransform2.anchoredPosition - vector) : Vector2.zero); if (vector2 == Vector2.zero) { return false; } for (int i = existing; i < 5; i++) { GameObject gameObject = UnityEngine.Object.Instantiate(parent.gameObject, parent.parent); gameObject.name = "Fatty_FoodSlot" + i; created.Add(gameObject); if (gameObject.transform is RectTransform rectTransform3) { rectTransform3.anchoredPosition = vector + vector2 * i; } Image image = FindComp(gameObject.transform, name); if (image == null) { return false; } newIcons[i] = image; if (flag) { TMP_Text tMP_Text = FindComp(gameObject.transform, name2); if (tMP_Text == null) { return false; } newTimes[i] = tMP_Text; } } if (!flag && !ExpandArray(times, newTimes, existing, created)) { return false; } return true; } private static void StyleIcon(Image icon) { if (!(icon == null) && !(icon.gameObject.GetComponent() != null)) { Outline outline = icon.gameObject.AddComponent(); outline.effectColor = GoldTrim; outline.effectDistance = new Vector2(2f, 2f); } } internal static TMP_Text CloneText(Transform parent, TMP_Text template, string name) { if (parent == null || template == null) { return null; } GameObject gameObject = UnityEngine.Object.Instantiate(template.gameObject, parent); gameObject.name = name; TMP_Text component = gameObject.GetComponent(); if (component == null) { UnityEngine.Object.Destroy(gameObject); return null; } component.text = string.Empty; component.enableAutoSizing = false; return component; } private static TMP_Text CreateCountLabel(Transform iconRoot, TMP_Text template) { TMP_Text tMP_Text = CloneText(iconRoot, template, "Fatty_StackCount"); if (tMP_Text == null) { return null; } GameObject gameObject = tMP_Text.gameObject; tMP_Text.color = GoldText; tMP_Text.fontStyle = FontStyles.Bold; tMP_Text.alignment = TextAlignmentOptions.TopRight; tMP_Text.raycastTarget = false; if (gameObject.transform is RectTransform rectTransform) { rectTransform.anchorMin = new Vector2(1f, 1f); rectTransform.anchorMax = new Vector2(1f, 1f); rectTransform.pivot = new Vector2(1f, 1f); rectTransform.anchoredPosition = new Vector2(-1f, -1f); } gameObject.transform.SetAsLastSibling(); gameObject.SetActive(value: false); return tMP_Text; } private static bool CanAcceptHotkey() { if ((Chat.instance == null || !Chat.instance.HasFocus()) && !Console.IsVisible() && !TextInput.IsVisible() && !Menu.IsVisible()) { return !InventoryGui.IsVisible(); } return false; } private static void SetActiveSafe(Transform t, bool active) { if (t != null && t.gameObject.activeSelf != active) { t.gameObject.SetActive(active); } } private static void Bail(string reason) { FattyPlugin.Instance.Log.LogWarning("Fatty Mod: Food HUD expansion skipped - " + reason + "."); } private static T[] Grow(T[] src, int length) { T[] array = new T[length]; for (int i = 0; i < src.Length && i < length; i++) { array[i] = src[i]; } return array; } private static T FirstNonNull(T[] arr) where T : class { if (arr == null) { return null; } foreach (T val in arr) { if (val != null) { return val; } } return null; } private static void DestroyClones(List objs) { foreach (GameObject obj in objs) { if (obj != null) { UnityEngine.Object.Destroy(obj); } } } private static T FindComp(Transform root, string name) where T : Component { if (root.name == name) { T component = root.GetComponent(); if (component != null) { return component; } } for (int i = 0; i < root.childCount; i++) { T val = FindComp(root.GetChild(i), name); if (val != null) { return val; } } return null; } } } // ---- plugins/Fatty.dll :: Fatty.Patches.PlayerPatches ---- namespace Fatty.Patches { [HarmonyPatch] public static class PlayerPatches { internal struct FoodContribution { public Player.Food Food; public string Name; public int StackIndex; public float Multiplier; public float DecayFactor; public float BurnTime; public float Hp; public float Stamina; public float Eitr; } private const int MaxFoodSlots = 4; private const int MaxDrinkSlots = 1; private const int VanillaFoodSlots = 3; private static readonly Dictionary _lastBlockLog = new Dictionary(); private const float BlockLogCooldown = 5f; private static readonly List _stashedFoods = new List(); private static bool _foodsStashed; private static readonly List _frozenFoodEntries = new List(); private static readonly List _frozenFoodTimes = new List(); private static readonly List _frozenFoodHealth = new List(); private static readonly List _frozenFoodStamina = new List(); private static readonly List _frozenFoodEitr = new List(); public static bool CanAddFood(List foods, ItemDrop.ItemData item, out string blockMessage) { blockMessage = null; if (item == null || item.m_shared == null) { return false; } string text = ResolvePrefabName(item); bool flag = CategoryManager.IsDrinkPrefab(text); int num = 0; HashSet hashSet = new HashSet(); HashSet hashSet2 = new HashSet(); if (foods != null) { foreach (Player.Food food in foods) { if (food != null && food.m_item != null && food.m_item.m_shared != null) { string text2 = ResolvePrefabName(food); if (text2 == text) { num++; } if (CategoryManager.IsDrinkPrefab(text2)) { hashSet2.Add(text2); } else { hashSet.Add(text2); } } } } int num2 = Mathf.Max(1, ConfigManager.maxFoodStacks.Value); if (num >= num2) { blockMessage = "You cannot eat any more of this right now!"; LogBlock(text, flag, num, num2, hashSet.Count, hashSet2.Count, blockMessage); return false; } if (num == 0) { if (flag && hashSet2.Count >= 1) { blockMessage = "Drink slot is full!"; LogBlock(text, flag, num, num2, hashSet.Count, hashSet2.Count, blockMessage); return false; } if (!flag && hashSet.Count >= 4) { blockMessage = "Food slots are full!"; LogBlock(text, flag, num, num2, hashSet.Count, hashSet2.Count, blockMessage); return false; } } return true; } private static void LogBlock(string prefabName, bool isDrink, int sameFood, int maxStacks, int foodTypeCount, int drinkTypeCount, string reason) { string key = prefabName + "|" + reason; float unscaledTime = Time.unscaledTime; if (!_lastBlockLog.TryGetValue(key, out var value) || !(unscaledTime - value < 5f)) { _lastBlockLog[key] = unscaledTime; FattyPlugin.Instance.Log.LogInfo(string.Format("[Fatty] Blocked eating '{0}' (category={1}, sameFood={2}/{3}, foodTypes={4}/{5}, drinkTypes={6}/{7}): {8}", prefabName, isDrink ? "Drink" : "Food", sameFood, maxStacks, foodTypeCount, 4, drinkTypeCount, 1, reason)); } } public static string ResolvePrefabName(ItemDrop.ItemData item, string savedName = null) { if (item != null && item.m_dropPrefab != null && !string.IsNullOrEmpty(item.m_dropPrefab.name)) { return item.m_dropPrefab.name; } if (!string.IsNullOrEmpty(savedName) && !IsLocalizationToken(savedName)) { return savedName; } string text = item?.m_shared?.m_name; if (!string.IsNullOrEmpty(text)) { string prefabNameForInGameName = CategoryManager.GetPrefabNameForInGameName(text); if (!string.IsNullOrEmpty(prefabNameForInGameName)) { return prefabNameForInGameName; } } return text ?? savedName ?? string.Empty; } public static string ResolvePrefabName(Player.Food food) { if (food == null) { return string.Empty; } return ResolvePrefabName(food.m_item, food.m_name); } private static bool IsLocalizationToken(string s) { if (s.Length > 0) { return s[0] == '$'; } return false; } private static float GetStackMultiplier(int stackIndex) { if (stackIndex <= 0) { return 1f; } return stackIndex switch { 1 => ConfigManager.stackDiminishingReturnStep1.Value, 2 => ConfigManager.stackDiminishingReturnStep2.Value, _ => ConfigManager.stackDiminishingReturnStep3.Value, }; } private static int NextStackIndex(Dictionary seen, string prefabName) { int value; int num = (seen.TryGetValue(prefabName, out value) ? value : 0); seen[prefabName] = num + 1; return num; } internal static FoodContribution ComputeContribution(Player.Food food, Dictionary seen, bool noDecay, float durationMultiplier, float hpBenefit, float staminaBenefit, float eitrBenefit) { ItemDrop.ItemData.SharedData shared = food.m_item.m_shared; string text = ResolvePrefabName(food); int stackIndex = NextStackIndex(seen, text); float stackMultiplier = GetStackMultiplier(stackIndex); float burnTime = GetBurnTime(food.m_item, durationMultiplier); float num = 1f; if (!noDecay) { num = ((burnTime > 0f) ? Mathf.Pow(Mathf.Clamp01(food.m_time / burnTime), 0.3f) : 0f); } return new FoodContribution { Food = food, Name = text, StackIndex = stackIndex, Multiplier = stackMultiplier, DecayFactor = num, BurnTime = burnTime, Hp = shared.m_food * num * stackMultiplier * hpBenefit, Stamina = shared.m_foodStamina * num * stackMultiplier * staminaBenefit, Eitr = shared.m_foodEitr * num * stackMultiplier * eitrBenefit }; } [HarmonyPatch(typeof(Player), "CanEat")] [HarmonyPrefix] public static bool CanEat_Prefix(Player __instance, ItemDrop.ItemData item, bool showMessages, ref bool __result) { try { if (item == null || item.m_shared == null) { return true; } string blockMessage; bool flag = CanAddFood(__instance.GetFoods(), item, out blockMessage); if (!flag && showMessages && !string.IsNullOrEmpty(blockMessage)) { __instance.Message(MessageHud.MessageType.Center, blockMessage); } __result = flag; return false; } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Eat Rules (CanEat). Reason: {arg}"); return true; } } [HarmonyPatch(typeof(Player), "EatFood")] [HarmonyPrefix] public static bool EatFood_Prefix(Player __instance, ItemDrop.ItemData item, ref List ___m_foods, ref bool __result) { try { if (item == null || item.m_shared == null) { return true; } if (!CanAddFood(___m_foods, item, out var blockMessage)) { if (!string.IsNullOrEmpty(blockMessage)) { __instance.Message(MessageHud.MessageType.Center, blockMessage); } __result = false; return false; } float multiplier = FoodDuration.GetMultiplier(); float burnTime = GetBurnTime(item, multiplier); if (burnTime <= 0f) { if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo("[Fatty Debug] '" + ResolvePrefabName(item) + "' has no food burn time and no status-effect duration; not taking a slot."); } __result = true; return false; } Player.Food food = new Player.Food { m_name = ResolvePrefabName(item), m_item = item, m_time = burnTime, m_health = item.m_shared.m_food, m_stamina = item.m_shared.m_foodStamina, m_eitr = item.m_shared.m_foodEitr }; ___m_foods.Add(food); string text = string.Empty; if (item.m_shared.m_food > 0f) { text = text + " +" + item.m_shared.m_food + " $item_food_health "; } if (item.m_shared.m_foodStamina > 0f) { text = text + " +" + item.m_shared.m_foodStamina + " $item_food_stamina "; } if (item.m_shared.m_foodEitr > 0f) { text = text + " +" + item.m_shared.m_foodEitr + " $item_food_eitr "; } if (!string.IsNullOrWhiteSpace(text)) { __instance.Message(MessageHud.MessageType.Center, text); } FoodMilestones.RecordEaten(__instance, CategoryManager.GetCategoryForPrefab(food.m_name)); FoodLifetimeLog.RecordEaten(__instance, food.m_name); __instance.UpdateFood(0f, true); if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo($"[Fatty Debug] Ate '{food.m_name}'. Total slots used: {___m_foods.Count}"); } __result = true; return false; } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Eat Handling (EatFood). Reason: {arg}"); return true; } } [HarmonyPatch(typeof(Player), "GetTotalFoodValue")] [HarmonyPrefix] [HarmonyPriority(0)] public static void GetTotalFoodValue_Prefix(List ___m_foods) { try { if (!_foodsStashed && ___m_foods != null && ___m_foods.Count != 0) { _stashedFoods.Clear(); _stashedFoods.AddRange(___m_foods); ___m_foods.Clear(); _foodsStashed = true; } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Totals (stash). Reason: {arg}"); } } [HarmonyPatch(typeof(Player), "GetTotalFoodValue")] [HarmonyPostfix] [HarmonyPriority(800)] public static void GetTotalFoodValue_Postfix(Player __instance, ref float hp, ref float stamina, ref float eitr, List ___m_foods) { RestoreFoods(___m_foods); try { if (___m_foods == null) { return; } float num = stamina; float num2 = eitr; bool value = ConfigManager.noFoodDecay.Value; float multiplier = FoodDuration.GetMultiplier(); float healthBenefit = FoodDuration.GetHealthBenefit(); float staminaBenefit = FoodDuration.GetStaminaBenefit(); float eitrBenefit = FoodDuration.GetEitrBenefit(); float num3 = 0f; float num4 = 0f; float num5 = 0f; Dictionary seen = new Dictionary(); int num6 = 0; foreach (Player.Food ___m_food in ___m_foods) { if (___m_food != null && ___m_food.m_item != null && ___m_food.m_item.m_shared != null) { ItemDrop.ItemData.SharedData shared = ___m_food.m_item.m_shared; FoodContribution foodContribution = ComputeContribution(___m_food, seen, value, multiplier, healthBenefit, staminaBenefit, eitrBenefit); if (foodContribution.StackIndex == 0) { num6++; } num3 += foodContribution.Hp; num4 += foodContribution.Stamina; num5 += foodContribution.Eitr; num3 += DrinkBuffs.GetMaxHealthBonus(foodContribution.Name, foodContribution.Multiplier); if (foodContribution.StackIndex == 0 && num6 <= 3) { num += shared.m_foodStamina * foodContribution.DecayFactor; num2 += shared.m_foodEitr * foodContribution.DecayFactor; } } } hp += num3; stamina += num4; eitr += num5; eitr += FoodMilestones.GetEitrBonus(__instance); RegenScaling.Apply(__instance, stamina, num, eitr, num2); } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Totals (GetTotalFoodValue). Reason: {arg}"); } } [HarmonyPatch(typeof(Player), "GetTotalFoodValue")] [HarmonyFinalizer] public static void GetTotalFoodValue_Finalizer(List ___m_foods) { RestoreFoods(___m_foods); } private static void RestoreFoods(List foods) { if (!_foodsStashed) { return; } _foodsStashed = false; try { if (foods != null) { foods.Clear(); foods.AddRange(_stashedFoods); } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Totals (restore). Reason: {arg}"); } finally { _stashedFoods.Clear(); } } private static float GetBurnTime(ItemDrop.ItemData item, float durationMultiplier) { ItemDrop.ItemData.SharedData sharedData = item?.m_shared; if (sharedData == null) { return 0f; } float num = sharedData.m_foodBurnTime; if (num <= 0f) { if (sharedData.m_consumeStatusEffect != null) { num = sharedData.m_consumeStatusEffect.m_ttl; } if (CategoryManager.IsDrinkPrefab(ResolvePrefabName(item))) { num = Mathf.Max(num, ConfigManager.drinkSlotDefaultDuration.Value); } } if (!(num > 0f)) { return 0f; } return num * durationMultiplier; } [HarmonyPatch(typeof(Player), "UpdateFood")] [HarmonyPrefix] public static void UpdateFood_Prefix(Player __instance, float dt, bool forceUpdate, ref float ___m_foodRegenTimer, List ___m_foods) { try { if (forceUpdate || __instance == null || ___m_foodRegenTimer + dt < 10f) { return; } ___m_foodRegenTimer = 0f; if (___m_foods == null || ___m_foods.Count == 0) { return; } float num = 0f; Dictionary seen = new Dictionary(); foreach (Player.Food ___m_food in ___m_foods) { if (___m_food != null && ___m_food.m_item != null && ___m_food.m_item.m_shared != null) { int stackIndex = NextStackIndex(seen, ResolvePrefabName(___m_food)); num += ___m_food.m_item.m_shared.m_foodRegen * GetStackMultiplier(stackIndex); } } if (!(num <= 0f)) { float regenMultiplier = 1f; __instance.GetSEMan().ModifyHealthRegen(ref regenMultiplier); __instance.Heal(num * regenMultiplier); } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Food Regen Stacking. Reason: {arg}"); } } [HarmonyPatch(typeof(Player), "UpdateFood")] [HarmonyPrefix] public static void FoodDecay_Prefix(List ___m_foods) { _frozenFoodEntries.Clear(); _frozenFoodTimes.Clear(); _frozenFoodHealth.Clear(); _frozenFoodStamina.Clear(); _frozenFoodEitr.Clear(); try { if (___m_foods == null || ___m_foods.Count < 2) { return; } Dictionary dictionary = new Dictionary(); for (int i = 0; i < ___m_foods.Count; i++) { Player.Food food = ___m_foods[i]; if (food != null && food.m_item != null && food.m_item.m_shared != null) { dictionary[ResolvePrefabName(food)] = i; } } for (int j = 0; j < ___m_foods.Count; j++) { Player.Food food2 = ___m_foods[j]; if (food2 != null && food2.m_item != null && food2.m_item.m_shared != null && (!dictionary.TryGetValue(ResolvePrefabName(food2), out var value) || value != j)) { _frozenFoodEntries.Add(food2); _frozenFoodTimes.Add(food2.m_time); _frozenFoodHealth.Add(food2.m_health); _frozenFoodStamina.Add(food2.m_stamina); _frozenFoodEitr.Add(food2.m_eitr); } } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Sequential Food Decay (prefix). Reason: {arg}"); _frozenFoodEntries.Clear(); } } [HarmonyPatch(typeof(Player), "UpdateFood")] [HarmonyPostfix] public static void FoodDecay_Postfix() { try { for (int i = 0; i < _frozenFoodEntries.Count; i++) { Player.Food food = _frozenFoodEntries[i]; if (food != null) { food.m_time = _frozenFoodTimes[i]; food.m_health = _frozenFoodHealth[i]; food.m_stamina = _frozenFoodStamina[i]; food.m_eitr = _frozenFoodEitr[i]; } } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Sequential Food Decay (postfix). Reason: {arg}"); } finally { _frozenFoodEntries.Clear(); _frozenFoodTimes.Clear(); _frozenFoodHealth.Clear(); _frozenFoodStamina.Clear(); _frozenFoodEitr.Clear(); } } private static bool IsStatOnlyDrink(ItemDrop.ItemData item) { if (item == null || item.m_shared == null) { return false; } if (item.m_shared.m_food > 0f) { return false; } return CategoryManager.IsDrinkPrefab(ResolvePrefabName(item)); } [HarmonyPatch(typeof(Player), "CanConsumeItem")] [HarmonyPrefix] public static bool CanConsumeItem_Prefix(Player __instance, ItemDrop.ItemData item, bool checkWorldLevel, ref bool __result) { try { if (item == null || item.m_shared == null) { return true; } if (ResolvePrefabName(item) != "MeadTasty") { return true; } if (item.m_shared.m_itemType != ItemDrop.ItemData.ItemType.Consumable) { return true; } if (checkWorldLevel && Game.m_worldLevel > 0 && item.m_worldLevel < Game.m_worldLevel) { return true; } if (!CanAddFood(__instance.GetFoods(), item, out var _)) { return true; } __result = true; return false; } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Tasty Mead Stacking (CanConsumeItem). Reason: {arg}"); return true; } } [HarmonyPatch(typeof(Player), "ConsumeItem")] [HarmonyPostfix] public static void ConsumeItem_Postfix(Player __instance, ItemDrop.ItemData item, bool __result) { try { if (__result && IsStatOnlyDrink(item) && CanAddFood(__instance.GetFoods(), item, out var _)) { __instance.EatFood(item); } } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Drink Slot (ConsumeItem). Reason: {arg}"); } } [HarmonyPatch(typeof(Player), "RemoveOneFood")] [HarmonyPrefix] public static bool RemoveOneFood_Prefix(Player __instance, ref List ___m_foods, ref bool __result) { try { if (___m_foods == null || ___m_foods.Count == 0) { __result = false; return false; } ___m_foods.Clear(); __instance.UpdateFood(0f, true); __result = true; return false; } catch (Exception arg) { FattyPlugin.Instance.Log.LogError($"Fatty Mod: Feature Lost - Puke Clears Stomach. Reason: {arg}"); return true; } } } } // ---- plugins/Fatty.dll :: Fatty.Configuration.CategoryManager ---- namespace Fatty.Configuration { public static class CategoryManager { public class FoodData { public string PrefabName { get; set; } public string InGameName { get; set; } public float Health { get; set; } public float Stamina { get; set; } public float Eitr { get; set; } public float Regen { get; set; } public float BurnTime { get; set; } public string Category { get; set; } public bool IsDrink { get; set; } } private class FoodDataFile { public int DataVersion { get; set; } public List Foods { get; set; } } public const int CurrentDataVersion = 5; public static readonly string[] BaseCategories = new string[6] { "Meat", "Vegetable", "Sweet", "Fish", "Eitr", "Unknown" }; public const string UnknownCategory = "Unknown"; private static readonly Dictionary _categoryByPrefab = new Dictionary(); private static readonly Dictionary _prefabByInGameName = new Dictionary(); private static readonly HashSet _hardcoded = new HashSet(); private static string ConfigPath => Path.Combine(Paths.ConfigPath, "Fatty_FoodData.json"); private static string BackupPath => ConfigPath + ".bak"; public static Dictionary> Categories { get; private set; } = new Dictionary>(); public static HashSet Drinks { get; private set; } = new HashSet(); public static List AllFoodData { get; private set; } = new List(); public static bool NeedsReclassify { get; internal set; } public static void Init() { if (File.Exists(ConfigPath)) { Load(); } else { GenerateDefaultCategories(); } EnsureBaseCategories(); } public static void EnsureBaseCategories() { string[] baseCategories = BaseCategories; foreach (string key in baseCategories) { if (!Categories.ContainsKey(key)) { Categories[key] = new List(); } } } public static void AddToCategory(string category, string prefabName) { if (string.IsNullOrEmpty(category) || string.IsNullOrEmpty(prefabName)) { return; } if (_categoryByPrefab.TryGetValue(prefabName, out var value)) { if (value == category) { return; } if (Categories.TryGetValue(value, out var value2)) { value2.Remove(prefabName); } } if (!Categories.TryGetValue(category, out var value3)) { value3 = new List(); Categories[category] = value3; } if (!value3.Contains(prefabName)) { value3.Add(prefabName); } _categoryByPrefab[prefabName] = category; } public static void MapInGameName(string inGameName, string prefabName) { if (!string.IsNullOrEmpty(inGameName) && !string.IsNullOrEmpty(prefabName)) { _prefabByInGameName[inGameName] = prefabName; } } public static string GetPrefabNameForInGameName(string inGameName) { if (string.IsNullOrEmpty(inGameName)) { return null; } if (!_prefabByInGameName.TryGetValue(inGameName, out var value)) { return null; } return value; } public static void SetDrink(string prefabName, bool isDrink) { if (!string.IsNullOrEmpty(prefabName)) { if (isDrink) { Drinks.Add(prefabName); } else { Drinks.Remove(prefabName); } } } public static bool IsDrinkPrefab(string prefabName) { if (!string.IsNullOrEmpty(prefabName)) { return Drinks.Contains(prefabName); } return false; } public static void Classify(string prefabName, string category, bool isDrink) { AddToCategory(category, prefabName); SetDrink(prefabName, isDrink); } public static bool IsHardcoded(string prefabName) { if (!string.IsNullOrEmpty(prefabName)) { return _hardcoded.Contains(prefabName); } return false; } public static void AddHardcodedCategory(string category, string prefabName) { if (!string.IsNullOrEmpty(prefabName)) { AddToCategory(category, prefabName); _hardcoded.Add(prefabName); } } public static void MarkHardcodedDrink(string prefabName) { if (!string.IsNullOrEmpty(prefabName)) { SetDrink(prefabName, isDrink: true); } } private static void ClearAll() { Categories.Clear(); Drinks.Clear(); _categoryByPrefab.Clear(); _prefabByInGameName.Clear(); } private static void GenerateDefaultCategories() { ClearAll(); AddAll("Meat", "CookedMeat", "NeckTailGrilled", "DeerMeatCooked", "SerpentMeatCooked", "LoxMeatCooked", "WolfMeatCooked", "BugMeatCooked"); AddAll("Vegetable", "Turnip", "Carrot", "Onion", "Mushroom", "MushroomYellow", "MushroomBlue"); AddAll("Sweet", "Raspberry", "Blueberry", "Cloudberry", "Honey", "MeadTasty"); AddAll("Fish", "FishRaw", "FishCooked"); AddAll("Eitr", "MushroomMagecap", "SeekerAspic", "MushroomOmelette", "MagicallyStuffedShroom", "YggdrasilPorridge", "MisthareSupreme"); EnsureBaseCategories(); string[] array = new string[3] { "MeadTasty", "MeadHealthMinor", "MeadStaminaMinor" }; foreach (string item in array) { Drinks.Add(item); } } private static void AddAll(string category, params string[] prefabNames) { foreach (string prefabName in prefabNames) { AddToCategory(category, prefabName); } } public static void Load() { try { ClearAll(); AllFoodData.Clear(); NeedsReclassify = false; int num = ReadFile(File.ReadAllText(ConfigPath)); foreach (FoodData allFoodDatum in AllFoodData) { if (allFoodDatum != null && !string.IsNullOrWhiteSpace(allFoodDatum.PrefabName)) { MapInGameName(allFoodDatum.InGameName, allFoodDatum.PrefabName); } } if (num < 5) { Migrate(num); return; } foreach (FoodData allFoodDatum2 in AllFoodData) { if (allFoodDatum2 != null && !string.IsNullOrWhiteSpace(allFoodDatum2.PrefabName)) { AddToCategory(string.IsNullOrWhiteSpace(allFoodDatum2.Category) ? "Unknown" : allFoodDatum2.Category, allFoodDatum2.PrefabName); SetDrink(allFoodDatum2.PrefabName, allFoodDatum2.IsDrink); } } FattyPlugin.Instance.Log.LogInfo($"Successfully loaded Fatty_FoodData.json with {AllFoodData.Count} items ({Drinks.Count} drinks)."); } catch (Exception ex) { FattyPlugin.Instance.Log.LogError("Error loading FoodData JSON: " + ex.Message + ". Falling back to defaults."); AllFoodData.Clear(); GenerateDefaultCategories(); } } private static int ReadFile(string json) { if (json.TrimStart(Array.Empty()).StartsWith("[")) { AllFoodData = JsonConvert.DeserializeObject>(json) ?? new List(); return 1; } FoodDataFile foodDataFile = JsonConvert.DeserializeObject(json); AllFoodData = foodDataFile?.Foods ?? new List(); return foodDataFile?.DataVersion ?? 1; } private static void Migrate(int fromVersion) { try { File.Copy(ConfigPath, BackupPath, overwrite: true); FattyPlugin.Instance.Log.LogWarning($"Fatty: migrating Fatty_FoodData.json from data version {fromVersion} to {5}. " + "Every food will be re-categorized, so any hand-edited categories WILL be overwritten - your previous file has been backed up to Fatty_FoodData.json.bak."); } catch (Exception ex) { FattyPlugin.Instance.Log.LogError("Fatty: could not back up Fatty_FoodData.json before migrating (" + ex.Message + "). Migrating anyway."); } Categories.Clear(); Drinks.Clear(); _categoryByPrefab.Clear(); EnsureBaseCategories(); NeedsReclassify = true; } public static void RequestRebuild() { try { if (File.Exists(ConfigPath)) { File.Copy(ConfigPath, BackupPath, overwrite: true); } } catch (Exception ex) { FattyPlugin.Instance.Log.LogError("Fatty: could not back up Fatty_FoodData.json before rebuilding (" + ex.Message + "). Rebuilding anyway."); } FattyPlugin.Instance.Log.LogWarning("Fatty: 'Rebuild Food Data' is on - every food will be re-categorized from scratch this launch and any hand-edited categories WILL be overwritten. Your previous file has been backed up to Fatty_FoodData.json.bak. The setting turns itself back off once the rebuild completes."); Categories.Clear(); Drinks.Clear(); _categoryByPrefab.Clear(); EnsureBaseCategories(); NeedsReclassify = true; } public static void Save() { try { string contents = JsonConvert.SerializeObject((object)new FoodDataFile { DataVersion = 5, Foods = AllFoodData }, (Formatting)1); File.WriteAllText(ConfigPath, contents); FattyPlugin.Instance.Log.LogInfo("Successfully updated Fatty_FoodData.json"); } catch (Exception ex) { FattyPlugin.Instance.Log.LogError("Error saving FoodData JSON: " + ex.Message); } } public static string GetCategoryForPrefab(string prefabName) { if (string.IsNullOrEmpty(prefabName)) { return "Unknown"; } if (!_categoryByPrefab.TryGetValue(prefabName, out var value)) { return "Unknown"; } return value; } } } // ---- plugins/Fatty.dll :: Fatty.Configuration.ConfigManager ---- namespace Fatty.Configuration { public static class ConfigManager { private static readonly ConfigSync configSync = new ConfigSync("com.wubarrk.fatty") { DisplayName = "Fatty", CurrentVersion = "1.1.2", MinimumRequiredVersion = "1.1.2" }; public static ConfigEntry serverConfigLocked; public static ConfigEntry noFoodDecay; public static ConfigEntry maxFoodStacks; public static ConfigEntry rebuildFoodData; public static ConfigEntry debugMode; public static ConfigEntry stackDiminishingReturnStep1; public static ConfigEntry stackDiminishingReturnStep2; public static ConfigEntry stackDiminishingReturnStep3; public static ConfigEntry scaleRegenWithPool; public static ConfigEntry maxRegenScale; public static ConfigEntry foodDurationMultiplier; public static ConfigEntry enableMilestones; public static ConfigEntry milestoneTier1Count; public static ConfigEntry milestoneTier2Count; public static ConfigEntry milestoneTier3Count; public static ConfigEntry milestoneTier1Hp; public static ConfigEntry milestoneTier2HpRegen; public static ConfigEntry milestoneTier3Hp; public static ConfigEntry milestoneTotalCap; public static ConfigEntry milestoneTier1CarryWeight; public static ConfigEntry milestoneTier2MeatSkill; public static ConfigEntry milestoneTier3CarryWeight; public static ConfigEntry milestoneCarryWeightCap; public static ConfigEntry milestoneTier1Stamina; public static ConfigEntry milestoneTier2StaminaRegen; public static ConfigEntry milestoneTier3Stamina; public static ConfigEntry milestoneStaminaCap; public static ConfigEntry milestoneTier1Eitr; public static ConfigEntry milestoneTier2EitrRegen; public static ConfigEntry milestoneTier3Eitr; public static ConfigEntry milestoneEitrCap; public static ConfigEntry milestoneTier1SwimSpeed; public static ConfigEntry milestoneTier2FishSwimStamina; public static ConfigEntry milestoneTier3SwimSpeed; public static ConfigEntry milestoneTier1Regen; public static ConfigEntry milestoneTier2Regen; public static ConfigEntry milestoneTier3Regen; public static ConfigEntry fatnessTier1Count; public static ConfigEntry fatnessTier2Count; public static ConfigEntry fatnessTier3Count; public static ConfigEntry enableDrinkBuffs; public static ConfigEntry drinkBuffSkillLevels; public static ConfigEntry drinkBuffEitrRegen; public static ConfigEntry drinkBuffStaminaRegen; public static ConfigEntry drinkBuffMaxHealth; public static ConfigEntry drinkSlotDefaultDuration; public static ConfigEntry enableDrunk; public static ConfigEntry drunkStacksRequired; public static ConfigEntry drunkDamageBonus; public static ConfigEntry drunkStaggerResist; public static ConfigEntry drunkBlockingPenalty; public static ConfigEntry drunkBlockStaminaPenalty; public static ConfigEntry enableSynergies; public static ConfigEntry balancedDietRegen; public static ConfigEntry sugarRushSpeedBonus; public static ConfigEntry fishermanSwimStaminaReduction; public static ConfigEntry lumberjackCarryWeight; public static ConfigEntry lumberjackWoodcuttingDamage; public static ConfigEntry foodBreakdownPanelX; public static ConfigEntry foodBreakdownPanelY; public static ConfigEntry foodBreakdownPanelWidth; public static ConfigEntry foodBreakdownPanelHeight; public static ConfigEntry useSegmentedStackBar; public static ConfigEntry breakdownToggleKey; public static ConfigEntry feastLedgerFontSizeDelta; public static void Init(ConfigFile config) { serverConfigLocked = config.Bind("1 - General", "Lock Configuration", defaultValue: true, "If on, the configuration is locked and can be changed by server admins only."); configSync.AddLockingConfigEntry(serverConfigLocked); noFoodDecay = BindSynced(config, "2 - Core", "No Food Decay", defaultValue: true, "Vanilla fades a food's stats to nothing as its timer runs down. ON = you keep 100% of a food's stats until its timer expires, then lose them all at once. OFF = vanilla's gradual decay curve."); maxFoodStacks = BindSynced(config, "2 - Core", "Max Food Stacks", 4, "How many times the same food can be eaten at once. Vanilla = 1 (no stacking), 4 = default. Each extra stack is worth less - see section 3.", 1, 20); stackDiminishingReturnStep1 = BindSynced(config, "3 - Stacking", "Stack 2 Multiplier", 0.3f, "Fraction of a food's stats granted by its 2nd stack. 1.0 = full value, 0.3 = 30% (default), 0.0 = nothing. The 1st stack is always 100%.", 0f, 1f); stackDiminishingReturnStep2 = BindSynced(config, "3 - Stacking", "Stack 3 Multiplier", 0.2f, "Fraction of a food's stats granted by its 3rd stack. 0.2 = 20% (default).", 0f, 1f); stackDiminishingReturnStep3 = BindSynced(config, "3 - Stacking", "Stack 4+ Multiplier", 0.1f, "Fraction of a food's stats granted by the 4th and every further stack. 0.1 = 10% (default).", 0f, 1f); enableSynergies = BindSynced(config, "4 - Synergies", "Enable Synergies", defaultValue: true, "Enables the dietary synergy status effects (Balanced Diet, Sugar Rush, Fisherman's Friend, Lumberjack's Feast)."); balancedDietRegen = BindSynced(config, "4 - Synergies", "Balanced Diet Regen Multiplier", 2f, "Balanced Diet (1 Meat + 1 Veggie): MULTIPLIER on vanilla health & stamina regen. 1.0 = vanilla, 2.0 = 2x vanilla / +100% (default).", 1f, 10f); sugarRushSpeedBonus = BindSynced(config, "4 - Synergies", "Sugar Rush Speed Multiplier", 2f, "Sugar Rush (3 Sweets + 1 Drink): MULTIPLIER on vanilla movement speed. 1.0 = vanilla, 2.0 = 2x / +100% (default).", 1f, 5f); fishermanSwimStaminaReduction = BindSynced(config, "4 - Synergies", "Fisherman Swim Stamina Reduction", 0.8f, "Fisherman's Friend (2 Fish + 1 Veggie): FRACTION of vanilla swim stamina drain removed. 0.0 = vanilla drain, 0.80 = 80% of the drain gone (default), 1.0 = free swimming.", 0f, 1f); lumberjackCarryWeight = BindSynced(config, "4 - Synergies", "Lumberjack Carry Weight Bonus", 175f, "Lumberjack's Feast (3 Meat + 1 Drink): FLAT carry weight added on top of vanilla. 0 = vanilla, 175 = +175 units (default).", 0f, 1000f); lumberjackWoodcuttingDamage = BindSynced(config, "4 - Synergies", "Lumberjack Woodcutting Damage", 0.45f, "Lumberjack's Feast (3 Meat + 1 Drink): FRACTION added to vanilla woodcutting damage. 0.0 = vanilla, 0.45 = +45% (default).", 0f, 5f); scaleRegenWithPool = BindSynced(config, "6 - Regen", "Scale Regen With Pool", defaultValue: true, "Vanilla regenerates stamina & eitr at a FLAT rate that ignores bar size, so any mod that enlarges your pools makes them refill slower. ON = Fatty scales the rate with the pool, holding refill TIME at vanilla parity. OFF = vanilla's flat rate."); maxRegenScale = BindSynced(config, "6 - Regen", "Max Regen Scale", 2f, "Safety cap on the above, as a MULTIPLIER of vanilla's regen rate. 1.0 = never speed up at all, 2.0 = at most 2x vanilla (default). Fatty never scales BELOW 1.0, so regen can never end up worse than vanilla.", 1f, 20f); foodDurationMultiplier = BindSynced(config, "7 - Duration", "Food Duration Multiplier", 2f, "MULTIPLIER on how long food lasts. 1.0 = vanilla duration, 2.0 = food lasts twice as long (default). IGNORED while blacks7ar's FoodDurationMultiplier is installed - that mod's value is used instead, so the two can never multiply together.", 0.1f, 10f); enableMilestones = BindSynced(config, "8 - Milestones", "Enable Milestones", defaultValue: true, "Eating a lot of a food category over the life of your character permanently raises a stat matched to that category's flavour: Meat->carry weight, Sweet->stamina, Eitr->eitr, Fish->swim speed, Vegetable->all-round regen. Total food eaten across EVERY category (a separate 'Fatness' tally) raises base max health. Small, infrequent awards that reward a broad diet."); milestoneTier1Count = BindSynced(config, "8 - Milestones", "Tier 1 Count", 50, "Items of a SINGLE category that must be eaten to earn that category's 1st milestone. Default 50.", 1, 100000); milestoneTier2Count = BindSynced(config, "8 - Milestones", "Tier 2 Count", 200, "Items of a single category needed for its 2nd milestone. Default 200. Should be higher than Tier 1 Count.", 1, 100000); milestoneTier3Count = BindSynced(config, "8 - Milestones", "Tier 3 Count", 500, "Items of a single category needed for its 3rd milestone. Default 500. Should be higher than Tier 2 Count.", 1, 100000); fatnessTier1Count = BindSynced(config, "8 - Milestones", "Fatness Tier 1 Count", 150, "TOTAL items eaten across EVERY category (including unrecognized food) for Fatness tier 1. Separate from the per-category thresholds above since this accumulates ~5x faster. Default 150.", 1, 500000); fatnessTier2Count = BindSynced(config, "8 - Milestones", "Fatness Tier 2 Count", 600, "Total items eaten across every category for Fatness tier 2. Default 600.", 1, 500000); fatnessTier3Count = BindSynced(config, "8 - Milestones", "Fatness Tier 3 Count", 1500, "Total items eaten across every category for Fatness tier 3. Default 1500.", 1, 500000); milestoneTier1Hp = BindSynced(config, "8 - Milestones", "Fatness Tier 1 Health", 5f, "FLAT base max health added by Fatness tier 1 (total food eaten, all categories). 5 = +5 on top of vanilla's 25 base (default).", 0f, 500f); milestoneTier2HpRegen = BindSynced(config, "8 - Milestones", "Fatness Tier 2 Health Regen", 0.05f, "Fatness tier 2 ALTERNATES to a permanent, always-on health regen MULTIPLIER instead of flat health. 1.0 = vanilla, so this value is added to 1.0 - 0.05 = +5% (default).", 0f, 1f); milestoneTier3Hp = BindSynced(config, "8 - Milestones", "Fatness Tier 3 Health", 10f, "Flat base max health added by Fatness tier 3 (back to the flat reward). Default +10.", 0f, 500f); milestoneTotalCap = BindSynced(config, "8 - Milestones", "Fatness Health Cap", 50f, "Hard cap on total base health from the Fatness axis' flat tiers (1 and 3 only - tier 2 is a regen multiplier, uncapped separately). 50 at the defaults (5+10=15 short of it).", 0f, 1000f); milestoneTier1CarryWeight = BindSynced(config, "8 - Milestones", "Meat Tier 1 Carry Weight", 10f, "FLAT carry weight added by Meat's 1st milestone. Default +10.", 0f, 500f); milestoneTier2MeatSkill = BindSynced(config, "8 - Milestones", "Meat Tier 2 Skill Bonus", 10f, "Meat tier 2 ALTERNATES to a permanent, always-on Swords + Axes skill level bonus instead of carry weight, matching Warrior's Draught's own flavour. Default +10 levels.", 0f, 100f); milestoneTier3CarryWeight = BindSynced(config, "8 - Milestones", "Meat Tier 3 Carry Weight", 20f, "Flat carry weight added by Meat's 3rd milestone (back to the flat reward). Default +20.", 0f, 500f); milestoneCarryWeightCap = BindSynced(config, "8 - Milestones", "Meat Carry Weight Cap", 40f, "Hard cap on total carry weight from Meat's flat tiers (1 and 3 only). Default 40, which leaves 10 of headroom over the 30 those tiers actually grant (10+20) - raise a tier's value without also raising this and the extra is silently clipped.", 0f, 1000f); milestoneTier1Stamina = BindSynced(config, "8 - Milestones", "Sweet Tier 1 Stamina", 5f, "FLAT base stamina added by Sweet's 1st milestone. Default +5.", 0f, 200f); milestoneTier2StaminaRegen = BindSynced(config, "8 - Milestones", "Sweet Tier 2 Stamina Regen", 0.05f, "Sweet tier 2 ALTERNATES to a permanent, always-on stamina regen MULTIPLIER instead of flat stamina. Added to 1.0, so 0.05 = +5% (default).", 0f, 1f); milestoneTier3Stamina = BindSynced(config, "8 - Milestones", "Sweet Tier 3 Stamina", 10f, "Flat base stamina added by Sweet's 3rd milestone (back to the flat reward). Default +10.", 0f, 200f); milestoneStaminaCap = BindSynced(config, "8 - Milestones", "Sweet Stamina Cap", 20f, "Hard cap on total base stamina from Sweet's flat tiers (1 and 3 only). Default 20, which leaves 5 of headroom over the 15 those tiers actually grant (5+10).", 0f, 500f); milestoneTier1Eitr = BindSynced(config, "8 - Milestones", "Eitr Tier 1 Eitr", 5f, "FLAT max eitr added by Eitr's 1st milestone. Vanilla has no base eitr at all outside food, so this is meaningful even at low values. Default +5.", 0f, 200f); milestoneTier2EitrRegen = BindSynced(config, "8 - Milestones", "Eitr Tier 2 Eitr Regen", 0.05f, "Eitr tier 2 ALTERNATES to a permanent, always-on eitr regen MULTIPLIER instead of flat eitr. Added to 1.0, so 0.05 = +5% (default).", 0f, 1f); milestoneTier3Eitr = BindSynced(config, "8 - Milestones", "Eitr Tier 3 Eitr", 10f, "Flat max eitr added by Eitr's 3rd milestone (back to the flat reward). Default +10.", 0f, 200f); milestoneEitrCap = BindSynced(config, "8 - Milestones", "Eitr Eitr Cap", 20f, "Hard cap on total max eitr from Eitr's flat tiers (1 and 3 only). Default 20, which leaves 5 of headroom over the 15 those tiers actually grant (5+10).", 0f, 500f); milestoneTier1SwimSpeed = BindSynced(config, "8 - Milestones", "Fish Tier 1 Swim Speed", 0.02f, "Fish's 1st milestone: permanent, always-on swim speed bonus (additive, like Seafarer's Draught's own modifier). 0.02 = +2% (default).", 0f, 1f); milestoneTier2FishSwimStamina = BindSynced(config, "8 - Milestones", "Fish Tier 2 Swim Stamina Reduction", 0.1f, "Fish tier 2 ALTERNATES to a permanent, always-on FRACTION of swim stamina drain removed instead of more swim speed, matching Seafarer's/Fisherman's Friend's own flavour. 0.10 = 10% less drain (default).", 0f, 1f); milestoneTier3SwimSpeed = BindSynced(config, "8 - Milestones", "Fish Tier 3 Swim Speed", 0.06f, "Fish's 3rd milestone swim speed bonus (back to the flat reward, adds to tier 1's). Default +6%.", 0f, 1f); milestoneTier1Regen = BindSynced(config, "8 - Milestones", "Vegetable Tier 1 Regen Multiplier", 1.02f, "Vegetable's 1st milestone: permanent, always-on MULTIPLIER on health, stamina AND eitr regen together. 1.0 = vanilla, 1.02 = +2% (default). Each tier is its own always-on effect and multipliers compound, so earning tiers 1-3 at the defaults gives roughly +12% overall.", 1f, 2f); milestoneTier2Regen = BindSynced(config, "8 - Milestones", "Vegetable Tier 2 Regen Multiplier", 1.04f, "Vegetable's 2nd milestone regen multiplier. Default 1.04 (+4%).", 1f, 2f); milestoneTier3Regen = BindSynced(config, "8 - Milestones", "Vegetable Tier 3 Regen Multiplier", 1.06f, "Vegetable's 3rd milestone regen multiplier. Default 1.06 (+6%).", 1f, 2f); enableDrinkBuffs = BindSynced(config, "9 - Drink Buffs", "Enable Drink Buffs", defaultValue: true, "Whatever holds your drink slot grants a buff chosen by its food category, lasting exactly as long as the drink does."); drinkBuffSkillLevels = BindSynced(config, "9 - Drink Buffs", "Skill Level Bonus", 15f, "FLAT temporary skill levels granted by a drink buff (each buff boosts two related skills). 0 = vanilla, 15 = +15 levels (default).", 0f, 100f); drinkBuffEitrRegen = BindSynced(config, "9 - Drink Buffs", "Arcane Eitr Regen Multiplier", 1.5f, "Arcane Draught (eitr drinks): MULTIPLIER on vanilla eitr regen. 1.0 = vanilla, 1.5 = +50% (default).", 1f, 10f); drinkBuffStaminaRegen = BindSynced(config, "9 - Drink Buffs", "Reveler Stamina Regen Multiplier", 1.5f, "Reveler's Draught (sweet drinks): MULTIPLIER on vanilla stamina regen. 1.0 = vanilla, 1.5 = +50% (default).", 1f, 10f); drinkBuffMaxHealth = BindSynced(config, "9 - Drink Buffs", "Max Health Per Drink", 5f, "FLAT bonus max health while a buffing drink is active. 0 = none, 5 = +5 (default). Follows the same stacking diminishing returns as food, so extra stacks add less.", 0f, 200f); drinkSlotDefaultDuration = BindSynced(config, "9 - Drink Buffs", "Default Drink Duration", 600f, "MINIMUM seconds a drink holds the drink slot. A health or stamina mead's own effect is over in seconds, which would leave its draught blinking on and off, so drinks are held for at least this long. Drinks that already last longer (the resistance meads) keep their own timer, and food with a real burn time is unaffected. This does NOT let you drink more often - vanilla's cooldown between meads is untouched. 600 = 10 minutes (default). Scaled by the Food Duration Multiplier like everything else.", 10f, 7200f); enableDrunk = BindSynced(config, "9 - Drink Buffs", "Enable Drunk", defaultValue: true, "Drinking several Tasty Meads at once makes you Drunk: harder hitting and harder to stagger, but your guard suffers. See 'Drunk Stacks Required' for how many."); drunkStacksRequired = BindSynced(config, "9 - Drink Buffs", "Drunk Stacks Required", 6, "How many Tasty Meads must be in your drink slot at once to get Drunk. Capped by Max Food Stacks, so it can never ask for more meads than your stomach can hold. 6 = default, which sits deliberately ABOVE the default Max Food Stacks of 4 - the cap pulls it back down, so Drunk means 'fill your drink slot with mead' at any stack setting rather than a fixed count that gets easy once you raise the stack limit.", 1, 20); drunkDamageBonus = BindSynced(config, "9 - Drink Buffs", "Drunk Damage Bonus", 0.15f, "Drunk: FRACTION added to vanilla damage on all attacks. 0.0 = vanilla, 0.15 = +15% (default).", 0f, 2f); drunkStaggerResist = BindSynced(config, "9 - Drink Buffs", "Drunk Stagger Resistance", 0.25f, "Drunk: FRACTION of incoming stagger ignored. 0.0 = vanilla, 0.25 = 25% less stagger (default), 1.0 = unstaggerable.", 0f, 1f); drunkBlockingPenalty = BindSynced(config, "9 - Drink Buffs", "Drunk Blocking Penalty", 20f, "Drunk: FLAT blocking skill levels LOST. 0 = no penalty, 20 = -20 levels (default).", 0f, 100f); drunkBlockStaminaPenalty = BindSynced(config, "9 - Drink Buffs", "Drunk Block Stamina Penalty", 0.5f, "Drunk: FRACTION added to the stamina cost of blocking. 0.0 = vanilla, 0.5 = +50% (default).", 0f, 5f); rebuildFoodData = config.Bind("2 - Core", "Rebuild Food Data", defaultValue: false, "One-shot repair switch. ON = re-derive every food's category and drink flag from scratch on the next launch, then turn itself back off. Your existing Fatty_FoodData.json is backed up to Fatty_FoodData.json.bak first, and ANY hand-edited categories in it are overwritten. Use this when a food is filed under the wrong category."); debugMode = config.Bind("5 - Debug", "Enable Debug Mode", defaultValue: false, "Verbose logging: what each food was categorized as, what you ate, and milestone awards. Slot-block reasons are always logged regardless of this setting."); foodBreakdownPanelX = BindLocal(config, "10 - HUD Panels", "Food Breakdown Panel X", 1f, "Saved screen X position (pixels from the left edge) of the Feast Ledger panel. Updated automatically when you drag it. Defaults to the top-left corner - drag it wherever you like, it remembers."); foodBreakdownPanelY = BindLocal(config, "10 - HUD Panels", "Food Breakdown Panel Y", 2f, "Saved screen Y position (pixels from the top edge) of the Feast Ledger panel. Updated automatically when you drag it."); foodBreakdownPanelWidth = BindLocalRanged(config, "10 - HUD Panels", "Food Breakdown Panel Width", 622f, "Saved width of the Feast Ledger panel. Updated automatically when you resize it. Range matches the gilt frame's own minimum/maximum - below the minimum the corner ornaments would overlap.", 340f, 900f); foodBreakdownPanelHeight = BindLocalRanged(config, "10 - HUD Panels", "Food Breakdown Panel Height", 700f, "Saved height of the Feast Ledger panel. Updated automatically when you resize it.", 280f, 700f); useSegmentedStackBar = BindLocal(config, "10 - HUD Panels", "Use Segmented Stack Bar", defaultValue: false, "How a stacked food slot shows its individual items. ON = a thin segmented bar under the icon, one segment per stacked item, dimming as each expires. OFF = a simple 'xN' count badge instead (default). Only one shows at a time."); breakdownToggleKey = config.Bind("10 - HUD Panels", "Breakdown Panel Hotkey", new KeyboardShortcut(KeyCode.F8), "Opens/closes the Feast Ledger panel from the keyboard, alongside its own on-screen toggle button. Default F8."); feastLedgerFontSizeDelta = config.Bind("10 - HUD Panels", "Feast Ledger Font Size Delta", 2, new ConfigDescription("FLAT points added to every Feast Ledger text size. 0 = the theme's own base sizes, 2 = +2pt (default, easier to read), negative = smaller. Takes effect on the next panel open (styles are built once).", new AcceptableValueRange(-6, 12))); } private static ConfigEntry BindSynced(ConfigFile config, string section, string key, T defaultValue, string description) { ConfigEntry configEntry = config.Bind(section, key, defaultValue, description); configSync.AddConfigEntry(configEntry); return configEntry; } private static ConfigEntry BindLocal(ConfigFile config, string section, string key, float defaultValue, string description) { return config.Bind(section, key, defaultValue, description); } private static ConfigEntry BindLocal(ConfigFile config, string section, string key, bool defaultValue, string description) { return config.Bind(section, key, defaultValue, description); } private static ConfigEntry BindLocalRanged(ConfigFile config, string section, string key, float defaultValue, string description, float min, float max) { return config.Bind(section, key, defaultValue, new ConfigDescription(description, new AcceptableValueRange(min, max))); } private static ConfigEntry BindSynced(ConfigFile config, string section, string key, float defaultValue, string description, float min, float max) { return BindRanged(config, section, key, defaultValue, description, new AcceptableValueRange(min, max)); } private static ConfigEntry BindSynced(ConfigFile config, string section, string key, int defaultValue, string description, int min, int max) { return BindRanged(config, section, key, defaultValue, description, new AcceptableValueRange(min, max)); } private static ConfigEntry BindRanged(ConfigFile config, string section, string key, T defaultValue, string description, AcceptableValueBase range) { ConfigEntry configEntry = config.Bind(section, key, defaultValue, new ConfigDescription(description, range)); configSync.AddConfigEntry(configEntry); return configEntry; } } } // ---- plugins/Fatty.dll :: Fatty.Compat.FoodDuration ---- namespace Fatty.Compat { public static class FoodDuration { public static float GetMultiplier() { if (FoodDurationMultiplierCompat.IsInstalled && !FoodDurationMultiplierCompat.ResolveFailed) { float durationMultiplier = FoodDurationMultiplierCompat.GetDurationMultiplier(); if (durationMultiplier > 0f) { return durationMultiplier; } } float value = ConfigManager.foodDurationMultiplier.Value; if (!(value > 0f)) { return 1f; } return value; } public static float GetHealthBenefit() { return FoodDurationMultiplierCompat.GetBenefit(FoodDurationMultiplierCompat.Benefit.Health); } public static float GetStaminaBenefit() { return FoodDurationMultiplierCompat.GetBenefit(FoodDurationMultiplierCompat.Benefit.Stamina); } public static float GetEitrBenefit() { return FoodDurationMultiplierCompat.GetBenefit(FoodDurationMultiplierCompat.Benefit.Eitr); } } } // ---- plugins/Fatty.dll :: Fatty.Compat.FoodDurationMultiplierCompat ---- namespace Fatty.Compat { public static class FoodDurationMultiplierCompat { public enum Benefit { Health, Stamina, Eitr } private const string PluginGuid = "blacks7ar.FoodDurationMultiplier"; private static bool _initialized; private static bool _isInstalled; private static Type _pluginType; private static PropertyInfo _valueProperty; private static object _configEntry; private static bool _resolveFailed; private static readonly object[] _benefitEntries = new object[3]; private static readonly PropertyInfo[] _benefitProperties = new PropertyInfo[3]; private static readonly bool[] _benefitFailed = new bool[3]; private static object _degradeEntry; private static PropertyInfo _degradeProperty; private static bool _degradeResolved; private static readonly string[] BenefitFields = new string[3] { "_foodHealthBenefits", "_foodStaminaBenefits", "_foodEitrBenefits" }; public static bool IsInstalled => _isInstalled; public static bool ResolveFailed => _resolveFailed; public static void Initialize() { if (_initialized) { return; } _initialized = true; try { if (Chainloader.PluginInfos.TryGetValue("blacks7ar.FoodDurationMultiplier", out var value)) { if (value.Instance == null) { FattyPlugin.Instance.Log.LogInfo("Found blacks7ar.FoodDurationMultiplier but it has not initialized yet; using Fatty's own food duration multiplier."); return; } _pluginType = value.Instance.GetType(); _isInstalled = true; FattyPlugin.Instance.Log.LogInfo("Found blacks7ar.FoodDurationMultiplier, deferring food duration to it."); } } catch (Exception ex) { _isInstalled = false; _pluginType = null; FattyPlugin.Instance.Log.LogWarning("Could not initialize blacks7ar.FoodDurationMultiplier compatibility: " + ex.Message + ". Using Fatty's own multiplier."); } } public static float GetDurationMultiplier() { if (!_isInstalled || _resolveFailed) { return 1f; } try { if (_valueProperty == null) { _configEntry = _pluginType.GetField("_durationMultiplier", BindingFlags.Static | BindingFlags.Public)?.GetValue(null); _valueProperty = _configEntry?.GetType().GetProperty("Value"); if (_valueProperty == null) { _resolveFailed = true; FattyPlugin.Instance.Log.LogWarning("Could not read blacks7ar.FoodDurationMultiplier._durationMultiplier; falling back to Fatty's own multiplier."); return 1f; } } return (float)_valueProperty.GetValue(_configEntry); } catch (Exception ex) { _resolveFailed = true; FattyPlugin.Instance.Log.LogWarning("Failed to read blacks7ar.FoodDurationMultiplier._durationMultiplier: " + ex.Message); return 1f; } } public static float GetBenefit(Benefit which) { if (!_isInstalled) { return 1f; } if (which < Benefit.Health || (int)which >= BenefitFields.Length || _benefitFailed[(int)which]) { return 1f; } try { if (IsDegradeEnabled()) { return 1f; } if (_benefitProperties[(int)which] == null) { FieldInfo field = _pluginType.GetField(BenefitFields[(int)which], BindingFlags.Static | BindingFlags.Public); _benefitEntries[(int)which] = field?.GetValue(null); _benefitProperties[(int)which] = _benefitEntries[(int)which]?.GetType().GetProperty("Value"); if (_benefitProperties[(int)which] == null) { _benefitFailed[(int)which] = true; FattyPlugin.Instance.Log.LogWarning("Could not read blacks7ar.FoodDurationMultiplier." + BenefitFields[(int)which] + "; treating that benefit as 100%."); return 1f; } } float num = (float)_benefitProperties[(int)which].GetValue(_benefitEntries[(int)which]); if (float.IsNaN(num) || float.IsInfinity(num) || num < 0f) { return 1f; } return num / 100f; } catch (Exception ex) { _benefitFailed[(int)which] = true; FattyPlugin.Instance.Log.LogWarning("Failed to read blacks7ar.FoodDurationMultiplier." + BenefitFields[(int)which] + ": " + ex.Message); return 1f; } } private static bool IsDegradeEnabled() { if (!_degradeResolved) { _degradeResolved = true; _degradeEntry = _pluginType.GetField("_enableDegrade", BindingFlags.Static | BindingFlags.Public)?.GetValue(null); _degradeProperty = _degradeEntry?.GetType().GetProperty("Value"); } if (_degradeProperty == null) { return false; } object value = _degradeProperty.GetValue(_degradeEntry); if (value != null) { return Convert.ToInt32(value) != 0; } return false; } } }