using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("HungerPangs")] [assembly: AssemblyDescription("Valheim HungerPangs Mod by DrummerCraig")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("HungerPangs")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("ea00b146-e2c4-41cd-8e70-7bbada299b8e")] [assembly: AssemblyFileVersion("1.2.2.0")] [assembly: AssemblyVersion("1.2.2.0")] namespace DrummerCraig.HungerPangs; public static class AdminSync { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class OnNewConnectionPatch { private static void Postfix(ZNetPeer peer, ZNet __instance) { if (peer != null && peer.m_rpc != null && !((Object)(object)__instance == (Object)null) && !__instance.IsServer()) { peer.m_rpc.Register("HungerPangs_Admin", (Action)OnServerAdminStatus); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] private static class ShutdownPatch { private static void Postfix() { IsAdmin = false; OnAdminStatusChanged?.Invoke(); } } private const string RpcAdmin = "HungerPangs_Admin"; public static Action OnAdminStatusChanged; private static FieldInfo _peersField; private static FieldInfo _adminListField; private static MethodInfo _listContainsId; public static bool IsAdmin { get; private set; } public static void PushTo(ZRpc rpc) { if (rpc == null || (Object)(object)ZNet.instance == (Object)null) { return; } try { bool flag = SenderIsAdmin(rpc); rpc.Invoke("HungerPangs_Admin", new object[1] { flag }); ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)$"[HungerPangs] Pushed admin status {flag} to peer."); } } catch (Exception ex) { ManualLogSource log2 = HungerPangs.Log; if (log2 != null) { log2.LogWarning((object)("[HungerPangs] admin push failed: " + ex.Message)); } } } private static void OnServerAdminStatus(ZRpc rpc, bool isAdmin) { if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { IsAdmin = isAdmin; ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)$"[HungerPangs] Server reports this client admin status: {isAdmin}."); } OnAdminStatusChanged?.Invoke(); } } public static bool IsSenderAdmin(ZRpc rpc) { return SenderIsAdmin(rpc); } private static bool SenderIsAdmin(ZRpc rpc) { try { ZNet instance = ZNet.instance; _peersField = _peersField ?? typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.NonPublic); List list = _peersField?.GetValue(instance) as List; string text = null; if (list != null) { foreach (ZNetPeer item in list) { if (item.m_rpc == rpc) { ISocket socket = item.m_socket; text = ((socket != null) ? socket.GetHostName() : null); break; } } } if (string.IsNullOrEmpty(text)) { return false; } _adminListField = _adminListField ?? typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.NonPublic); _listContainsId = _listContainsId ?? typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.NonPublic); object obj = _adminListField?.GetValue(instance); if (obj == null || _listContainsId == null) { return false; } return (bool)_listContainsId.Invoke(instance, new object[2] { obj, text }); } catch (Exception) { return false; } } } public static class ConfigSync { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class OnNewConnectionPatch { private static void Postfix(ZNetPeer peer, ZNet __instance) { if (peer != null && peer.m_rpc != null && !((Object)(object)__instance == (Object)null)) { if (__instance.IsServer()) { peer.m_rpc.Register("HungerPangs_AdminConfig", (Action)OnAdminConfig); return; } _serverRpc = peer.m_rpc; peer.m_rpc.Register("HungerPangs_Config", (Action)OnServerConfig); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] private static class ShutdownPatch { private static void Postfix() { _serverRpc = null; ServerConfig.ResetToLocal(); ServerConfig.RefreshLockState(); } } private const string RpcConfig = "HungerPangs_Config"; private const string RpcAdminConfig = "HungerPangs_AdminConfig"; private static ZRpc _serverRpc; internal static bool Suppressing; private static FieldInfo _peersField; public static void PushTo(ZRpc rpc) { try { rpc.Invoke("HungerPangs_Config", new object[1] { WriteBundle() }); ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)($"[HungerPangs] Pushed server config to peer (ModStatus={ServerConfig.ModStatus.Value}, " + $"AutoEat={ServerConfig.AutoEat.Value}, AutoMead={ServerConfig.AutoMead.Value}).")); } } catch (Exception ex) { ManualLogSource log2 = HungerPangs.Log; if (log2 != null) { log2.LogWarning((object)("[HungerPangs] config push failed: " + ex.Message)); } } } public static void HookConfigWatch(ConfigFile cfg) { if (cfg != null) { cfg.SettingChanged += OnConfigChanged; } } private static void OnConfigChanged(object sender, SettingChangedEventArgs args) { if (Suppressing) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { ServerConfig.RefreshLockState(); return; } if (instance.IsServer()) { BroadcastConfig(); } else if (_serverRpc != null && AdminSync.IsAdmin) { try { _serverRpc.Invoke("HungerPangs_AdminConfig", new object[1] { WriteBundle() }); } catch (Exception) { } } ServerConfig.RefreshLockState(); } private static void OnAdminConfig(ZRpc rpc, ZPackage z) { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && z != null && AdminSync.IsSenderAdmin(rpc)) { Suppressing = true; try { ApplyBundleToLocal(z); } finally { Suppressing = false; } BroadcastConfig(); } } private static void BroadcastConfig() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } List list = Peers(instance); if (list == null) { return; } foreach (ZNetPeer item in list) { if (item?.m_rpc != null) { try { item.m_rpc.Invoke("HungerPangs_Config", new object[1] { WriteBundle() }); } catch (Exception) { } } } } private static List Peers(ZNet net) { try { _peersField = _peersField ?? typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.NonPublic); return _peersField?.GetValue(net) as List; } catch (Exception) { return null; } } private static ZPackage WriteBundle() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(ServerConfig.ModStatus.Value); val.Write(ServerConfig.AutoEat.Value); val.Write(ServerConfig.PauseNearWorkbench.Value); val.Write(ServerConfig.PauseOnBoat.Value); val.Write(ServerConfig.KeepFoodOnDeath.Value); val.Write(ServerConfig.AutoMead.Value); val.Write(ServerConfig.AutoPoisonMead.Value); val.Write(ServerConfig.AutoFireMead.Value); val.Write(ServerConfig.AutoFrostMead.Value); return val; } private static void OnServerConfig(ZRpc rpc, ZPackage z) { if (z == null) { return; } if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)"[HungerPangs] Ignoring server config push (this instance is the server/host)."); } return; } z.SetPos(0); bool flag = z.ReadBool(); bool flag2 = z.ReadBool(); bool flag3 = z.ReadBool(); bool flag4 = z.ReadBool(); bool flag5 = z.ReadBool(); bool flag6 = z.ReadBool(); bool flag7 = z.ReadBool(); bool flag8 = z.ReadBool(); bool flag9 = z.ReadBool(); ServerConfig.ApplyServer(flag, flag2, flag3, flag4, flag5, flag6, flag7, flag8, flag9); ServerConfig.RefreshLockState(); ManualLogSource log2 = HungerPangs.Log; if (log2 != null) { log2.LogInfo((object)($"[HungerPangs] Adopted server config: ModStatus={flag}, AutoEat={flag2}, " + $"PauseNearWorkbench={flag3}, PauseOnBoat={flag4}, KeepFoodOnDeath={flag5}, " + $"AutoMead={flag6}, AutoPoisonMead={flag7}, AutoFireMead={flag8}, AutoFrostMead={flag9}. " + $"ConfigEntry now reads ModStatus={ServerConfig.ModStatus.Value}, AutoEat={ServerConfig.AutoEat.Value}.")); } } private static void ApplyBundleToLocal(ZPackage z) { z.SetPos(0); bool modStatus = z.ReadBool(); bool autoEat = z.ReadBool(); bool pauseNearWorkbench = z.ReadBool(); bool pauseOnBoat = z.ReadBool(); bool keepFoodOnDeath = z.ReadBool(); bool autoMead = z.ReadBool(); bool autoPoisonMead = z.ReadBool(); bool autoFireMead = z.ReadBool(); bool autoFrostMead = z.ReadBool(); ServerConfig.ApplyLocalFromBundle(modStatus, autoEat, pauseNearWorkbench, pauseOnBoat, keepFoodOnDeath, autoMead, autoPoisonMead, autoFireMead, autoFrostMead); } } public sealed class ConfigurationManagerAttributes { public bool? ReadOnly = false; } [BepInPlugin("drummercraig.hungerpangs", "Hunger Pangs", "1.3.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class HungerPangs : BaseUnityPlugin { [HarmonyPatch(typeof(Player), "UpdateFood", new Type[] { typeof(float), typeof(bool) })] private static class Player_UpdateFood_Patch { private static void Postfix(Player __instance) { try { if ((!_nearWorkbench && !_onBoat) || (Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return; } float deltaTime = Time.deltaTime; foreach (Food food in __instance.GetFoods()) { if (food?.m_item != null) { food.m_time += deltaTime; } } } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] UpdateFood postfix failed: {arg}"); } } } } [HarmonyPatch(typeof(Player), "OnDeath", new Type[] { })] private static class Player_OnDeath_Patch { private static void Prefix(Player __instance, out List __state) { __state = null; try { if (ServerConfig.ModStatusValue && ServerConfig.KeepFoodOnDeathValue && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { List foods = __instance.GetFoods(); if (foods != null && foods.Count > 0) { __state = new List(foods); } } } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] OnDeath prefix failed: {arg}"); } } } private static void Postfix(Player __instance, List __state) { try { if (__state != null && __state.Count != 0) { List foods = __instance.GetFoods(); if (foods != null) { foods.Clear(); foods.AddRange(__state); } } } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] OnDeath postfix failed: {arg}"); } } } } [HarmonyPatch(typeof(Player), "EatFood", new Type[] { typeof(ItemData) })] private static class Player_EatFood_Patch { private static void Postfix(Player __instance, ItemData item, bool __result) { try { if (!__result || item?.m_shared == null || !ServerConfig.ModStatusValue || s_scaleWithTimeControl == null || !s_scaleWithTimeControl.Value || (Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return; } float timeControlMultiplier = GetTimeControlMultiplier(); if (timeControlMultiplier <= 0f || Mathf.Abs(timeControlMultiplier - 1f) <= 0.001f) { return; } string name = item.m_shared.m_name; foreach (Food food in __instance.GetFoods()) { if (food?.m_item?.m_shared != null && food.m_item.m_shared.m_name == name) { food.m_time = food.m_item.m_shared.m_foodBurnTime * timeControlMultiplier; break; } } } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] EatFood postfix failed: {arg}"); } } } } [HarmonyPatch(typeof(Character), "RPC_Damage", new Type[] { typeof(long), typeof(HitData) })] private static class Character_RPC_Damage_Patch { private static void Prefix(Character __instance, HitData hit) { try { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { Character attacker = hit.GetAttacker(); if ((Object)(object)attacker != (Object)null && !attacker.IsPlayer()) { _lastEnemyHitTime = Time.time; } if (hit.m_damage.m_frost > 0f) { _frostHitTimes.Enqueue(Time.time); } } } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] RPC_Damage prefix failed: {arg}"); } } } } public const string PluginGuid = "drummercraig.hungerpangs"; public const string PluginName = "Hunger Pangs"; public const string PluginVersion = "1.3.0"; private ConfigEntry modStatus; private ConfigEntry autoEat; private ConfigEntry foodExpiryNotify; private ConfigEntry autoEatPercent; private ConfigEntry foodExpiryPercent; private ConfigEntry autoEatNotify; private ConfigEntry lowSupplyNotify; private ConfigEntry lowSupplyCount; private ConfigEntry autoMead; private ConfigEntry autoMeadHealthPercent; private ConfigEntry autoMeadOnlyOnEnemyHit; private ConfigEntry autoMeadEnemyHitWindow; private ConfigEntry autoMeadRequireMaxHealth; private ConfigEntry autoMeadNotify; private ConfigEntry autoPoisonMead; private ConfigEntry autoPoisonMeadRange; private ConfigEntry autoPoisonMeadNotify; private ConfigEntry autoFireMead; private ConfigEntry autoFireMeadRange; private ConfigEntry autoFireMeadNotify; private ConfigEntry autoFrostMead; private ConfigEntry autoFrostMeadTickCount; private ConfigEntry autoFrostMeadTickWindow; private ConfigEntry autoFrostMeadNotify; private ConfigEntry pauseFoodNearWorkbench; private ConfigEntry pauseFoodOnBoat; private ConfigEntry keepFoodOnDeath; private ConfigEntry scaleWithTimeControl; private static float _lastEnemyHitTime = float.MinValue; private static ManualLogSource _log; internal static ManualLogSource Log; private static ConfigEntry s_scaleWithTimeControl; private const string TimeControlGuid = "drummercraig.time_control"; private static bool _tcResolved; private static MethodInfo _tcEffectiveMultiplierGetter; private static bool _nearWorkbench; private static bool _onBoat; private static readonly List _nearbyCharacterBuffer = new List(); private static readonly Queue _frostHitTimes = new Queue(); private static readonly HashSet _poisonEnemyPrefabs = new HashSet(); private static readonly HashSet _fireEnemyPrefabs = new HashSet(); private static readonly HashSet _checkedPrefabs = new HashSet(); private const float MeleeRange = 3f; private const float MeleeRangeSq = 9f; private readonly HashSet hasShown = new HashSet(); private readonly HashSet seenThisTick = new HashSet(); private readonly HashSet lowSupplyShown = new HashSet(); private Predicate _notSeenThisTick; private void Awake() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Expected O, but got Unknown //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Expected O, but got Unknown //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Expected O, but got Unknown //IL_01e5: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Expected O, but got Unknown //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Expected O, but got Unknown //IL_026f: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Expected O, but got Unknown //IL_02ce: Unknown result type (might be due to invalid IL or missing references) //IL_02d8: Expected O, but got Unknown //IL_0345: Unknown result type (might be due to invalid IL or missing references) //IL_034f: Expected O, but got Unknown //IL_0383: Unknown result type (might be due to invalid IL or missing references) //IL_038d: Expected O, but got Unknown //IL_03d9: Unknown result type (might be due to invalid IL or missing references) //IL_03e3: Expected O, but got Unknown //IL_0417: Unknown result type (might be due to invalid IL or missing references) //IL_0421: Expected O, but got Unknown //IL_046d: Unknown result type (might be due to invalid IL or missing references) //IL_0477: Expected O, but got Unknown //IL_04a0: Unknown result type (might be due to invalid IL or missing references) //IL_04aa: Expected O, but got Unknown //IL_04de: Unknown result type (might be due to invalid IL or missing references) //IL_04e8: Expected O, but got Unknown //IL_05f4: Unknown result type (might be due to invalid IL or missing references) //IL_0613: Expected O, but got Unknown modStatus = ((BaseUnityPlugin)this).Config.Bind("General", "01. Mod Status", true, new ConfigDescription("Master toggle for the entire mod. When disabled, no auto-eating or notifications occur.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoEat = ((BaseUnityPlugin)this).Config.Bind("General", "02. Auto-Eat", true, new ConfigDescription("Automatically re-eat food before it expires. When disabled, the mod will only show notifications if NotificationsEnabled is on, allowing you to eat manually on cue.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoEatPercent = ((BaseUnityPlugin)this).Config.Bind("General", "03. Auto-Eat Percent", 5, new ConfigDescription("Percentage of a food's total duration remaining when it is automatically re-eaten. Food becomes eligible to re-eat at 50% remaining (when it starts blinking), so this is your window. Lower values eat later and waste less food; higher values re-eat sooner after becoming eligible. Example: 5 = eat when 5% remains (e.g. ~1.5 min of a 30-min food). Requires Auto-Eat Enabled.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 49), Array.Empty())); autoEatNotify = ((BaseUnityPlugin)this).Config.Bind("General", "04. Auto-Eat Notify", true, "Show a HUD notification when a food item is automatically eaten. Requires AutomaticallyEat."); foodExpiryNotify = ((BaseUnityPlugin)this).Config.Bind("General", "05. Food Expiry Notify", true, "Show a HUD notification when a food item nears its expiry threshold. Useful on its own when Auto-Eat is Disabled,as a manual reminder to eat."); foodExpiryPercent = ((BaseUnityPlugin)this).Config.Bind("General", "06. Food Expiry Percent", 30, new ConfigDescription("Percentage of a food's total duration remaining when the HUD notification appears. Food becomes eligible to re-eat at 50% remaining (when it starts blinking). Set this higher than Auto-Eat Percent so the notification fires before auto-eat. Example: 30 = notify when 30% remains (e.g. ~9 min of a 30-min food). Requires Notifications.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 50), Array.Empty())); lowSupplyNotify = ((BaseUnityPlugin)this).Config.Bind("General", "07. Low Supply Notify", true, "Show a HUD notification when AutomaticallyEat is enabled, a food item is consumed and the remaining count in your inventory is at or below the LowSupplyThreshold."); lowSupplyCount = ((BaseUnityPlugin)this).Config.Bind("General", "08. Low Supply Count", 1, "The number of food items in your inventory for the LowSupplyNotification to appear."); pauseFoodNearWorkbench = ((BaseUnityPlugin)this).Config.Bind("General", "09. Pause Near Workbench", true, new ConfigDescription("Pause food expiry timers when within range of a workbench. Food effects (health and stamina regen) continue normally — only the countdown is paused.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); pauseFoodOnBoat = ((BaseUnityPlugin)this).Config.Bind("General", "10. Pause On Boat", true, new ConfigDescription("Pause food expiry timers while sailing on a boat. Food effects (health and stamina regen) continue normally — only the countdown is paused.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); keepFoodOnDeath = ((BaseUnityPlugin)this).Config.Bind("General", "11. Keep Food On Death", false, new ConfigDescription("When enabled, the food you have eaten is preserved when you die — you respawn with the same active food buffs and their remaining timers, instead of losing all food on death. Disabled by default, as this removes a normal death penalty.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); scaleWithTimeControl = ((BaseUnityPlugin)this).Config.Bind("General", "12. Scale With TimeControl", true, "Scale food expiry timers by the TimeControl mod's day/night multiplier so food lasts proportionally longer when days are longer. Example: if TimeControl is set to 4x, food lasts ~4x longer. Uses TimeControl's effective multiplier, so on a server that pushes its own value everyone's food scales with the shared clock. Has no effect if TimeControl is not installed."); autoMead = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "01. Auto Health Mead", true, new ConfigDescription("Automatically drink a health mead when your health drops below the configured threshold.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoMeadHealthPercent = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "02. Health Threshold", 30, new ConfigDescription("Health percentage below which a health mead will be automatically consumed. Example: 40 = drink when health drops below 40% of your maximum.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 99), Array.Empty())); autoMeadOnlyOnEnemyHit = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "03. Only On Enemy Hit", true, "Only auto-drink when the health drop was caused by an enemy. Prevents wasting meads on fall damage, lava, or other environmental hazards when not in combat."); autoMeadEnemyHitWindow = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "04. Enemy Hit Window", 5f, new ConfigDescription("Seconds after the last enemy hit during which the mead trigger remains active. Requires Only On Enemy Hit. Example: 5 = the mead can fire within 5 seconds of being struck.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 30f), Array.Empty())); autoMeadRequireMaxHealth = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "05. Require Sufficient Max Health", false, "Only drink a mead if your maximum health is at least equal to the mead's total healing value. Prevents drinking a mead that heals more than your health cap, which would waste the excess. Meads that exceed your max health are skipped; the next-strongest eligible mead is tried instead."); autoMeadNotify = ((BaseUnityPlugin)this).Config.Bind("Health Mead", "06. Notify", true, "Show a HUD notification when a health mead is automatically consumed."); autoPoisonMead = ((BaseUnityPlugin)this).Config.Bind("Poison Resist Mead", "01. Auto Poison Mead", true, new ConfigDescription("Automatically drink a poison resist mead when a nearby enemy can deal poison damage.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoPoisonMeadRange = ((BaseUnityPlugin)this).Config.Bind("Poison Resist Mead", "02. Detection Range", 4f, new ConfigDescription("Radius in meters to scan for poison-capable enemies.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 50f), Array.Empty())); autoPoisonMeadNotify = ((BaseUnityPlugin)this).Config.Bind("Poison Resist Mead", "03. Notify", true, "Show a HUD notification when a poison resist mead is automatically consumed."); autoFireMead = ((BaseUnityPlugin)this).Config.Bind("Fire Resist Mead", "01. Auto Fire Mead", true, new ConfigDescription("Automatically drink a fire resist mead when a nearby enemy can deal fire damage.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoFireMeadRange = ((BaseUnityPlugin)this).Config.Bind("Fire Resist Mead", "02. Detection Range", 15f, new ConfigDescription("Radius in meters to scan for fire-capable enemies.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 50f), Array.Empty())); autoFireMeadNotify = ((BaseUnityPlugin)this).Config.Bind("Fire Resist Mead", "03. Notify", true, "Show a HUD notification when a fire resist mead is automatically consumed."); autoFrostMead = ((BaseUnityPlugin)this).Config.Bind("Frost Resist Mead", "01. Auto Frost Mead", true, new ConfigDescription("Automatically drink a frost resist mead after receiving repeated frost damage hits.", (AcceptableValueBase)null, new object[1] { ServerConfig.NewLock() })); autoFrostMeadTickCount = ((BaseUnityPlugin)this).Config.Bind("Frost Resist Mead", "02. Frost Tick Count", 3, new ConfigDescription("Number of frost damage hits within the time window required to trigger auto-consume.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 10), Array.Empty())); autoFrostMeadTickWindow = ((BaseUnityPlugin)this).Config.Bind("Frost Resist Mead", "03. Frost Tick Window", 10f, new ConfigDescription("Seconds over which frost hits are counted toward the trigger threshold.", (AcceptableValueBase)(object)new AcceptableValueRange(2f, 60f), Array.Empty())); autoFrostMeadNotify = ((BaseUnityPlugin)this).Config.Bind("Frost Resist Mead", "04. Notify", true, "Show a HUD notification when a frost resist mead is automatically consumed."); _notSeenThisTick = (string key) => !seenThisTick.Contains(key); _log = ((BaseUnityPlugin)this).Logger; Log = ((BaseUnityPlugin)this).Logger; ServerConfig.ModStatus = modStatus; ServerConfig.AutoEat = autoEat; ServerConfig.PauseNearWorkbench = pauseFoodNearWorkbench; ServerConfig.PauseOnBoat = pauseFoodOnBoat; ServerConfig.KeepFoodOnDeath = keepFoodOnDeath; ServerConfig.AutoMead = autoMead; ServerConfig.AutoPoisonMead = autoPoisonMead; ServerConfig.AutoFireMead = autoFireMead; ServerConfig.AutoFrostMead = autoFrostMead; s_scaleWithTimeControl = scaleWithTimeControl; VersionGate.OnPeerValidated = delegate(ZRpc rpc) { ConfigSync.PushTo(rpc); AdminSync.PushTo(rpc); }; AdminSync.OnAdminStatusChanged = ServerConfig.RefreshLockState; ConfigSync.HookConfigWatch(((BaseUnityPlugin)this).Config); ResilientPatcher.ApplyAll(new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID), typeof(HungerPangs).Assembly, ((BaseUnityPlugin)this).Logger); ((MonoBehaviour)this).StartCoroutine(FoodCheckLoop()); ((MonoBehaviour)this).StartCoroutine(MeadCheckLoop()); } private IEnumerator FoodCheckLoop() { WaitForSeconds wait = new WaitForSeconds(1f); while (true) { yield return wait; try { CheckFoods(); } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] CheckFoods failed: {arg}"); } } } } private IEnumerator MeadCheckLoop() { WaitForSeconds wait = new WaitForSeconds(0.5f); while (true) { yield return wait; try { CheckMeads(); CheckProximityResistMeads(); CheckFrostResistMeads(); } catch (Exception arg) { ManualLogSource log = _log; if (log != null) { log.LogError((object)$"[HungerPangs] MeadCheckLoop failed: {arg}"); } } } } private void CheckFoods() { Player localPlayer = Player.m_localPlayer; _nearWorkbench = (Object)(object)localPlayer != (Object)null && ServerConfig.PauseNearWorkbenchValue && NearWorkbench(localPlayer); _onBoat = (Object)(object)localPlayer != (Object)null && ServerConfig.PauseOnBoatValue && IsOnBoat(localPlayer); if (!ServerConfig.ModStatusValue || (Object)(object)localPlayer == (Object)null || !ServerConfig.ModStatusValue || (Object)(object)localPlayer == (Object)null) { return; } Humanoid val = (Humanoid)(object)localPlayer; Inventory inventory = val.GetInventory(); List foods = localPlayer.GetFoods(); seenThisTick.Clear(); for (int i = 0; i < foods.Count; i++) { Food val2 = foods[i]; string name = val2.m_item.m_shared.m_name; seenThisTick.Add(name); float time = val2.m_time; float foodBurnTime = val2.m_item.m_shared.m_foodBurnTime; float num = ((foodBurnTime > 0f) ? (time / foodBurnTime * 100f) : 0f); if (!val2.CanEatAgain()) { hasShown.Remove(name); lowSupplyShown.Remove(name); continue; } if (foodExpiryNotify.Value && !hasShown.Contains(name) && num <= (float)foodExpiryPercent.Value) { string arg = Localization.instance.Localize(val2.m_item.m_shared.m_name); int num2 = Mathf.CeilToInt(time / 60f); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, $"{arg} expires in {num2} min", 0, (Sprite)null, false); } hasShown.Add(name); } if (!ServerConfig.AutoEatValue || !(num <= (float)autoEatPercent.Value)) { continue; } ItemData item = inventory.GetItem(name, -1, false); if (item == null || !val.ConsumeItem(inventory, item, false)) { continue; } hasShown.Remove(name); if (autoEatNotify.Value) { string text = Localization.instance.Localize(val2.m_item.m_shared.m_name); MessageHud instance2 = MessageHud.instance; if (instance2 != null) { instance2.ShowMessage((MessageType)2, text + " automatically re-eaten", 0, (Sprite)null, false); } } if (!lowSupplyNotify.Value || lowSupplyCount.Value <= 0 || lowSupplyShown.Contains(name)) { continue; } int num3 = inventory.CountItems(name, -1, true); if (num3 <= lowSupplyCount.Value) { string arg2 = Localization.instance.Localize(val2.m_item.m_shared.m_name); MessageHud instance3 = MessageHud.instance; if (instance3 != null) { instance3.ShowMessage((MessageType)2, $"Low supply: {num3} {arg2} remaining", 0, (Sprite)null, false); } lowSupplyShown.Add(name); } } hasShown.RemoveWhere(_notSeenThisTick); lowSupplyShown.RemoveWhere(_notSeenThisTick); } private void CheckMeads() { //IL_00d4: Unknown result type (might be due to invalid IL or missing references) if (!ServerConfig.ModStatusValue || !ServerConfig.AutoMeadValue) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || ((Character)localPlayer).GetHealthPercentage() * 100f >= (float)autoMeadHealthPercent.Value || (autoMeadOnlyOnEnemyHit.Value && Time.time - _lastEnemyHitTime > autoMeadEnemyHitWindow.Value)) { return; } Humanoid val = (Humanoid)(object)localPlayer; Inventory inventory = val.GetInventory(); List healthMeads = GetHealthMeads(inventory); float maxHealth = ((Character)localPlayer).GetMaxHealth(); SEMan sEMan = ((Character)localPlayer).GetSEMan(); foreach (ItemData item in healthMeads) { StatusEffect consumeStatusEffect = item.m_shared.m_consumeStatusEffect; if ((Object)(object)consumeStatusEffect != (Object)null && sEMan.HaveStatusEffect(consumeStatusEffect.NameHash())) { continue; } if (autoMeadRequireMaxHealth.Value) { float healthOverTime = ((SE_Stats)item.m_shared.m_consumeStatusEffect).m_healthOverTime; if (maxHealth < healthOverTime) { continue; } } if (!val.ConsumeItem(inventory, item, false)) { continue; } if (autoMeadNotify.Value) { string text = Localization.instance.Localize(item.m_shared.m_name); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, text + " automatically consumed", 0, (Sprite)null, false); } } break; } } private static bool NearWorkbench(Player player) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) return (Object)(object)CraftingStation.FindClosestStationInRange("$piece_workbench", ((Component)player).transform.position, 20f) != (Object)null; } private static bool IsOnBoat(Player player) { return ((Character)player).IsAttachedToShip(); } private static float GetTimeControlMultiplier() { if (!_tcResolved) { _tcResolved = true; try { if (Chainloader.PluginInfos.ContainsKey("drummercraig.time_control")) { Type type = AccessTools.TypeByName("TimeControl.TimeControlConfig"); if (type != null) { _tcEffectiveMultiplierGetter = AccessTools.PropertyGetter(type, "EffectiveMultiplier"); } ManualLogSource log = _log; if (log != null) { log.LogInfo((object)((_tcEffectiveMultiplierGetter != null) ? "[HungerPangs] TimeControl detected; food duration will scale with its multiplier." : "[HungerPangs] TimeControl detected but EffectiveMultiplier could not be resolved; food scaling disabled.")); } } } catch (Exception ex) { _tcEffectiveMultiplierGetter = null; ManualLogSource log2 = _log; if (log2 != null) { log2.LogWarning((object)("[HungerPangs] TimeControl detection failed: " + ex.Message)); } } } if (_tcEffectiveMultiplierGetter == null) { return 1f; } try { return (float)_tcEffectiveMultiplierGetter.Invoke(null, null); } catch (Exception ex2) { ManualLogSource log3 = _log; if (log3 != null) { log3.LogWarning((object)("[HungerPangs] reading TimeControl multiplier failed: " + ex2.Message)); } return 1f; } } private void CheckProximityResistMeads() { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) bool flag = ServerConfig.ModStatusValue && ServerConfig.AutoPoisonMeadValue; bool flag2 = ServerConfig.ModStatusValue && ServerConfig.AutoFireMeadValue; if (!flag && !flag2) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } float num = 0f; if (flag) { num = Mathf.Max(num, autoPoisonMeadRange.Value); } if (flag2) { num = Mathf.Max(num, autoFireMeadRange.Value); } bool flag3 = false; bool flag4 = false; _nearbyCharacterBuffer.Clear(); Character.GetCharactersInRange(((Component)localPlayer).transform.position, num, _nearbyCharacterBuffer); float num2 = autoPoisonMeadRange.Value * autoPoisonMeadRange.Value; float num3 = autoFireMeadRange.Value * autoFireMeadRange.Value; foreach (Character item in _nearbyCharacterBuffer) { if ((Object)(object)item == (Object)null || item.IsPlayer() || item.IsDead()) { continue; } Vector3 val = ((Component)item).transform.position - ((Component)localPlayer).transform.position; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (IsThreateningPlayer(item, localPlayer, sqrMagnitude)) { string prefabName = GetPrefabName(item); EnsureCached(prefabName, item); if (flag && !flag3 && sqrMagnitude <= num2 && _poisonEnemyPrefabs.Contains(prefabName)) { flag3 = true; } if (flag2 && !flag4 && sqrMagnitude <= num3 && _fireEnemyPrefabs.Contains(prefabName)) { flag4 = true; } if (flag3 && flag4) { break; } } } if (flag3) { ConsumeResistMead(localPlayer, autoPoisonMeadNotify, (DamageType)256); } if (flag4) { ConsumeResistMead(localPlayer, autoFireMeadNotify, (DamageType)32); } } private static bool IsThreateningPlayer(Character c, Player localPlayer, float distSq) { if (distSq <= 9f) { return true; } MonsterAI component = ((Component)c).GetComponent(); if ((Object)(object)component != (Object)null) { return (object)((BaseAI)component).GetTargetCreature() == localPlayer; } return false; } private static string GetPrefabName(Character c) { return ((Object)((Component)c).gameObject).name.Replace("(Clone)", "").Trim(); } private static void EnsureCached(string prefab, Character c) { if (_checkedPrefabs.Contains(prefab)) { return; } Humanoid val = (Humanoid)(object)((c is Humanoid) ? c : null); if ((Object)(object)val == (Object)null) { _checkedPrefabs.Add(prefab); return; } foreach (ItemData allItem in val.GetInventory().GetAllItems()) { if (allItem.m_shared.m_damages.m_poison > 0f) { _poisonEnemyPrefabs.Add(prefab); } if (allItem.m_shared.m_damages.m_fire > 0f) { _fireEnemyPrefabs.Add(prefab); } } ItemData currentWeapon = val.GetCurrentWeapon(); if (currentWeapon != null) { if (currentWeapon.m_shared.m_damages.m_poison > 0f) { _poisonEnemyPrefabs.Add(prefab); } if (currentWeapon.m_shared.m_damages.m_fire > 0f) { _fireEnemyPrefabs.Add(prefab); } } _checkedPrefabs.Add(prefab); } private void ConsumeResistMead(Player localPlayer, ConfigEntry notify, DamageType type) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) Inventory inventory = ((Humanoid)localPlayer).GetInventory(); SEMan sEMan = ((Character)localPlayer).GetSEMan(); foreach (ItemData resistMead in GetResistMeads(inventory, type)) { StatusEffect consumeStatusEffect = resistMead.m_shared.m_consumeStatusEffect; if ((Object)(object)consumeStatusEffect != (Object)null && sEMan.HaveStatusEffect(consumeStatusEffect.NameHash())) { break; } if (!((Humanoid)localPlayer).ConsumeItem(inventory, resistMead, false)) { continue; } if (notify.Value) { string text = Localization.instance.Localize(resistMead.m_shared.m_name); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, text + " automatically consumed", 0, (Sprite)null, false); } } break; } } private void CheckFrostResistMeads() { if (!ServerConfig.ModStatusValue || !ServerConfig.AutoFrostMeadValue) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } float num = Time.time - autoFrostMeadTickWindow.Value; while (_frostHitTimes.Count > 0 && _frostHitTimes.Peek() < num) { _frostHitTimes.Dequeue(); } SEMan sEMan = ((Character)localPlayer).GetSEMan(); bool num2 = sEMan.HaveStatusEffect(StringExtensionMethods.GetStableHashCode("Freezing")); bool flag = _frostHitTimes.Count >= autoFrostMeadTickCount.Value; if (!num2 && !flag) { return; } Humanoid val = (Humanoid)(object)localPlayer; Inventory inventory = val.GetInventory(); foreach (ItemData resistMead in GetResistMeads(inventory, (DamageType)64)) { StatusEffect consumeStatusEffect = resistMead.m_shared.m_consumeStatusEffect; if ((Object)(object)consumeStatusEffect != (Object)null && sEMan.HaveStatusEffect(consumeStatusEffect.NameHash())) { _frostHitTimes.Clear(); break; } if (!val.ConsumeItem(inventory, resistMead, false)) { continue; } _frostHitTimes.Clear(); if (autoFrostMeadNotify.Value) { string text = Localization.instance.Localize(resistMead.m_shared.m_name); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, text + " automatically consumed", 0, (Sprite)null, false); } } break; } } private static List GetResistMeads(Inventory inventory, DamageType type) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) List list = new List(); foreach (ItemData allItem in inventory.GetAllItems()) { if ((int)allItem.m_shared.m_itemType != 2) { continue; } StatusEffect consumeStatusEffect = allItem.m_shared.m_consumeStatusEffect; SE_Stats val = (SE_Stats)(object)((consumeStatusEffect is SE_Stats) ? consumeStatusEffect : null); if (val == null) { continue; } foreach (DamageModPair mod in val.m_mods) { if (mod.m_type == type && (int)mod.m_modifier != 0) { list.Add(allItem); break; } } } return list; } private static List GetHealthMeads(Inventory inventory) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 List list = new List(); foreach (ItemData allItem in inventory.GetAllItems()) { if ((int)allItem.m_shared.m_itemType == 2) { StatusEffect consumeStatusEffect = allItem.m_shared.m_consumeStatusEffect; SE_Stats val = (SE_Stats)(object)((consumeStatusEffect is SE_Stats) ? consumeStatusEffect : null); if (val != null && val.m_healthOverTime > 0f) { list.Add(allItem); } } } list.Sort(delegate(ItemData a, ItemData b) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) float healthOverTime = ((SE_Stats)a.m_shared.m_consumeStatusEffect).m_healthOverTime; float healthOverTime2 = ((SE_Stats)b.m_shared.m_consumeStatusEffect).m_healthOverTime; return healthOverTime2.CompareTo(healthOverTime); }); return list; } } internal static class ResilientPatcher { internal static void ApplyAll(Harmony harmony, Assembly assembly, ManualLogSource log) { if (harmony == null || assembly == null) { return; } int num = 0; int num2 = 0; List list = null; Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly); foreach (Type type in typesFromAssembly) { try { List list2 = harmony.CreateClassProcessor(type).Patch(); if (list2 != null && list2.Count > 0) { num++; if (log != null) { log.LogDebug((object)$"[HungerPangs] patched {type.Name} ({list2.Count} method(s))."); } } } catch (Exception ex) { num2++; (list ?? (list = new List())).Add(type.Name); if (log != null) { log.LogWarning((object)("[HungerPangs] SKIP patch class " + type.Name + ": " + ex.Message)); } } } if (num2 == 0) { if (log != null) { log.LogInfo((object)$"[HungerPangs] patches: {num} applied, 0 skipped."); } } else if (log != null) { log.LogWarning((object)(string.Format("[HungerPangs] patches: {0} applied, {1} skipped ({2}). ", num, num2, string.Join(", ", list)) + "A skipped patch usually means a Valheim update changed a hooked method; that feature is disabled but the mod is still running.")); } } } internal static class ServerConfig { public static ConfigEntry ModStatus; public static ConfigEntry AutoEat; public static ConfigEntry PauseNearWorkbench; public static ConfigEntry PauseOnBoat; public static ConfigEntry KeepFoodOnDeath; public static ConfigEntry AutoMead; public static ConfigEntry AutoPoisonMead; public static ConfigEntry AutoFireMead; public static ConfigEntry AutoFrostMead; private static readonly List _lockable = new List(); private static bool _usingServer; private static bool _haveBackup; private static bool _bModStatus; private static bool _bAutoEat; private static bool _bPauseNearWorkbench; private static bool _bPauseOnBoat; private static bool _bKeepFoodOnDeath; private static bool _bAutoMead; private static bool _bAutoPoisonMead; private static bool _bAutoFireMead; private static bool _bAutoFrostMead; public static bool UsingServerConfig => _usingServer; public static bool ModStatusValue => ModStatus.Value; public static bool AutoEatValue => AutoEat.Value; public static bool PauseNearWorkbenchValue => PauseNearWorkbench.Value; public static bool PauseOnBoatValue => PauseOnBoat.Value; public static bool KeepFoodOnDeathValue => KeepFoodOnDeath.Value; public static bool AutoMeadValue => AutoMead.Value; public static bool AutoPoisonMeadValue => AutoPoisonMead.Value; public static bool AutoFireMeadValue => AutoFireMead.Value; public static bool AutoFrostMeadValue => AutoFrostMead.Value; public static ConfigurationManagerAttributes NewLock() { ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes(); _lockable.Add(configurationManagerAttributes); return configurationManagerAttributes; } public static void ApplyServer(bool modStatus, bool autoEat, bool pauseNearWorkbench, bool pauseOnBoat, bool keepFoodOnDeath, bool autoMead, bool autoPoisonMead, bool autoFireMead, bool autoFrostMead) { if (!_haveBackup) { _bModStatus = ModStatus.Value; _bAutoEat = AutoEat.Value; _bPauseNearWorkbench = PauseNearWorkbench.Value; _bPauseOnBoat = PauseOnBoat.Value; _bKeepFoodOnDeath = KeepFoodOnDeath.Value; _bAutoMead = AutoMead.Value; _bAutoPoisonMead = AutoPoisonMead.Value; _bAutoFireMead = AutoFireMead.Value; _bAutoFrostMead = AutoFrostMead.Value; _haveBackup = true; } ConfigSync.Suppressing = true; try { ModStatus.Value = modStatus; AutoEat.Value = autoEat; PauseNearWorkbench.Value = pauseNearWorkbench; PauseOnBoat.Value = pauseOnBoat; KeepFoodOnDeath.Value = keepFoodOnDeath; AutoMead.Value = autoMead; AutoPoisonMead.Value = autoPoisonMead; AutoFireMead.Value = autoFireMead; AutoFrostMead.Value = autoFrostMead; } finally { ConfigSync.Suppressing = false; } _usingServer = true; } public static void ResetToLocal() { if (_haveBackup) { ConfigSync.Suppressing = true; try { ModStatus.Value = _bModStatus; AutoEat.Value = _bAutoEat; PauseNearWorkbench.Value = _bPauseNearWorkbench; PauseOnBoat.Value = _bPauseOnBoat; KeepFoodOnDeath.Value = _bKeepFoodOnDeath; AutoMead.Value = _bAutoMead; AutoPoisonMead.Value = _bAutoPoisonMead; AutoFireMead.Value = _bAutoFireMead; AutoFrostMead.Value = _bAutoFrostMead; } finally { ConfigSync.Suppressing = false; } _haveBackup = false; } _usingServer = false; } public static void ApplyLocalFromBundle(bool modStatus, bool autoEat, bool pauseNearWorkbench, bool pauseOnBoat, bool keepFoodOnDeath, bool autoMead, bool autoPoisonMead, bool autoFireMead, bool autoFrostMead) { ModStatus.Value = modStatus; AutoEat.Value = autoEat; PauseNearWorkbench.Value = pauseNearWorkbench; PauseOnBoat.Value = pauseOnBoat; KeepFoodOnDeath.Value = keepFoodOnDeath; AutoMead.Value = autoMead; AutoPoisonMead.Value = autoPoisonMead; AutoFireMead.Value = autoFireMead; AutoFrostMead.Value = autoFrostMead; } public static void SetUiLocked(bool locked) { for (int i = 0; i < _lockable.Count; i++) { _lockable[i].ReadOnly = locked; } } public static void RefreshLockState() { ZNet instance = ZNet.instance; SetUiLocked((Object)(object)instance != (Object)null && !instance.IsServer() && _usingServer && !AdminSync.IsAdmin); } } public static class VersionGate { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class OnNewConnectionPatch { private static void Postfix(ZNetPeer peer, ZNet __instance) { if (peer != null && peer.m_rpc != null && !((Object)(object)__instance == (Object)null)) { if (__instance.IsServer()) { peer.m_rpc.Register("HungerPangs_Version", (Action)OnServerReceiveClientVersion); peer.m_rpc.Invoke("HungerPangs_Version", new object[1] { "1.3.0" }); } else { _serverSentVersion = false; peer.m_rpc.Register("HungerPangs_Version", (Action)OnClientReceiveServerVersion); peer.m_rpc.Invoke("HungerPangs_Version", new object[1] { "1.3.0" }); } } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static class RpcPeerInfoPatch { private static bool Prefix(ZRpc rpc, ZNet __instance) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Invalid comparison between Unknown and I4 if (rpc == null || (Object)(object)__instance == (Object)null) { return true; } if (__instance.IsServer()) { if (!_validatedPeers.Contains(rpc)) { ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogWarning((object)"[HungerPangs] Kicking peer without a matching HungerPangs version."); } rpc.Invoke("Error", new object[1] { 3 }); return false; } return true; } if (!_serverSentVersion) { if ((int)ZNet.GetConnectionStatus() != 3) { AccessTools.Field(typeof(ZNet), "m_connectionStatus")?.SetValue(null, (object)(ConnectionStatus)3); } if ((Object)(object)Game.instance != (Object)null) { Game.instance.Logout(true, true); } return false; } return true; } private static void Postfix(ZRpc rpc, ZNet __instance) { if (rpc != null && !((Object)(object)__instance == (Object)null) && __instance.IsServer() && _validatedPeers.Contains(rpc)) { ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)"[HungerPangs] Peer validated; pushing config + admin status."); } OnPeerValidated?.Invoke(rpc); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] private static class ShutdownPatch { private static void Postfix() { _validatedPeers.Clear(); _serverSentVersion = false; } } private const string RpcVersion = "HungerPangs_Version"; private static readonly HashSet _validatedPeers = new HashSet(); private static bool _serverSentVersion; public static Action OnPeerValidated; private static void OnServerReceiveClientVersion(ZRpc rpc, string clientVersion) { if (string.Equals(clientVersion, "1.3.0", StringComparison.Ordinal)) { _validatedPeers.Add(rpc); ManualLogSource log = HungerPangs.Log; if (log != null) { log.LogInfo((object)("[HungerPangs] Client presented matching version " + clientVersion + ".")); } } else { ManualLogSource log2 = HungerPangs.Log; if (log2 != null) { log2.LogWarning((object)("[HungerPangs] Client version '" + clientVersion + "' != server '1.3.0'; will be disconnected at handshake.")); } } } private static void OnClientReceiveServerVersion(ZRpc rpc, string serverVersion) { _serverSentVersion = true; } }