Decompiled source of FoodGuard v0.1.13

FoodGuard.dll

Decompiled 7 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("jg224")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 jg224")]
[assembly: AssemblyDescription("Client-side food-reminder mod for Valheim. Shows a center-screen popup (and an optional combat alert sound) when food is low, when leaving base, or when entering combat without food.")]
[assembly: AssemblyFileVersion("0.1.13.0")]
[assembly: AssemblyInformationalVersion("0.1.13")]
[assembly: AssemblyProduct("FoodGuard")]
[assembly: AssemblyTitle("FoodGuard")]
[assembly: AssemblyVersion("0.1.13.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace FoodGuard
{
	internal static class BaseZoneChecker
	{
		public struct BaseState
		{
			public bool IsInBase;

			public bool LeftBase;
		}

		private const float BaseRadius = 40f;

		private const int FoodGuardBaseValueRequired = 4;

		private static bool? _committedInBase;

		private static bool _prevReading;

		public static BaseState Evaluate(Player localPlayer)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			Vector3 position = ((Component)localPlayer).transform.position;
			BaseState result = new BaseState
			{
				IsInBase = false
			};
			bool flag = (result.IsInBase = EffectArea.GetBaseValue(position, 40f) >= 4);
			if (flag != _prevReading)
			{
				_prevReading = flag;
				return result;
			}
			if (!_committedInBase.HasValue)
			{
				_committedInBase = flag;
			}
			else if (_committedInBase.Value != flag)
			{
				_committedInBase = flag;
				result.LeftBase = !flag;
			}
			return result;
		}

		public static void Reset(bool preserveCommittedState = false)
		{
			bool? flag = (_committedInBase = (preserveCommittedState ? _committedInBase : ((bool?)null)));
			_prevReading = flag == true;
		}
	}
	internal static class BiomeFoodRules
	{
		internal static int GetRequiredSlots(Biome biome)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Invalid comparison between Unknown and I4
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Invalid comparison between Unknown and I4
			if ((int)biome == 1)
			{
				return 1;
			}
			if ((int)biome == 8)
			{
				return 2;
			}
			return 3;
		}

		internal unsafe static string GetDisplayName(Biome biome)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0003: Invalid comparison between Unknown and I4
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Invalid comparison between Unknown and I4
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected I4, but got Unknown
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Invalid comparison between Unknown and I4
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Invalid comparison between Unknown and I4
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Invalid comparison between Unknown and I4
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Invalid comparison between Unknown and I4
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Invalid comparison between Unknown and I4
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Invalid comparison between Unknown and I4
			if ((int)biome <= 16)
			{
				switch (biome - 1)
				{
				default:
					if ((int)biome != 8)
					{
						if ((int)biome != 16)
						{
							break;
						}
						return "Plains";
					}
					return "Black Forest";
				case 0:
					return "Meadows";
				case 1:
					return "Swamp";
				case 3:
					return "Mountains";
				case 2:
					break;
				}
			}
			else if ((int)biome <= 64)
			{
				if ((int)biome == 32)
				{
					return "Ashlands";
				}
				if ((int)biome == 64)
				{
					return "Deep North";
				}
			}
			else
			{
				if ((int)biome == 256)
				{
					return "Ocean";
				}
				if ((int)biome == 512)
				{
					return "Mistlands";
				}
			}
			return ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString();
		}
	}
	internal static class CustomAlertSound
	{
		private static AudioClip _clip;

		private static bool _loadStarted;

		private static bool _warnedUnavailable;

		internal static void BeginLoad(MonoBehaviour host, string path)
		{
			if (!_loadStarted)
			{
				_loadStarted = true;
				if (!File.Exists(path))
				{
					Plugin.Log.LogWarning((object)("Custom alert sound is missing: " + path + ". Popups will still work."));
				}
				else
				{
					host.StartCoroutine(Load(path));
				}
			}
		}

		private static IEnumerator Load(string path)
		{
			string absoluteUri = new Uri(path).AbsoluteUri;
			UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(absoluteUri, (AudioType)14);
			try
			{
				yield return request.SendWebRequest();
				if ((int)request.result != 1)
				{
					Plugin.Log.LogWarning((object)("Could not load EAT.ogg: " + request.error + ". Popups will still work."));
					yield break;
				}
				_clip = DownloadHandlerAudioClip.GetContent(request);
				if ((Object)(object)_clip == (Object)null)
				{
					Plugin.Log.LogWarning((object)"EAT.ogg decoded without an AudioClip. Popups will still work.");
					yield break;
				}
				((Object)_clip).name = "FoodGuard_EAT";
				Plugin.Log.LogInfo((object)($"Loaded custom alert EAT.ogg ({_clip.length:F2}s, {_clip.frequency} Hz, " + $"{_clip.channels} channel(s))."));
			}
			finally
			{
				((IDisposable)request)?.Dispose();
			}
		}

		internal static bool Play()
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Expected O, but got Unknown
			if ((Object)(object)_clip == (Object)null)
			{
				if (!_warnedUnavailable)
				{
					_warnedUnavailable = true;
					Plugin.Log.LogWarning((object)"Custom alert EAT.ogg is not available; sound skipped.");
				}
				return false;
			}
			try
			{
				GameObject val = new GameObject("FoodGuard EAT alert");
				AudioSource obj = val.AddComponent<AudioSource>();
				obj.playOnAwake = false;
				obj.spatialBlend = 0f;
				obj.volume = Plugin.AlertVolume.Value;
				obj.clip = _clip;
				obj.Play();
				Object.Destroy((Object)val, _clip.length + 0.5f);
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Debug("Custom EAT.ogg playback failed (non-fatal): " + ex.Message);
				return false;
			}
		}
	}
	internal static class FoodEatTracker
	{
		[HarmonyPatch(typeof(Player), "EatFood", new Type[] { typeof(ItemData) })]
		private static class EatFoodPatch
		{
			private static void Postfix(Player __instance, bool __result)
			{
				if (__result && __instance == Player.m_localPlayer)
				{
					Plugin.ArmPostEatGrace("successful EatFood");
				}
			}
		}
	}
	internal static class FoodMonitor
	{
		public struct FoodState
		{
			public bool NeedsFood;

			public bool HasNoFood;

			public int LowestRemainingPct;

			public int ActiveFoodCount;

			public int EmptyFoodSlots;

			public List<ExpiringFood> ActiveFoods;

			public List<ExpiringFood> ExpiringFoods;
		}

		public struct ExpiringFood
		{
			public string Key;

			public string Name;

			public int RemainingPct;
		}

		private const int MaxFoodSlots = 3;

		private static readonly FieldInfo FoodsField = AccessTools.Field(typeof(Player), "m_foods");

		public static FoodState Evaluate(Player player)
		{
			FoodState result = new FoodState
			{
				ActiveFoods = new List<ExpiringFood>(),
				ExpiringFoods = new List<ExpiringFood>()
			};
			if (FoodsField == null)
			{
				Plugin.Debug("m_foods field not found via AccessTools; FoodMonitor inactive.");
				return result;
			}
			if (!(FoodsField.GetValue(player) is List<Food> { Count: not 0 } list))
			{
				result.HasNoFood = true;
				result.NeedsFood = true;
				result.EmptyFoodSlots = 3;
				return result;
			}
			float num = Mathf.Clamp01((float)Plugin.FoodThresholdPercent.Value / 100f);
			float num2 = 2f;
			bool flag = false;
			bool flag2 = false;
			int num3 = 0;
			for (int i = 0; i < list.Count; i++)
			{
				Food val = list[i];
				if (val != null && val.m_item != null && val.m_item.m_shared != null)
				{
					float num4 = val.m_item.m_shared.m_foodBurnTime;
					if (num4 <= 0f)
					{
						num4 = 600f;
					}
					float num5 = val.m_time;
					if (num5 < 0f)
					{
						num5 = 0f;
					}
					float num6 = num5 / num4;
					if (num6 > 1f)
					{
						num6 = 1f;
					}
					flag = true;
					num3++;
					if (num6 < num2)
					{
						num2 = num6;
					}
					ExpiringFood item = new ExpiringFood
					{
						Key = GetFoodKey(val, i),
						Name = GetDisplayName(val),
						RemainingPct = Mathf.RoundToInt(num6 * 100f)
					};
					result.ActiveFoods.Add(item);
					if (num6 <= num)
					{
						flag2 = true;
						result.ExpiringFoods.Add(item);
					}
				}
			}
			int num7 = Mathf.Max(0, 3 - num3);
			result.HasNoFood = !flag;
			result.NeedsFood = flag2 || !flag || (Plugin.EmptySlotCountsAsLow.Value && num7 > 0);
			result.ActiveFoodCount = num3;
			result.EmptyFoodSlots = num7;
			result.LowestRemainingPct = (flag ? Mathf.RoundToInt(num2 * 100f) : 0);
			return result;
		}

		private static string GetFoodKey(Food food, int slotIndex)
		{
			if (!string.IsNullOrWhiteSpace(food.m_name))
			{
				return food.m_name;
			}
			if (food.m_item?.m_shared != null && !string.IsNullOrWhiteSpace(food.m_item.m_shared.m_name))
			{
				return food.m_item.m_shared.m_name;
			}
			return $"unknown-food-{slotIndex}";
		}

		private static string GetDisplayName(Food food)
		{
			string text = food.m_name;
			if (string.IsNullOrWhiteSpace(text) && food.m_item?.m_shared != null)
			{
				text = food.m_item.m_shared.m_name;
			}
			if (string.IsNullOrWhiteSpace(text))
			{
				return "Unknown food";
			}
			try
			{
				if (Localization.instance != null)
				{
					text = Localization.instance.Localize(text);
				}
			}
			catch (Exception ex)
			{
				Plugin.Debug("Food name localization failed for '" + text + "': " + ex.Message);
			}
			if (!string.IsNullOrWhiteSpace(text))
			{
				return text;
			}
			return "Unknown food";
		}
	}
	internal static class LocalCombatScanner
	{
		private static readonly FieldInfo BaseAIField = AccessTools.Field(typeof(Character), "m_baseAI");

		private static float? _lastLocalCombatTime;

		private const float CombatClearSeconds = 5f;

		public static bool IsLocalPlayerInCombat(Player localPlayer)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			List<Character> allCharacters = Character.GetAllCharacters();
			if (allCharacters == null)
			{
				return InCombatWithCooldown(realtimeSinceStartup);
			}
			bool flag = false;
			for (int i = 0; i < allCharacters.Count; i++)
			{
				Character val = allCharacters[i];
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				object? obj = BaseAIField?.GetValue(val);
				BaseAI val2 = (BaseAI)((obj is BaseAI) ? obj : null);
				if (!((Object)(object)val2 == (Object)null))
				{
					Character targetCreature = val2.GetTargetCreature();
					if (!((Object)(object)targetCreature == (Object)null) && (object)targetCreature == localPlayer)
					{
						flag = true;
						break;
					}
				}
			}
			if (flag)
			{
				_lastLocalCombatTime = realtimeSinceStartup;
				return true;
			}
			return InCombatWithCooldown(realtimeSinceStartup);
		}

		public static void Reset()
		{
			_lastLocalCombatTime = null;
		}

		private static bool InCombatWithCooldown(float now)
		{
			if (!_lastLocalCombatTime.HasValue)
			{
				return false;
			}
			return now - _lastLocalCombatTime.Value < 5f;
		}
	}
	[BepInPlugin("jg224.FoodGuard", "FoodGuard", "0.1.13")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "jg224.FoodGuard";

		public const string PluginName = "FoodGuard";

		public const string PluginVersion = "0.1.13";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> DebugLogging;

		internal static ConfigEntry<int> FoodThresholdPercent;

		internal static ConfigEntry<bool> EmptySlotCountsAsLow;

		internal static ConfigEntry<float> PollInterval;

		internal static ConfigEntry<float> PostEatGraceSeconds;

		internal static ConfigEntry<float> RespawnGraceSeconds;

		internal static ConfigEntry<float> LoginGraceSeconds;

		internal static ConfigEntry<bool> SuppressLowFoodInBase;

		internal static ConfigEntry<bool> LeaveBaseEnabled;

		internal static ConfigEntry<bool> LowFoodEnabled;

		internal static ConfigEntry<bool> BiomeFoodEnabled;

		internal static ConfigEntry<bool> CombatReadinessEnabled;

		internal static ConfigEntry<bool> AlertSoundEnabled;

		internal static ConfigEntry<float> AlertSoundCooldown;

		internal static ConfigEntry<float> AlertVolume;

		internal static ConfigEntry<float> LeaveBaseCooldown;

		internal static ConfigEntry<float> LowFoodCooldown;

		internal static ConfigEntry<float> BiomeFoodCooldown;

		internal static ConfigEntry<float> CombatReadinessCooldown;

		internal static ConfigEntry<float> PopupSpacingSeconds;

		internal static ConfigEntry<string> LeaveBaseNoRestedMessage;

		internal static ConfigEntry<string> LeaveBaseNoFoodMessage;

		internal static ConfigEntry<string> LeaveBaseNoFoodAndRestedMessage;

		internal static ConfigEntry<string> LowFoodMessage;

		internal static ConfigEntry<string> BiomeFoodMessage;

		internal static ConfigEntry<string> CombatNoRestedMessage;

		internal static ConfigEntry<string> CombatNoFoodMessage;

		internal static ConfigEntry<string> CombatNoFoodAndRestedMessage;

		private static float _nextPoll;

		internal static float _suppressUntil;

		private static int _lastFoodCount = -1;

		private static readonly FieldRef<Player, bool> _isLoadingRef = CreatePlayerFieldRef<bool>("m_isLoading");

		private static readonly FieldRef<Player, float> _timeSinceDeathRef = CreatePlayerFieldRef<float>("m_timeSinceDeath");

		private static bool _wasTransitioning;

		private static bool _inDeathGrace;

		private void Awake()
		{
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Expected O, but got Unknown
			//IL_0274: Unknown result type (might be due to invalid IL or missing references)
			//IL_027e: Expected O, but got Unknown
			//IL_04f1: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Master switch. When false, FoodGuard does nothing.");
			DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DebugMode", false, "Verbose logging for diagnosing triggers. Leave off in normal play.");
			FoodThresholdPercent = ((BaseUnityPlugin)this).Config.Bind<int>("General", "FoodThresholdPercent", 25, new ConfigDescription("A food counts as 'low' when its remaining time is at or under this percent of its total duration. Default 25 = remind when a food has 25% time left. Range 1-99.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 99), Array.Empty<object>()));
			EmptySlotCountsAsLow = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EmptySlotCountsAsLow", false, "When true, an empty food slot (you only ate 2 of 3, or a food just expired) also counts as 'needing food'. Default FALSE: an empty slot is usually a deliberate choice, and the reliable signal is a food actually expiring (caught by FoodThresholdPercent). Set true only if you want nagged for not running a full 3 foods at all times.");
			PollInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PollInterval", 0.5f, "Seconds between evaluation passes. Lower = more responsive, higher = cheaper. 0.5s is a good balance and avoids catching transient states (e.g. mid-teleport) where food or base-zone reads are momentarily unsettled.");
			PostEatGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PostEatGraceSeconds", 15f, "After you eat any food, suppress all reminders for this many seconds. Prevents a popup the instant a slot refreshes (e.g. a 1% food expiring just as you eat a new one). The grace only matters if you're still 'needing food' after eating; if everything's fine, no popup would have fired anyway.");
			RespawnGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RespawnGraceSeconds", 25f, "After you die (and respawn), suppress ALL reminders for this many seconds. On death Valheim wipes your food, so 'needs food' is instantly true -- without this the mod spams you the moment you respawn, before you've had a chance to loot your body and eat. Measured from the moment of death. Default 25s; raise it if you want more breathing room after a death.");
			if (Mathf.Approximately(RespawnGraceSeconds.Value, 60f))
			{
				RespawnGraceSeconds.Value = 25f;
				Log.LogInfo((object)"Migrated RespawnGraceSeconds from the old 60s default to 25s.");
			}
			LoginGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "LoginGraceSeconds", 20f, "After you log in / spawn into the world, suppress ALL reminders for this many seconds. Gives you time to load in and get oriented before any popups appear. Fires on both initial login and post-respawn. Set 0 to disable the grace entirely.");
			SuppressLowFoodInBase = ((BaseUnityPlugin)this).Config.Bind<bool>("Base", "SuppressLowFoodInBase", true, "When true, the plain low-food reminder (#2) stays quiet while you are in base. Only the idle 'food is low' nudge is controlled by this option.");
			LeaveBaseEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "LeaveBaseEnabled", true, "Remind and play EAT.ogg once when you leave base without Rested, without enough food slots for the current biome, or when expiring food would leave too few safe foods. Rearms when you re-enter base.");
			LowFoodEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "LowFoodEnabled", true, "Remind about food at/under FoodThresholdPercent only when the foods remaining above that threshold are below the biome minimum. Suppressed in base if SuppressLowFoodInBase is true.");
			BiomeFoodEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "BiomeFoodEnabled", true, "Remind and play EAT.ogg while away from base when you have fewer than 1 active food in Meadows, fewer than 2 in Black Forest, or fewer than 3 in later biomes.");
			CombatReadinessEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "CombatReadinessEnabled", true, "Remind and play EAT.ogg in combat while away from base when you either lack Rested OR have too few active food slots for the biome (1 Meadows, 2 Black Forest, 3 later). Uses the same 40-meter base scan as the food reminders.");
			AlertSoundEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Sound", "AlertSoundEnabled", true, "Play the bundled EAT.ogg for leave-base readiness (#1), the 10% and 5% low-food milestones (#2), biome food shortage (#3), and combat readiness (#4). Set false for silent operation.");
			AlertSoundCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Sound", "AlertSoundCooldown", 60f, "Minimum seconds between repeated EAT.ogg alerts across both sound-producing reminders.");
			AlertVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Sound", "AlertVolume", 1f, new ConfigDescription("Volume slider for the bundled EAT.ogg alert. 0 = silent, 1 = full volume. The bundled sound has been boosted by 25% and peak-limited to prevent clipping.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			LeaveBaseCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "LeaveBaseCooldown", 30f, "Seconds between repeated leave-base popups (the trigger also rearms on re-enter).");
			LowFoodCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "LowFoodCooldown", 30f, "Minimum seconds between low-food milestone popups. Each food first warns at FoodThresholdPercent, then at every 5 percentage points below it.");
			BiomeFoodCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "BiomeFoodCooldown", 30f, "Seconds between repeated biome food-slot shortage popups while out of base.");
			if (Mathf.Approximately(BiomeFoodCooldown.Value, 45f))
			{
				BiomeFoodCooldown.Value = 30f;
				Log.LogInfo((object)"Migrated BiomeFoodCooldown from the old 45s default to 30s.");
			}
			CombatReadinessCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "CombatReadinessCooldown", 30f, "Seconds between repeated combat readiness popups (sound uses AlertSoundCooldown).");
			PopupSpacingSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "PopupSpacingSeconds", 10f, "MINIMUM seconds between ANY two popups, across all triggers. Center-screen banners overwrite each other instantly, so without this, two triggers firing a few tenths of a second apart (e.g. right after a teleport) would stack and you'd only see the last. 10s gives you time to read each one. Set 0 to allow stacking (not recommended).");
			LeaveBaseNoRestedMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LeaveBaseNoRestedMessage", "LEAVING BASE\nWithout RESTED buff", "Popup shown when leaving base without Rested but the biome food requirement is met.");
			LeaveBaseNoFoodMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LeaveBaseNoFoodMessage", "LEAVING BASE\nWithout FOOD", "Popup shown when leaving base without enough safe food while Rested.");
			LeaveBaseNoFoodAndRestedMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LeaveBaseNoFoodAndRestedMessage", "LEAVING BASE\nWithout FOOD & RESTED buff", "Popup shown when leaving base both without enough safe food and without Rested.");
			LowFoodMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LowFoodMessage", "Food expiring -- time to eat!", "Popup heading. Every active food is appended on its own line with its localized name and remaining percentage in aligned columns at 50% of the heading size and 75% line height, including foods above the warning threshold. Optional '{pct}' is replaced with the lowest percentage.");
			BiomeFoodMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "BiomeFoodMessage", "Not enough food for {biome} -- EAT!", "Popup heading shown with EAT.ogg. Supports {biome}, {active}, and {required} tokens; food-slot details are appended automatically.");
			CombatNoRestedMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "CombatNoRestedMessage", "ENTERING COMBAT\nWithout RESTED buff", "Popup shown when combat begins without Rested but the biome food requirement is met.");
			CombatNoFoodMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "CombatNoFoodMessage", "ENTERING COMBAT\nWithout FOOD", "Popup shown when combat begins below the biome food requirement while Rested.");
			CombatNoFoodAndRestedMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "CombatNoFoodAndRestedMessage", "ENTERING COMBAT\nWithout FOOD & RESTED buff", "Popup shown when combat begins both below the biome food requirement and without Rested.");
			if (CombatNoFoodAndRestedMessage.Value == "ENTERING COMBAT\nWithout FOOD or RESTED buff")
			{
				CombatNoFoodAndRestedMessage.Value = "ENTERING COMBAT\nWithout FOOD & RESTED buff";
			}
			Log.LogInfo((object)("FoodGuard v0.1.13 loaded. BaseDetection=RaidStyle40mMin4, " + $"Threshold={FoodThresholdPercent.Value}%, AlertSound={AlertSoundEnabled.Value}."));
			try
			{
				new Harmony("jg224.FoodGuard").PatchAll();
				Log.LogInfo((object)"Harmony patches applied.");
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Harmony patchall failed (non-fatal; spawn/eat tracking falls back): " + ex.Message));
			}
			string path = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? "";
			CustomAlertSound.BeginLoad((MonoBehaviour)(object)this, Path.Combine(path, "EAT.ogg"));
		}

		internal static void ArmPostEatGrace(string source)
		{
			float num = Mathf.Max(0f, PostEatGraceSeconds.Value);
			float num2 = Time.realtimeSinceStartup + num;
			if (num2 > _suppressUntil)
			{
				_suppressUntil = num2;
			}
			Debug($"Eat detected ({source}); suppressing reminders for {num:F1}s.");
		}

		private void Update()
		{
			if (!Enabled.Value)
			{
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			bool flag = IsPlayerTransitioning(localPlayer);
			if (flag != _wasTransitioning)
			{
				if (flag)
				{
					ReminderEngine.Reset(preserveBaseState: true);
					_nextPoll = Time.realtimeSinceStartup + PollInterval.Value;
					_lastFoodCount = -1;
					Debug("Teleport/load transition detected; state reset.");
				}
				_wasTransitioning = flag;
			}
			if (flag)
			{
				_wasTransitioning = flag;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup < _nextPoll)
			{
				return;
			}
			_nextPoll = realtimeSinceStartup + PollInterval.Value;
			float? timeSinceDeath = GetTimeSinceDeath(localPlayer);
			bool flag2 = false;
			if (timeSinceDeath.HasValue && timeSinceDeath.Value < RespawnGraceSeconds.Value)
			{
				flag2 = true;
				if (!_inDeathGrace)
				{
					_inDeathGrace = true;
					ReminderEngine.Reset();
					Debug($"death grace armed (timeSinceDeath={timeSinceDeath.Value:F1}s); suppressing.");
				}
			}
			else if (_inDeathGrace)
			{
				_inDeathGrace = false;
				ReminderEngine.Reset();
				Debug("death grace elapsed; resuming normal evaluation.");
			}
			FoodMonitor.FoodState food = FoodMonitor.Evaluate(localPlayer);
			if (_lastFoodCount < 0)
			{
				_lastFoodCount = food.ActiveFoodCount;
			}
			else if (food.ActiveFoodCount > _lastFoodCount)
			{
				ArmPostEatGrace($"food count {_lastFoodCount} -> {food.ActiveFoodCount}; fallback");
			}
			_lastFoodCount = food.ActiveFoodCount;
			SpawnTracker.EnsureSpawnTime();
			bool isWithinLoginGrace = SpawnTracker.IsWithinLoginGrace;
			bool suppressed = realtimeSinceStartup < _suppressUntil || flag2 || isWithinLoginGrace;
			ReminderEngine.Tick(food, suppressed);
		}

		private static float? GetTimeSinceDeath(Player local)
		{
			if (_timeSinceDeathRef == null)
			{
				return null;
			}
			return _timeSinceDeathRef.Invoke(local);
		}

		private static bool IsPlayerTransitioning(Player local)
		{
			try
			{
				if (((Character)local).IsTeleporting())
				{
					return true;
				}
			}
			catch
			{
			}
			if (_isLoadingRef != null && _isLoadingRef.Invoke(local))
			{
				return true;
			}
			return false;
		}

		private static FieldRef<Player, T> CreatePlayerFieldRef<T>(string name)
		{
			try
			{
				FieldInfo fieldInfo = AccessTools.Field(typeof(Player), name);
				return (fieldInfo == null) ? null : AccessTools.FieldRefAccess<Player, T>(fieldInfo);
			}
			catch
			{
				return null;
			}
		}

		internal static void Debug(string msg)
		{
			if (DebugLogging.Value)
			{
				Log.LogInfo((object)("[debug] " + msg));
			}
		}
	}
	internal static class ReminderEngine
	{
		private static float _lastLeaveBase = -999f;

		private static float _lastLowFood = -999f;

		private static float _lastBiomeFood = -999f;

		private static float _lastCombatReadiness = -999f;

		private static float _lastAlertSfx = -999f;

		private static float _lastAnyPopup = -999f;

		private const int ConfirmPolls = 2;

		private static int _foodDurationRiskStreak;

		private static int _notEnoughFoodStreak;

		private static int _notRestedStreak;

		private const int LowFoodReminderStepPercent = 5;

		private static readonly Dictionary<string, int> _lastFoodMilestone = new Dictionary<string, int>();

		private static readonly Dictionary<string, int> _lastFoodSoundMilestone = new Dictionary<string, int>();

		internal static void Tick(FoodMonitor.FoodState food, bool suppressed)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_02fe: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			BaseZoneChecker.BaseState baseState = BaseZoneChecker.Evaluate(localPlayer);
			bool isInBase = baseState.IsInBase;
			bool flag = LocalCombatScanner.IsLocalPlayerInCombat(localPlayer);
			bool flag2 = RestStatusChecker.IsRested(localPlayer);
			Biome currentBiome = localPlayer.GetCurrentBiome();
			int requiredSlots = BiomeFoodRules.GetRequiredSlots(currentBiome);
			bool flag3 = food.ActiveFoodCount >= requiredSlots;
			int num = food.ExpiringFoods?.Count ?? 0;
			int num2 = food.ActiveFoodCount - num;
			bool flag4 = num > 0 && num2 < requiredSlots;
			_foodDurationRiskStreak = (flag4 ? (_foodDurationRiskStreak + 1) : 0);
			_notEnoughFoodStreak = ((!flag3) ? (_notEnoughFoodStreak + 1) : 0);
			_notRestedStreak = ((!flag2) ? (_notRestedStreak + 1) : 0);
			bool flag5 = _foodDurationRiskStreak >= 2;
			bool flag6 = _notEnoughFoodStreak >= 2;
			bool flag7 = _notRestedStreak >= 2;
			SyncLowFoodMilestones(food);
			SyncLowFoodSoundMilestones(food);
			Plugin.Debug($"tick: biome={currentBiome} requiredFood={requiredSlots} " + $"active={food.ActiveFoodCount} enough={flag3}" + $"(streak={_notEnoughFoodStreak}) safeFood={num2} " + $"durationRisk={flag4}(streak={_foodDurationRiskStreak}) " + $"lowest={food.LowestRemainingPct}% " + $"rested={flag2}(notRestedStreak={_notRestedStreak}) " + $"inBase={isInBase} leftBase={baseState.LeftBase} combat={flag} " + $"suppressed={suppressed}");
			if (suppressed)
			{
				Plugin.Debug("suppressed by an active grace window; skipping trigger evaluation.");
				return;
			}
			if (Plugin.CombatReadinessEnabled.Value && flag && !isInBase && (flag7 || flag6) && CooldownElapsed(realtimeSinceStartup, _lastCombatReadiness, Plugin.CombatReadinessCooldown.Value))
			{
				string text = BuildCombatReadinessMessage(flag2, flag3);
				if (TryFire(realtimeSinceStartup, text, "_lastCombatReadiness"))
				{
					_lastCombatReadiness = realtimeSinceStartup;
					PlayAlertSound(realtimeSinceStartup, ignoreCooldown: false);
					Plugin.Debug("#4 combat-readiness trigger fired.");
					return;
				}
			}
			bool flag8 = flag7 || flag6 || flag5;
			if (Plugin.LeaveBaseEnabled.Value && baseState.LeftBase && flag8 && CooldownElapsed(realtimeSinceStartup, _lastLeaveBase, Plugin.LeaveBaseCooldown.Value))
			{
				bool hasEnoughSafeFood = flag3 && !flag4;
				string text2 = BuildLeaveBaseMessage(flag2, hasEnoughSafeFood);
				if (TryFire(realtimeSinceStartup, text2, "_lastLeaveBase"))
				{
					_lastLeaveBase = realtimeSinceStartup;
					PlayAlertSound(realtimeSinceStartup, ignoreCooldown: true);
					Plugin.Debug("#1 leave-base-readiness trigger fired.");
					return;
				}
			}
			if (Plugin.BiomeFoodEnabled.Value && !isInBase && flag6 && CooldownElapsed(realtimeSinceStartup, _lastBiomeFood, Plugin.BiomeFoodCooldown.Value))
			{
				string text3 = BuildBiomeFoodMessage(food, currentBiome, requiredSlots);
				if (TryFire(realtimeSinceStartup, text3, "_lastBiomeFood"))
				{
					_lastBiomeFood = realtimeSinceStartup;
					PlayAlertSound(realtimeSinceStartup, ignoreCooldown: false);
					Plugin.Debug("#3 biome-food trigger fired.");
					return;
				}
			}
			bool flag9 = food.ExpiringFoods != null && food.ExpiringFoods.Count > 0;
			bool flag10 = flag4 && HasDueLowFoodMilestone(food);
			if (Plugin.LowFoodEnabled.Value && flag5 && flag10 && (!isInBase || !Plugin.SuppressLowFoodInBase.Value) && CooldownElapsed(realtimeSinceStartup, _lastLowFood, Plugin.LowFoodCooldown.Value) && TryFire(realtimeSinceStartup, BuildLowFoodMessage(food), "_lastLowFood"))
			{
				_lastLowFood = realtimeSinceStartup;
				bool flag11 = flag9 && HasDueLowFoodSoundMilestone(food);
				if (flag9)
				{
					RecordLowFoodMilestones(food);
				}
				if (flag11)
				{
					RecordLowFoodSoundMilestones(food);
					PlayAlertSound(realtimeSinceStartup, ignoreCooldown: true);
				}
				Plugin.Debug($"#2 low-food trigger fired; soundDue={flag11}.");
			}
		}

		private static string BuildLeaveBaseMessage(bool isRested, bool hasEnoughSafeFood)
		{
			if (!isRested && !hasEnoughSafeFood)
			{
				return Plugin.LeaveBaseNoFoodAndRestedMessage.Value ?? "LEAVING BASE\nWithout FOOD & RESTED buff";
			}
			if (!isRested)
			{
				return Plugin.LeaveBaseNoRestedMessage.Value ?? "LEAVING BASE\nWithout RESTED buff";
			}
			return Plugin.LeaveBaseNoFoodMessage.Value ?? "LEAVING BASE\nWithout FOOD";
		}

		private static string BuildBiomeFoodMessage(FoodMonitor.FoodState food, Biome biome, int requiredFoodSlots)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			string displayName = BiomeFoodRules.GetDisplayName(biome);
			string text = ReplaceStatusTokens(Plugin.BiomeFoodMessage.Value ?? "Not enough food -- EAT!", food, displayName, requiredFoodSlots);
			return text + "\n<size=50%>- Food slots: " + food.ActiveFoodCount + "/" + requiredFoodSlots + " required (" + displayName + ")</size>";
		}

		private static string BuildCombatReadinessMessage(bool isRested, bool hasEnoughFoodSlots)
		{
			if (!isRested && !hasEnoughFoodSlots)
			{
				return Plugin.CombatNoFoodAndRestedMessage.Value ?? "ENTERING COMBAT\nWithout FOOD & RESTED buff";
			}
			if (!isRested)
			{
				return Plugin.CombatNoRestedMessage.Value ?? "ENTERING COMBAT\nWithout RESTED buff";
			}
			return Plugin.CombatNoFoodMessage.Value ?? "ENTERING COMBAT\nWithout FOOD";
		}

		private static string ReplaceStatusTokens(string text, FoodMonitor.FoodState food, string biomeName, int requiredFoodSlots)
		{
			return text.Replace("{biome}", biomeName).Replace("{active}", food.ActiveFoodCount.ToString()).Replace("{required}", requiredFoodSlots.ToString());
		}

		private static string BuildLowFoodMessage(FoodMonitor.FoodState food)
		{
			string text = (Plugin.LowFoodMessage.Value ?? "Time to eat!").Replace("{pct}", food.LowestRemainingPct.ToString());
			if (food.ActiveFoods == null || food.ActiveFoods.Count == 0)
			{
				return text;
			}
			int num = 0;
			for (int i = 0; i < food.ActiveFoods.Count; i++)
			{
				string text2 = food.ActiveFoods[i].Name ?? "Food";
				if (text2.Length > num)
				{
					num = text2.Length;
				}
			}
			text += "\n<size=50%><line-height=75%><align=center><mspace=0.65em>";
			for (int j = 0; j < food.ActiveFoods.Count; j++)
			{
				FoodMonitor.ExpiringFood expiringFood = food.ActiveFoods[j];
				if (j > 0)
				{
					text += "\n";
				}
				string text3 = expiringFood.Name ?? "Food";
				string text4 = expiringFood.RemainingPct + "%";
				text = text + "- " + text3 + new string('\u00a0', num - text3.Length) + "\u00a0\u00a0" + text4 + new string('\u00a0', Mathf.Max(0, 4 - text4.Length));
			}
			return text + "</mspace></align></line-height></size>";
		}

		private static bool HasDueLowFoodMilestone(FoodMonitor.FoodState food)
		{
			for (int i = 0; i < food.ExpiringFoods.Count; i++)
			{
				FoodMonitor.ExpiringFood expiringFood = food.ExpiringFoods[i];
				if (!_lastFoodMilestone.TryGetValue(expiringFood.Key, out var value))
				{
					return true;
				}
				if (value > 0 && expiringFood.RemainingPct <= value - 5)
				{
					return true;
				}
			}
			return false;
		}

		private static void RecordLowFoodMilestones(FoodMonitor.FoodState food)
		{
			int value = Plugin.FoodThresholdPercent.Value;
			for (int i = 0; i < food.ExpiringFoods.Count; i++)
			{
				FoodMonitor.ExpiringFood expiringFood = food.ExpiringFoods[i];
				if (!_lastFoodMilestone.TryGetValue(expiringFood.Key, out var value2) || (value2 > 0 && expiringFood.RemainingPct <= value2 - 5))
				{
					int num = Mathf.Max(0, (value - expiringFood.RemainingPct) / 5);
					_lastFoodMilestone[expiringFood.Key] = Mathf.Max(0, value - num * 5);
				}
			}
		}

		private static void SyncLowFoodMilestones(FoodMonitor.FoodState food)
		{
			if (_lastFoodMilestone.Count == 0)
			{
				return;
			}
			HashSet<string> hashSet = new HashSet<string>();
			if (food.ExpiringFoods != null)
			{
				for (int i = 0; i < food.ExpiringFoods.Count; i++)
				{
					hashSet.Add(food.ExpiringFoods[i].Key);
				}
			}
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, int> item in _lastFoodMilestone)
			{
				if (!hashSet.Contains(item.Key))
				{
					list.Add(item.Key);
				}
			}
			for (int j = 0; j < list.Count; j++)
			{
				_lastFoodMilestone.Remove(list[j]);
			}
		}

		private static bool HasDueLowFoodSoundMilestone(FoodMonitor.FoodState food)
		{
			if (food.ActiveFoods == null)
			{
				return false;
			}
			for (int i = 0; i < food.ActiveFoods.Count; i++)
			{
				FoodMonitor.ExpiringFood expiringFood = food.ActiveFoods[i];
				int num = ((expiringFood.RemainingPct <= 5) ? 5 : ((expiringFood.RemainingPct <= 10) ? 10 : 0));
				if (num != 0 && (!_lastFoodSoundMilestone.TryGetValue(expiringFood.Key, out var value) || value > num))
				{
					return true;
				}
			}
			return false;
		}

		private static void RecordLowFoodSoundMilestones(FoodMonitor.FoodState food)
		{
			for (int i = 0; i < food.ActiveFoods.Count; i++)
			{
				FoodMonitor.ExpiringFood expiringFood = food.ActiveFoods[i];
				int num = ((expiringFood.RemainingPct <= 5) ? 5 : ((expiringFood.RemainingPct <= 10) ? 10 : 0));
				if (num != 0 && (!_lastFoodSoundMilestone.TryGetValue(expiringFood.Key, out var value) || value > num))
				{
					_lastFoodSoundMilestone[expiringFood.Key] = num;
				}
			}
		}

		private static void SyncLowFoodSoundMilestones(FoodMonitor.FoodState food)
		{
			if (_lastFoodSoundMilestone.Count == 0)
			{
				return;
			}
			HashSet<string> hashSet = new HashSet<string>();
			if (food.ActiveFoods != null)
			{
				for (int i = 0; i < food.ActiveFoods.Count; i++)
				{
					if (food.ActiveFoods[i].RemainingPct <= 10)
					{
						hashSet.Add(food.ActiveFoods[i].Key);
					}
				}
			}
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, int> item in _lastFoodSoundMilestone)
			{
				if (!hashSet.Contains(item.Key))
				{
					list.Add(item.Key);
				}
			}
			for (int j = 0; j < list.Count; j++)
			{
				_lastFoodSoundMilestone.Remove(list[j]);
			}
		}

		private static void PlayAlertSound(float now, bool ignoreCooldown)
		{
			if (Plugin.AlertSoundEnabled.Value && (ignoreCooldown || CooldownElapsed(now, _lastAlertSfx, Plugin.AlertSoundCooldown.Value)) && CustomAlertSound.Play())
			{
				_lastAlertSfx = now;
			}
		}

		private static bool TryFire(float now, string text, string triggerName)
		{
			float value = Plugin.PopupSpacingSeconds.Value;
			if (!CooldownElapsed(now, _lastAnyPopup, value))
			{
				Plugin.Debug(triggerName + " ready but blocked by global popup spacer (" + (_lastAnyPopup + value - now).ToString("F1") + "s remaining).");
				return false;
			}
			if (!ShowPopup(text))
			{
				return false;
			}
			_lastAnyPopup = now;
			return true;
		}

		public static void Reset(bool preserveBaseState = false)
		{
			_lastLeaveBase = -999f;
			_lastLowFood = -999f;
			_lastBiomeFood = -999f;
			_lastCombatReadiness = -999f;
			_lastAlertSfx = -999f;
			_lastAnyPopup = -999f;
			_foodDurationRiskStreak = 0;
			_notEnoughFoodStreak = 0;
			_notRestedStreak = 0;
			_lastFoodMilestone.Clear();
			_lastFoodSoundMilestone.Clear();
			LocalCombatScanner.Reset();
			BaseZoneChecker.Reset(preserveBaseState);
		}

		private static bool CooldownElapsed(float now, float last, float cooldown)
		{
			return now - last >= cooldown;
		}

		private static bool ShowPopup(string text)
		{
			MessageHud instance = MessageHud.instance;
			if ((Object)(object)instance == (Object)null)
			{
				Plugin.Debug("MessageHud not ready; popup skipped without starting its cooldown.");
				return false;
			}
			try
			{
				instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false);
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Debug("ShowMessage failed: " + ex.Message);
				return false;
			}
		}
	}
	internal static class RestStatusChecker
	{
		private static readonly FieldInfo SemanField = AccessTools.Field(typeof(Character), "m_seman");

		public static bool IsRested(Player local)
		{
			if (SemanField == null)
			{
				Plugin.Debug("m_seman field not found via AccessTools; rest check inactive.");
				return true;
			}
			object? value = SemanField.GetValue(local);
			SEMan val = (SEMan)((value is SEMan) ? value : null);
			if (val == null)
			{
				return true;
			}
			int s_statusEffectRested = SEMan.s_statusEffectRested;
			if (s_statusEffectRested == 0)
			{
				return true;
			}
			try
			{
				return val.HaveStatusEffect(s_statusEffectRested);
			}
			catch (Exception ex)
			{
				Plugin.Debug("HaveStatusEffect threw (non-fatal, treated as rested): " + ex.Message);
				return true;
			}
		}
	}
	internal static class SpawnTracker
	{
		[HarmonyPatch(typeof(Player), "OnSpawned")]
		private static class OnSpawnedPatch
		{
			private static void Postfix(Player __instance)
			{
				if (__instance == Player.m_localPlayer)
				{
					LastSpawnTime = Time.realtimeSinceStartup;
					Plugin.Debug($"SpawnTracker: OnSpawned fired for local player at {LastSpawnTime.Value:F1}.");
				}
			}
		}

		public static float? LastSpawnTime;

		public static bool IsWithinLoginGrace
		{
			get
			{
				if (!LastSpawnTime.HasValue)
				{
					return true;
				}
				return Time.realtimeSinceStartup - LastSpawnTime.Value < Plugin.LoginGraceSeconds.Value;
			}
		}

		public static void EnsureSpawnTime()
		{
			if (!LastSpawnTime.HasValue)
			{
				LastSpawnTime = Time.realtimeSinceStartup;
				Plugin.Debug("SpawnTracker: fallback spawn stamp set (no OnSpawned yet).");
			}
		}
	}
}