// Consolidated decompiled source — Wubarrk-Fatty v1.1.0 // 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 System.Text.RegularExpressions; using Fatty.Progression; using BepInEx.Bootstrap; using System.Text; using UnityEngine.UI; using Fatty.Synergies; 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.0")] public class FattyPlugin : BaseUnityPlugin { public const string PluginGUID = "wubarrk.fatty"; public const string PluginName = "Fatty"; public const string PluginVersion = "1.1.0"; 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.0 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); } 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); } 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 static float _pristineBaseHp = 25f; private static bool _capturedBase; public static void OnSpawned(Player player) { if (!(player == null)) { if (!_capturedBase) { _pristineBaseHp = player.m_baseHP; _capturedBase = true; } Apply(player, announce: false); } } 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); dictionary[category] = num + 1; MilestoneStore.Save(player, dictionary); if (MilestoneStore.EarnsMilestones(category)) { int num2 = TiersEarned(num); int num3 = TiersEarned(num + 1); if (num3 > num2) { Apply(player, announce: true, category, num3); } } } public static void Apply(Player player, bool announce, string justEarnedCategory = null, int tier = 0) { if (player == null) { return; } if (!_capturedBase) { _pristineBaseHp = player.m_baseHP; _capturedBase = true; } float num = (ConfigManager.enableMilestones.Value ? TotalBonus(player) : 0f); player.m_baseHP = _pristineBaseHp + num; if (announce && !string.IsNullOrEmpty(justEarnedCategory)) { float num2 = HpForTier(tier); player.Message(MessageHud.MessageType.Center, $"You're getting fat! {justEarnedCategory} {tier} - +{num2:0.#} max health"); if (ConfigManager.debugMode.Value) { FattyPlugin.Instance.Log.LogInfo($"[Fatty] Milestone: {justEarnedCategory} tier {tier}, +{num2} HP (total bonus {num}, base HP now {player.m_baseHP})."); } } } public static float TotalBonus(Player player) { Dictionary counts = MilestoneStore.Load(player); float num = 0f; string[] baseCategories = CategoryManager.BaseCategories; foreach (string category in baseCategories) { if (MilestoneStore.EarnsMilestones(category)) { int num2 = TiersEarned(MilestoneStore.Get(counts, category)); for (int j = 1; j <= num2; j++) { num += HpForTier(j); } } } return Mathf.Min(num, Mathf.Max(0f, ConfigManager.milestoneTotalCap.Value)); } 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; } 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, 2 => ConfigManager.milestoneTier2Hp.Value, 3 => ConfigManager.milestoneTier3Hp.Value, _ => 0f, }; } public static string Describe(Player player) { if (player == null) { return "no player"; } Dictionary counts = MilestoneStore.Load(player); List list = new List(); string[] baseCategories = CategoryManager.BaseCategories; foreach (string text in baseCategories) { int num = MilestoneStore.Get(counts, text); if (num > 0) { list.Add($"{text}={num}({TiersEarned(num)} tier(s))"); } } if (list.Count != 0) { return string.Format("{0} -> +{1:0.#} HP", string.Join(", ", list), TotalBonus(player)); } return "nothing eaten yet"; } } } // ---- 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 TitleHeight = 24f; private const float ToggleButtonWidth = 26f; private const float ClosedWidth = 96f; private const float GripSize = 14f; private const float MinWidth = 200f; private const float MaxWidth = 800f; private const float MinHeight = 150f; private const float MaxHeight = 600f; 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 _panelBoxStyle; private static GUIStyle _titleBoxStyle; private static GUIStyle _titleLabelStyle; private static GUIStyle _toggleBtnStyle; private static GUIStyle _sectionStyle; private static GUIStyle _foldoutStyle; private static GUIStyle _labelStyle; private static GUIStyle _smallLabelStyle; private static GUIStyle _boldLabelStyle; private static GUIStyle _gripStyle; private static readonly Color BgColor = new Color(0f, 0f, 0f, 0.72f); private static readonly Color TitleBgColor = new Color(0.16f, 0.11f, 0.05f, 0.96f); private static readonly Color GoldText = new Color(1f, 0.86f, 0.35f, 1f); private static readonly Color GoldDim = new Color(0.85f, 0.72f, 0.35f, 1f); private static readonly Color GoldTrim = new Color(0.82f, 0.66f, 0.22f, 0.9f); private static readonly Color BodyText = new Color(0.95f, 0.9f, 0.8f, 1f); 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, 96f, 24f)); 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, 200f, 800f); float height = Mathf.Clamp(ConfigManager.foodBreakdownPanelHeight.Value, 150f, 600f); _rect = new Rect(ConfigManager.foodBreakdownPanelX.Value, ConfigManager.foodBreakdownPanelY.Value, width, height); } } private static void DrawWindow(int id) { float num = (_open ? _rect.width : 96f); float num2 = (_open ? _rect.height : 24f); GUI.Box(new Rect(0f, 0f, num, num2), GUIContent.none, _panelBoxStyle); GUI.Box(new Rect(0f, 0f, num, 24f), GUIContent.none, _titleBoxStyle); GUI.Label(new Rect(8f, 0f, num - 26f - 12f, 24f), _open ? "The Feast Ledger" : "FEAST", _titleLabelStyle); if (GUI.Button(new Rect(num - 26f, 1f, 24f, 22f), _open ? "▼" : "▲", _toggleBtnStyle)) { _open = !_open; } GUI.DragWindow(new Rect(0f, 0f, num - 26f, 24f)); if (_open) { GUILayout.BeginArea(new Rect(8f, 28f, num - 16f, num2 - 24f - 12f)); _scroll = GUILayout.BeginScrollView(_scroll); DrawStomachSection(); GUILayout.Space(8f); DrawMilestoneSection(); GUILayout.Space(8f); DrawFeastLogSection(); GUILayout.Space(4f); GUILayout.EndScrollView(); GUILayout.EndArea(); } } private static void DrawStomachSection() { GUILayout.Label("Stomach", _sectionStyle); if (HudPatches.GroupCount == 0) { GUILayout.Label("An empty stomach is no way to live.", _labelStyle); 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); if (foodGroup.Count <= 1) { PlayerPatches.FoodContribution c = foodGroup.Members[0]; GUILayout.Label(FormatMemberLine(text + " - ", c), _labelStyle); 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.MaxTime)) .Append(", ") .Append(FoodBreakdownUI.FormatTime(foodGroup.TotalTime)) .Append(" banked"); if (GUILayout.Button(_sb.ToString(), _foldoutStyle)) { _stomachExpanded[key] = !flag; } 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 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)", _smallLabelStyle); return; } Dictionary counts = MilestoneStore.Load(localPlayer); string[] baseCategories = CategoryManager.BaseCategories; foreach (string text in baseCategories) { if (!MilestoneStore.EarnsMilestones(text)) { continue; } int num = MilestoneStore.Get(counts, text); int num2 = FoodMilestones.TiersEarned(num); _sb.Clear(); _sb.Append(text).Append(": ").Append(num) .Append(" eaten"); if (num2 > 0) { float num3 = 0f; for (int j = 1; j <= num2; j++) { num3 += FoodMilestones.HpForTier(j); } _sb.Append(" - tier ").Append(num2).Append(", +") .Append(num3.ToString("0.#")) .Append(" HP"); } GUILayout.Label(_sb.ToString(), _labelStyle); } GUILayout.Label($"Total: +{FoodMilestones.TotalBonus(localPlayer):0.#} max health (cap +{ConfigManager.milestoneTotalCap.Value:0.#})", _boldLabelStyle); } 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)", _smallLabelStyle); return; } Dictionary dictionary = FoodLifetimeLog.Load(localPlayer); if (dictionary.Count == 0) { GUILayout.Label("Nothing eaten yet.", _smallLabelStyle); 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 - 14f, _rect.yMax - 14f, 14f, 14f); 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, 200f, 800f); _rect.height = Mathf.Clamp(_resizeStartSize.y + vector.y, 150f, 600f); 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; _windowStyle = new GUIStyle(); _panelBoxStyle = new GUIStyle(GUI.skin.box) { border = new RectOffset(0, 0, 0, 0) }; _panelBoxStyle.normal.background = MakeTex(BgColor); _titleBoxStyle = new GUIStyle(GUI.skin.box) { border = new RectOffset(0, 0, 0, 0) }; _titleBoxStyle.normal.background = MakeTex(TitleBgColor); _titleLabelStyle = new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleLeft, normal = { textColor = GoldText } }; _toggleBtnStyle = new GUIStyle(GUI.skin.button) { fontSize = 12, fontStyle = FontStyle.Bold, normal = { textColor = GoldText } }; _sectionStyle = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, normal = { textColor = GoldDim } }; _foldoutStyle = new GUIStyle(GUI.skin.button) { fontSize = 12, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(2, 2, 2, 2) }; _foldoutStyle.normal.textColor = GoldText; _foldoutStyle.normal.background = null; _foldoutStyle.hover.textColor = Color.white; _foldoutStyle.hover.background = null; _foldoutStyle.active.textColor = Color.white; _foldoutStyle.active.background = null; _labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 12, normal = { textColor = BodyText } }; _smallLabelStyle = new GUIStyle(GUI.skin.label) { fontSize = 11, normal = { textColor = BodyText } }; _boldLabelStyle = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = FontStyle.Bold, normal = { textColor = GoldText } }; _gripStyle = new GUIStyle(GUI.skin.box) { border = new RectOffset(0, 0, 0, 0) }; _gripStyle.normal.background = MakeTex(GoldTrim); } } 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.HudPatches ---- namespace Fatty.Patches { [HarmonyPatch] public static class HudPatches { internal class FoodGroup { public Player.Food Rep; public int Count; public float MaxTime; 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; if (item2.m_time > foodGroup.MaxTime) { foodGroup.MaxTime = 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.MaxTime = 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 maxTime = foodGroup2.MaxTime; tMP_Text.text = ((maxTime >= 60f) ? (Mathf.CeilToInt(maxTime / 60f) + "m") : (Mathf.FloorToInt(maxTime) + "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; 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; 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}"); } } 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.0", MinimumRequiredVersion = "1.1.0" }; 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 milestoneTier2Hp; public static ConfigEntry milestoneTier3Hp; public static ConfigEntry milestoneTotalCap; 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 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", 5, "How many times the same food can be eaten at once. Vanilla = 1 (no stacking). Each extra stack is worth less - see section 3.", 1, 20); stackDiminishingReturnStep1 = BindSynced(config, "3 - Stacking", "Stack 2 Multiplier", 0.5f, "Fraction of a food's stats granted by its 2nd stack. 1.0 = full value, 0.5 = 50% (default), 0.0 = nothing. The 1st stack is always 100%.", 0f, 1f); stackDiminishingReturnStep2 = BindSynced(config, "3 - Stacking", "Stack 3 Multiplier", 0.25f, "Fraction of a food's stats granted by its 3rd stack. 0.25 = 25% (default).", 0f, 1f); stackDiminishingReturnStep3 = BindSynced(config, "3 - Stacking", "Stack 4+ Multiplier", 0.05f, "Fraction of a food's stats granted by the 4th and every further stack. 0.05 = 5% (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", 3f, "Balanced Diet (1 Meat + 1 Veggie): MULTIPLIER on vanilla health & stamina regen. 1.0 = vanilla, 3.0 = 3x vanilla / +200% (default).", 1f, 10f); sugarRushSpeedBonus = BindSynced(config, "4 - Synergies", "Sugar Rush Speed Multiplier", 2.5f, "Sugar Rush (3 Sweets + 1 Drink): MULTIPLIER on vanilla movement speed. 1.0 = vanilla, 2.5 = 2.5x / +150% (default).", 1f, 5f); fishermanSwimStaminaReduction = BindSynced(config, "4 - Synergies", "Fisherman Swim Stamina Reduction", 0.5f, "Fisherman's Friend (2 Fish + 1 Veggie): FRACTION of vanilla swim stamina drain removed. 0.0 = vanilla drain, 0.50 = half the drain (default), 1.0 = free swimming.", 0f, 1f); lumberjackCarryWeight = BindSynced(config, "4 - Synergies", "Lumberjack Carry Weight Bonus", 100f, "Lumberjack's Feast (3 Meat + 1 Drink): FLAT carry weight added on top of vanilla. 0 = vanilla, 100 = +100 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", 4f, "Safety cap on the above, as a MULTIPLIER of vanilla's regen rate. 1.0 = never speed up at all, 4.0 = at most 4x 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", 1f, "MULTIPLIER on how long food lasts. 1.0 = vanilla duration (default), 2.0 = food lasts twice as long. 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 your BASE max health. Vanilla base is 25. 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); milestoneTier1Hp = BindSynced(config, "8 - Milestones", "Tier 1 Health", 5f, "FLAT base max health added by a category's 1st milestone. 5 = +5 on top of vanilla's 25 base (default).", 0f, 500f); milestoneTier2Hp = BindSynced(config, "8 - Milestones", "Tier 2 Health", 5f, "Flat base max health added by a category's 2nd milestone. Default +5.", 0f, 500f); milestoneTier3Hp = BindSynced(config, "8 - Milestones", "Tier 3 Health", 10f, "Flat base max health added by a category's 3rd milestone. Default +10.", 0f, 500f); milestoneTotalCap = BindSynced(config, "8 - Milestones", "Total Health Cap", 50f, "Hard cap on total base health from ALL milestones combined. Each of the 5 real categories can grant 20 at the defaults, so a broad diet is what reaches the 50 cap.", 0f, 1000f); 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", 10f, "FLAT bonus max health while a buffing drink is active. 0 = none, 10 = +10 (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", 3, "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. 3 = default.", 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", 200f, "Saved screen X position (pixels from the left edge) of the Feast Ledger panel. Updated automatically when you drag it."); foodBreakdownPanelY = BindLocal(config, "10 - HUD Panels", "Food Breakdown Panel Y", 100f, "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", 420f, "Saved width of the Feast Ledger panel. Updated automatically when you resize it.", 200f, 800f); foodBreakdownPanelHeight = BindLocalRanged(config, "10 - HUD Panels", "Food Breakdown Panel Height", 420f, "Saved height of the Feast Ledger panel. Updated automatically when you resize it.", 150f, 600f); useSegmentedStackBar = BindLocal(config, "10 - HUD Panels", "Use Segmented Stack Bar", defaultValue: true, "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. 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."); } 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; } } }