Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Hunger Pangs v1.3.0
plugins/HungerPangs.dll
Decompiled a week agousing 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<bool>("HungerPangs_Admin", (Action<ZRpc, bool>)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<ZNetPeer> list = _peersField?.GetValue(instance) as List<ZNetPeer>; 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<ZPackage>("HungerPangs_AdminConfig", (Action<ZRpc, ZPackage>)OnAdminConfig); return; } _serverRpc = peer.m_rpc; peer.m_rpc.Register<ZPackage>("HungerPangs_Config", (Action<ZRpc, ZPackage>)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<ZNetPeer> 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<ZNetPeer> Peers(ZNet net) { try { _peersField = _peersField ?? typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.NonPublic); return _peersField?.GetValue(net) as List<ZNetPeer>; } 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<Food> __state) { __state = null; try { if (ServerConfig.ModStatusValue && ServerConfig.KeepFoodOnDeathValue && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { List<Food> foods = __instance.GetFoods(); if (foods != null && foods.Count > 0) { __state = new List<Food>(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<Food> __state) { try { if (__state != null && __state.Count != 0) { List<Food> 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<bool> modStatus; private ConfigEntry<bool> autoEat; private ConfigEntry<bool> foodExpiryNotify; private ConfigEntry<int> autoEatPercent; private ConfigEntry<int> foodExpiryPercent; private ConfigEntry<bool> autoEatNotify; private ConfigEntry<bool> lowSupplyNotify; private ConfigEntry<int> lowSupplyCount; private ConfigEntry<bool> autoMead; private ConfigEntry<int> autoMeadHealthPercent; private ConfigEntry<bool> autoMeadOnlyOnEnemyHit; private ConfigEntry<float> autoMeadEnemyHitWindow; private ConfigEntry<bool> autoMeadRequireMaxHealth; private ConfigEntry<bool> autoMeadNotify; private ConfigEntry<bool> autoPoisonMead; private ConfigEntry<float> autoPoisonMeadRange; private ConfigEntry<bool> autoPoisonMeadNotify; private ConfigEntry<bool> autoFireMead; private ConfigEntry<float> autoFireMeadRange; private ConfigEntry<bool> autoFireMeadNotify; private ConfigEntry<bool> autoFrostMead; private ConfigEntry<int> autoFrostMeadTickCount; private ConfigEntry<float> autoFrostMeadTickWindow; private ConfigEntry<bool> autoFrostMeadNotify; private ConfigEntry<bool> pauseFoodNearWorkbench; private ConfigEntry<bool> pauseFoodOnBoat; private ConfigEntry<bool> keepFoodOnDeath; private ConfigEntry<bool> scaleWithTimeControl; private static float _lastEnemyHitTime = float.MinValue; private static ManualLogSource _log; internal static ManualLogSource Log; private static ConfigEntry<bool> 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<Character> _nearbyCharacterBuffer = new List<Character>(); private static readonly Queue<float> _frostHitTimes = new Queue<float>(); private static readonly HashSet<string> _poisonEnemyPrefabs = new HashSet<string>(); private static readonly HashSet<string> _fireEnemyPrefabs = new HashSet<string>(); private static readonly HashSet<string> _checkedPrefabs = new HashSet<string>(); private const float MeleeRange = 3f; private const float MeleeRangeSq = 9f; private readonly HashSet<string> hasShown = new HashSet<string>(); private readonly HashSet<string> seenThisTick = new HashSet<string>(); private readonly HashSet<string> lowSupplyShown = new HashSet<string>(); private Predicate<string> _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<bool>("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<bool>("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<int>("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<int>(1, 49), Array.Empty<object>())); autoEatNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "04. Auto-Eat Notify", true, "Show a HUD notification when a food item is automatically eaten. Requires AutomaticallyEat."); foodExpiryNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("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<int>("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<int>(1, 50), Array.Empty<object>())); lowSupplyNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("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<int>("General", "08. Low Supply Count", 1, "The number of food items in your inventory for the LowSupplyNotification to appear."); pauseFoodNearWorkbench = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("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<bool>("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<bool>("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<bool>("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<int>("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<int>(1, 99), Array.Empty<object>())); autoMeadOnlyOnEnemyHit = ((BaseUnityPlugin)this).Config.Bind<bool>("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<float>("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<float>(1f, 30f), Array.Empty<object>())); autoMeadRequireMaxHealth = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("Health Mead", "06. Notify", true, "Show a HUD notification when a health mead is automatically consumed."); autoPoisonMead = ((BaseUnityPlugin)this).Config.Bind<bool>("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<float>("Poison Resist Mead", "02. Detection Range", 4f, new ConfigDescription("Radius in meters to scan for poison-capable enemies.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 50f), Array.Empty<object>())); autoPoisonMeadNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("Poison Resist Mead", "03. Notify", true, "Show a HUD notification when a poison resist mead is automatically consumed."); autoFireMead = ((BaseUnityPlugin)this).Config.Bind<bool>("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<float>("Fire Resist Mead", "02. Detection Range", 15f, new ConfigDescription("Radius in meters to scan for fire-capable enemies.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 50f), Array.Empty<object>())); autoFireMeadNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("Fire Resist Mead", "03. Notify", true, "Show a HUD notification when a fire resist mead is automatically consumed."); autoFrostMead = ((BaseUnityPlugin)this).Config.Bind<bool>("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<int>("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<int>(1, 10), Array.Empty<object>())); autoFrostMeadTickWindow = ((BaseUnityPlugin)this).Config.Bind<float>("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<float>(2f, 60f), Array.Empty<object>())); autoFrostMeadNotify = ((BaseUnityPlugin)this).Config.Bind<bool>("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<Food> 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<ItemData> 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<MonsterAI>(); 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<bool> 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<ItemData> 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<ItemData> list = new List<ItemData>(); 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<ItemData> 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<ItemData> list = new List<ItemData>(); 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<string> list = null; Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly); foreach (Type type in typesFromAssembly) { try { List<MethodInfo> 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<string>())).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<bool> ModStatus; public static ConfigEntry<bool> AutoEat; public static ConfigEntry<bool> PauseNearWorkbench; public static ConfigEntry<bool> PauseOnBoat; public static ConfigEntry<bool> KeepFoodOnDeath; public static ConfigEntry<bool> AutoMead; public static ConfigEntry<bool> AutoPoisonMead; public static ConfigEntry<bool> AutoFireMead; public static ConfigEntry<bool> AutoFrostMead; private static readonly List<ConfigurationManagerAttributes> _lockable = new List<ConfigurationManagerAttributes>(); 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<string>("HungerPangs_Version", (Action<ZRpc, string>)OnServerReceiveClientVersion); peer.m_rpc.Invoke("HungerPangs_Version", new object[1] { "1.3.0" }); } else { _serverSentVersion = false; peer.m_rpc.Register<string>("HungerPangs_Version", (Action<ZRpc, string>)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<ZRpc> _validatedPeers = new HashSet<ZRpc>(); private static bool _serverSentVersion; public static Action<ZRpc> 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; } }