Decompiled source of foodguard v0.1.2

FoodGuard.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
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;

[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.2.0")]
[assembly: AssemblyInformationalVersion("0.1.2")]
[assembly: AssemblyProduct("FoodGuard")]
[assembly: AssemblyTitle("FoodGuard")]
[assembly: AssemblyVersion("0.1.2.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;

			public float DistanceToBase;
		}

		private const Type CraftingStationFlag = (Type)4;

		private const Type BuildingFlag = (Type)64;

		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)
			//IL_000b: 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_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			Vector3 position = ((Component)localPlayer).transform.position;
			BaseState result = new BaseState
			{
				IsInBase = false
			};
			string text = (Plugin.BaseZoneMode.Value ?? "Marked").Trim();
			float dist = -1f;
			bool flag = text switch
			{
				"CraftingStation" => InsideAny(position, (Type)4), 
				"Building" => InsideAny(position, (Type)64), 
				"Both" => InsideAny(position, (Type)4) || InsideAny(position, (Type)64), 
				_ => InsideMarked(position, out dist), 
			};
			result.IsInBase = flag;
			result.DistanceToBase = dist;
			if (flag != _prevReading)
			{
				_prevReading = flag;
				return result;
			}
			if (!_committedInBase.HasValue)
			{
				_committedInBase = flag;
			}
			else if (_committedInBase.Value != flag)
			{
				_committedInBase = flag;
				result.LeftBase = !flag;
			}
			return result;
		}

		private static bool InsideMarked(Vector3 pos, out float dist)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			dist = -1f;
			if (!TryParseCenter(Plugin.MarkedBaseCenter.Value, out var center))
			{
				return false;
			}
			dist = Vector3.Distance(pos, center);
			return dist <= Plugin.MarkedBaseRadius.Value;
		}

		private static bool TryParseCenter(string s, out Vector3 center)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			center = Vector3.zero;
			if (string.IsNullOrWhiteSpace(s))
			{
				return false;
			}
			string[] array = s.Split(',');
			if (array.Length != 3)
			{
				return false;
			}
			if (!float.TryParse(array[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || !float.TryParse(array[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2) || !float.TryParse(array[2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result3))
			{
				return false;
			}
			center = new Vector3(result, result2, result3);
			return true;
		}

		private static bool InsideAny(Vector3 pos, Type flag)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			return (Object)(object)EffectArea.IsPointInsideArea(pos, flag, 0f) != (Object)null;
		}

		public static void Reset()
		{
			_committedInBase = null;
			_prevReading = false;
		}
	}
	internal static class FoodMonitor
	{
		public struct FoodState
		{
			public bool NeedsFood;

			public bool HasNoFood;

			public int LowestRemainingPct;

			public int ActiveFoodCount;

			public int EmptyFoodSlots;
		}

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

		public static FoodState Evaluate(Player player)
		{
			FoodState result = default(FoodState);
			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;
				return result;
			}
			float num = (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)
				{
					if (Plugin.EmptySlotCountsAsLow.Value)
					{
						flag2 = true;
					}
					continue;
				}
				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;
				}
				if (num6 <= num)
				{
					flag2 = true;
				}
			}
			result.HasNoFood = !flag;
			result.NeedsFood = flag2 || (Plugin.EmptySlotCountsAsLow.Value && !flag);
			result.ActiveFoodCount = num3;
			int num7 = list.Count;
			if (num7 < num3)
			{
				num7 = num3;
			}
			result.EmptyFoodSlots = num7 - num3;
			result.LowestRemainingPct = (flag ? Mathf.RoundToInt(num2 * 100f) : 0);
			return result;
		}
	}
	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.2")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "jg224.FoodGuard";

		public const string PluginName = "FoodGuard";

		public const string PluginVersion = "0.1.2";

		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<string> BaseZoneMode;

		internal static ConfigEntry<bool> SuppressLowFoodInBase;

		internal static ConfigEntry<KeyCode> MarkBaseHotkey;

		internal static ConfigEntry<string> MarkedBaseCenter;

		internal static ConfigEntry<float> MarkedBaseRadius;

		internal static ConfigEntry<bool> RemindIfUnmarked;

		internal static ConfigEntry<float> UnmarkedReminderCooldown;

		internal static ConfigEntry<bool> LeaveBaseEnabled;

		internal static ConfigEntry<bool> LowFoodEnabled;

		internal static ConfigEntry<bool> CombatEnabled;

		internal static ConfigEntry<int> CombatMinEmptySlots;

		internal static ConfigEntry<bool> NoFoodOutEnabled;

		internal static ConfigEntry<bool> CombatNoRestEnabled;

		internal static ConfigEntry<bool> CombatSoundEnabled;

		internal static ConfigEntry<float> CombatSoundCooldown;

		internal static ConfigEntry<string> AlertSfxName;

		internal static ConfigEntry<float> LeaveBaseCooldown;

		internal static ConfigEntry<float> LowFoodCooldown;

		internal static ConfigEntry<float> CombatCooldown;

		internal static ConfigEntry<float> NoFoodOutCooldown;

		internal static ConfigEntry<float> CombatNoRestCooldown;

		internal static ConfigEntry<float> PopupSpacingSeconds;

		internal static ConfigEntry<string> LeaveBaseMessage;

		internal static ConfigEntry<string> LowFoodMessage;

		internal static ConfigEntry<string> CombatMessage;

		internal static ConfigEntry<string> NoFoodOutMessage;

		internal static ConfigEntry<string> CombatNoRestMessage;

		private static float _nextPoll;

		internal static float _suppressUntil;

		private static int _lastFoodCount = -1;

		private static bool _markKeyDown;

		private static FieldInfo _isLoadingField;

		private static bool _wasTransitioning;

		private static FieldInfo _timeSinceDeathField;

		private static bool _inDeathGrace;

		private void Awake()
		{
			//IL_056f: 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, "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.");
			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", 60f, "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. 60s is usually enough to get reoriented; raise it if you want more breathing room after a death.");
			LoginGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "LoginGraceSeconds", 20f, "After you log in / spawn into the world, suppress ALL reminders (including the 'mark your base' nudge) for this many seconds. Gives you time to load in, get oriented, and -- if you haven't yet -- mark your base with F7 before any popups appear. Fires on both initial login and post-respawn. Set 0 to disable the grace entirely.");
			BaseZoneMode = ((BaseUnityPlugin)this).Config.Bind<string>("Base", "BaseZoneMode", "Marked", "What counts as 'base' for the quiet zone and the leave-base trigger. Marked (default, recommended) = a single location YOU mark with the MarkBaseHotkey. Only that spot counts -- farms, outposts, and other players' bases never will. CraftingStation = inside any workbench/forge effect area (broad; trips on farms). Building = inside a warm/roofed interior (WarmCozyArea). Both = either of the two above.");
			MarkBaseHotkey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Base", "MarkBaseHotkey", (KeyCode)288, "Stand at the center of your main base and press this key once to mark it. Your current position is saved to MarkedBaseCenter and used as the base center (only matters when BaseZoneMode = Marked). Re-mark any time you move base.");
			MarkedBaseCenter = ((BaseUnityPlugin)this).Config.Bind<string>("Base", "MarkedBaseCenter", "", "The marked base center as 'x,y,z'. Auto-filled by the MarkBaseHotkey; you usually don't edit this by hand. Empty (default) means no base is marked yet -- nothing counts as base until you press the hotkey once at your base.");
			MarkedBaseRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Base", "MarkedBaseRadius", 30f, "Radius in meters around the marked base center that counts as 'base'. 30 covers a typical main base. Increase for a sprawling base, decrease for a compact one.");
			RemindIfUnmarked = ((BaseUnityPlugin)this).Config.Bind<bool>("Base", "RemindIfUnmarked", true, "When true (and BaseZoneMode = Marked), periodically show a popup reminding you to press the MarkBaseHotkey at your base if you haven't marked one yet. Without a mark, NOTHING counts as base -- so the low-food nudge won't be suppressed at home. This nudge stops the moment you mark a base. Set false to silence it.");
			UnmarkedReminderCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Base", "UnmarkedReminderCooldown", 300f, "Seconds between 'mark your base' reminder popups (only when RemindIfUnmarked is true and no base is marked). Default 300s = once every 5 minutes. Raise to nag less, lower to nag more.");
			SuppressLowFoodInBase = ((BaseUnityPlugin)this).Config.Bind<bool>("Base", "SuppressLowFoodInBase", true, "When true, the plain low-food reminder (#2) stays quiet while you are in base. The leave-base and combat triggers still fire -- only the idle 'food is low' nudge is suppressed at home.");
			LeaveBaseEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "LeaveBaseEnabled", true, "Remind once when you leave base while needing food. Fires once per base-exit and rearms when you re-enter base.");
			LowFoodEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "LowFoodEnabled", true, "Remind when any food is at/under FoodThresholdPercent remaining. Suppressed in base if SuppressLowFoodInBase is true.");
			CombatEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "CombatEnabled", true, "Remind (and play the alert sound) when you are in combat AND have at least CombatMinEmptySlots empty food slots. Never suppressed by base.");
			CombatMinEmptySlots = ((BaseUnityPlugin)this).Config.Bind<int>("Triggers", "CombatMinEmptySlots", 2, "Number of EMPTY food slots (of 3) required for the combat+food trigger (#3) to fire. Default 2 = only nag when 2+ slots are empty (i.e. you have 1 or 0 foods eaten). Set 1 to nag when any slot is empty, 3 to nag only when you have no food at all.");
			NoFoodOutEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "NoFoodOutEnabled", true, "Remind when you have NO food eaten at all and you are away from base.");
			CombatNoRestEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "CombatNoRestEnabled", true, "Remind (and play the alert sound) when you are in combat AND do not have the Rested buff. Going into combat without Rested means no health/stamina regen bonus. Never suppressed by base, death/teleport/eat grace still apply.");
			CombatSoundEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Sound", "CombatSoundEnabled", true, "Play an alert sound ONLY for the combat + low-food case (#3), per design. All other triggers are text-only. Set false for silent operation.");
			CombatSoundCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Sound", "CombatSoundCooldown", 60f, "Minimum seconds between repeated combat alert sounds while the condition persists. Prevents an audio loop if you stay in combat with low food.");
			AlertSfxName = ((BaseUnityPlugin)this).Config.Bind<string>("Sound", "AlertSfxName", "sfx_perfectblock", "Valheim prefab name of the alert sound effect. Resolved via ZNetScene.GetPrefab. Must be a networked prefab that contains an AudioSource. Default sfx_perfectblock = the sharp metallic perfect-block ring. If it can't be found, the sound is skipped silently (the popup still shows).");
			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, "Seconds between repeated plain low-food popups.");
			CombatCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "CombatCooldown", 30f, "Seconds between repeated combat popups (sound uses CombatSoundCooldown separately).");
			NoFoodOutCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "NoFoodOutCooldown", 45f, "Seconds between repeated 'no food at all' popups while out of base.");
			CombatNoRestCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Cooldowns", "CombatNoRestCooldown", 30f, "Seconds between repeated combat-no-rest popups (sound uses the shared CombatSoundCooldown).");
			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. 5s gives you time to read each one. Set 0 to allow stacking (not recommended).");
			LeaveBaseMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LeaveBaseMessage", "You left base without food -- EAT NOW before you head out!", "Popup text. Plain text; no substitution tokens.");
			LowFoodMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "LowFoodMessage", "Food at {pct}% -- time to eat!", "Popup text. '{pct}' is replaced with the lowest remaining-food percentage (integer).");
			CombatMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "CombatMessage", "COMBAT with too few foods eaten -- EAT NOW!", "Popup text. Shown alongside the alert sound.");
			NoFoodOutMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "NoFoodOutMessage", "You have no food eaten and you're away from base -- EAT!", "Popup text.");
			CombatNoRestMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "CombatNoRestMessage", "COMBAT without Rested -- get to a fire/shelter first!", "Popup text. Shown alongside the alert sound (same channel as combat+low-food).");
			Log.LogInfo((object)("FoodGuard v0.1.2 loaded. BaseZoneMode=" + BaseZoneMode.Value + ", " + $"Threshold={FoodThresholdPercent.Value}%, CombatSound={CombatSoundEnabled.Value}."));
			string text = (BaseZoneMode.Value ?? "").Trim();
			switch (text)
			{
			case "Both":
			case "CraftingStation":
			case "Building":
				Log.LogInfo((object)("[hint] BaseZoneMode is '" + text + "' (carried from an earlier FoodGuard build). Set it to 'Marked' and press F7 at your base for strict per-base detection (farms/outposts won't count). Current mode treats any matching area as base."));
				break;
			}
			try
			{
				new Harmony("jg224.FoodGuard").PatchAll();
				Log.LogInfo((object)"Harmony patches applied.");
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Harmony patchall failed (non-fatal; spawn-time tracking falls back): " + ex.Message));
			}
		}

		private static void MarkBaseHere(Player local)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			Vector3 position = ((Component)local).transform.position;
			string text = $"{position.x:F1},{position.y:F1},{position.z:F1}";
			MarkedBaseCenter.Value = text;
			BaseZoneChecker.Reset();
			string text2 = $"FoodGuard: main base marked here ({text}). Radius {MarkedBaseRadius.Value:F0}m.";
			try
			{
				MessageHud instance = MessageHud.instance;
				if (instance != null)
				{
					instance.ShowMessage((MessageType)2, text2, 0, (Sprite)null, false);
				}
			}
			catch
			{
			}
			Log.LogInfo((object)$"[mark] base marked at {text}, radius {MarkedBaseRadius.Value}.");
		}

		private void Update()
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			if (!Enabled.Value)
			{
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			if ((int)MarkBaseHotkey.Value != 0)
			{
				bool keyDown = Input.GetKeyDown(MarkBaseHotkey.Value);
				if (keyDown && !_markKeyDown)
				{
					_markKeyDown = true;
					MarkBaseHere(localPlayer);
				}
				else if (!keyDown)
				{
					_markKeyDown = false;
				}
			}
			bool flag = IsPlayerTransitioning(localPlayer);
			if (flag != _wasTransitioning)
			{
				if (flag)
				{
					ReminderEngine.Reset();
					_nextPoll = Time.realtimeSinceStartup + PollInterval.Value;
					_lastFoodCount = -1;
					Debug("Teleport/load transition detected; state reset.");
				}
				_wasTransitioning = flag;
			}
			if (flag)
			{
				_wasTransitioning = flag;
				return;
			}
			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.");
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!(realtimeSinceStartup < _nextPoll))
			{
				_nextPoll = realtimeSinceStartup + PollInterval.Value;
				FoodMonitor.FoodState food = FoodMonitor.Evaluate(localPlayer);
				if (_lastFoodCount < 0)
				{
					_lastFoodCount = food.ActiveFoodCount;
				}
				else if (food.ActiveFoodCount > _lastFoodCount)
				{
					_suppressUntil = realtimeSinceStartup + PostEatGraceSeconds.Value;
					Debug($"Eat detected (foods {_lastFoodCount} -> {food.ActiveFoodCount}); " + $"suppressing for {PostEatGraceSeconds.Value}s.");
				}
				_lastFoodCount = food.ActiveFoodCount;
				SpawnTracker.EnsureSpawnTime();
				bool isWithinLoginGrace = SpawnTracker.IsWithinLoginGrace;
				bool flag3 = realtimeSinceStartup < _suppressUntil || flag2 || isWithinLoginGrace;
				if (RemindIfUnmarked.Value && !flag3 && IsMarkedModeWithoutMark())
				{
					ReminderEngine.TryUnmarkedReminder(realtimeSinceStartup);
				}
				ReminderEngine.Tick(food, flag3);
			}
		}

		private static bool IsMarkedModeWithoutMark()
		{
			if ((BaseZoneMode.Value ?? "").Trim() != "Marked")
			{
				return false;
			}
			return string.IsNullOrWhiteSpace(MarkedBaseCenter.Value);
		}

		private static float? GetTimeSinceDeath(Player local)
		{
			if (_timeSinceDeathField == null)
			{
				_timeSinceDeathField = AccessTools.Field(typeof(Player), "m_timeSinceDeath");
			}
			if (_timeSinceDeathField == null)
			{
				return null;
			}
			object value = _timeSinceDeathField.GetValue(local);
			if (value is float)
			{
				return (float)value;
			}
			return null;
		}

		private static bool IsPlayerTransitioning(Player local)
		{
			try
			{
				if (((Character)local).IsTeleporting())
				{
					return true;
				}
			}
			catch
			{
			}
			if (_isLoadingField == null)
			{
				_isLoadingField = AccessTools.Field(typeof(Player), "m_isLoading");
			}
			if (_isLoadingField != null)
			{
				object value = _isLoadingField.GetValue(local);
				bool flag = default(bool);
				int num;
				if (value is bool)
				{
					flag = (bool)value;
					num = 1;
				}
				else
				{
					num = 0;
				}
				if (((uint)num & (flag ? 1u : 0u)) != 0)
				{
					return true;
				}
			}
			return false;
		}

		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 _lastCombat = -999f;

		private static float _lastNoFoodOut = -999f;

		private static float _lastCombatNoRest = -999f;

		private static float _lastCombatSfx = -999f;

		private static float _lastUnmarkedReminder = -999f;

		private static float _lastAnyPopup = -999f;

		private const int NeedsFoodConfirmPolls = 2;

		private static int _needsFoodStreak;

		private static int _noFoodStreak;

		private static int _notRestedStreak;

		private static int _combatEmptySlotsStreak;

		private static GameObject _alertSfxPrefab;

		private static bool _alertSfxResolved;

		internal static void Tick(FoodMonitor.FoodState food, bool suppressed)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			BaseZoneChecker.BaseState baseState = BaseZoneChecker.Evaluate(localPlayer);
			bool flag = LocalCombatScanner.IsLocalPlayerInCombat(localPlayer);
			bool isInBase = baseState.IsInBase;
			_needsFoodStreak = (food.NeedsFood ? (_needsFoodStreak + 1) : 0);
			_noFoodStreak = (food.HasNoFood ? (_noFoodStreak + 1) : 0);
			bool flag2 = _needsFoodStreak >= 2;
			bool flag3 = _noFoodStreak >= 2;
			bool flag4 = RestStatusChecker.IsRested(localPlayer);
			_notRestedStreak = ((!flag4) ? (_notRestedStreak + 1) : 0);
			bool flag5 = _notRestedStreak >= 2;
			bool flag6 = food.EmptyFoodSlots >= Plugin.CombatMinEmptySlots.Value;
			_combatEmptySlotsStreak = (flag6 ? (_combatEmptySlotsStreak + 1) : 0);
			bool flag7 = _combatEmptySlotsStreak >= 2;
			Plugin.Debug($"tick: needsFood={food.NeedsFood}(streak={_needsFoodStreak}) " + $"noFood={food.HasNoFood}(streak={_noFoodStreak}) " + $"active={food.ActiveFoodCount} empty={food.EmptyFoodSlots} lowest={food.LowestRemainingPct}% " + $"combatEmpty={flag6}(streak={_combatEmptySlotsStreak}) " + $"rested={flag4}(notRestedStreak={_notRestedStreak}) " + $"inBase={isInBase} leftBase={baseState.LeftBase} combat={flag} " + $"suppressed={suppressed}");
			if (suppressed)
			{
				Plugin.Debug("suppressed (post-eat grace); skipping trigger evaluation.");
			}
			else if (Plugin.CombatEnabled.Value && flag && flag7 && CooldownElapsed(realtimeSinceStartup, _lastCombat, Plugin.CombatCooldown.Value) && TryFire(realtimeSinceStartup, Plugin.CombatMessage.Value, "_lastCombat"))
			{
				_lastCombat = realtimeSinceStartup;
				if (Plugin.CombatSoundEnabled.Value && CooldownElapsed(realtimeSinceStartup, _lastCombatSfx, Plugin.CombatSoundCooldown.Value))
				{
					PlayAlertSfx(localPlayer);
					_lastCombatSfx = realtimeSinceStartup;
				}
				Plugin.Debug("#3 combat trigger fired.");
			}
			else if (Plugin.CombatNoRestEnabled.Value && flag && flag5 && CooldownElapsed(realtimeSinceStartup, _lastCombatNoRest, Plugin.CombatNoRestCooldown.Value) && TryFire(realtimeSinceStartup, Plugin.CombatNoRestMessage.Value, "_lastCombatNoRest"))
			{
				_lastCombatNoRest = realtimeSinceStartup;
				if (Plugin.CombatSoundEnabled.Value && CooldownElapsed(realtimeSinceStartup, _lastCombatSfx, Plugin.CombatSoundCooldown.Value))
				{
					PlayAlertSfx(localPlayer);
					_lastCombatSfx = realtimeSinceStartup;
				}
				Plugin.Debug("#5 combat-no-rest trigger fired.");
			}
			else if (Plugin.LeaveBaseEnabled.Value && baseState.LeftBase && flag2 && CooldownElapsed(realtimeSinceStartup, _lastLeaveBase, Plugin.LeaveBaseCooldown.Value) && TryFire(realtimeSinceStartup, Plugin.LeaveBaseMessage.Value, "_lastLeaveBase"))
			{
				_lastLeaveBase = realtimeSinceStartup;
				Plugin.Debug("#1 leave-base trigger fired.");
			}
			else if (Plugin.NoFoodOutEnabled.Value && flag3 && !isInBase && CooldownElapsed(realtimeSinceStartup, _lastNoFoodOut, Plugin.NoFoodOutCooldown.Value) && TryFire(realtimeSinceStartup, Plugin.NoFoodOutMessage.Value, "_lastNoFoodOut"))
			{
				_lastNoFoodOut = realtimeSinceStartup;
				Plugin.Debug("#4 no-food-out trigger fired.");
			}
			else if (Plugin.LowFoodEnabled.Value && flag2 && (!isInBase || !Plugin.SuppressLowFoodInBase.Value) && CooldownElapsed(realtimeSinceStartup, _lastLowFood, Plugin.LowFoodCooldown.Value))
			{
				string text = Plugin.LowFoodMessage.Value.Replace("{pct}", food.LowestRemainingPct.ToString());
				if (TryFire(realtimeSinceStartup, text, "_lastLowFood"))
				{
					_lastLowFood = realtimeSinceStartup;
					Plugin.Debug("#2 low-food trigger fired.");
				}
			}
		}

		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:F1}s until next allowed).");
				return false;
			}
			ShowPopup(text);
			_lastAnyPopup = now;
			return true;
		}

		public static void TryUnmarkedReminder(float now)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (CooldownElapsed(now, _lastUnmarkedReminder, Plugin.UnmarkedReminderCooldown.Value))
			{
				string text = ((object)Plugin.MarkBaseHotkey.Value/*cast due to .constrained prefix*/).ToString();
				string text2 = "FoodGuard: no base marked -- press " + text + " at your base so 'in base' works.";
				if (TryFire(now, text2, "_lastUnmarkedReminder"))
				{
					_lastUnmarkedReminder = now;
					Plugin.Debug("unmarked-base reminder fired.");
				}
			}
		}

		public static void Reset()
		{
			_lastLeaveBase = -999f;
			_lastLowFood = -999f;
			_lastCombat = -999f;
			_lastNoFoodOut = -999f;
			_lastCombatNoRest = -999f;
			_lastCombatSfx = -999f;
			_lastUnmarkedReminder = -999f;
			_lastAnyPopup = -999f;
			_needsFoodStreak = 0;
			_noFoodStreak = 0;
			_notRestedStreak = 0;
			_combatEmptySlotsStreak = 0;
			_alertSfxPrefab = null;
			_alertSfxResolved = false;
			LocalCombatScanner.Reset();
			BaseZoneChecker.Reset();
		}

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

		private static void ShowPopup(string text)
		{
			MessageHud instance = MessageHud.instance;
			if ((Object)(object)instance == (Object)null)
			{
				Plugin.Debug("MessageHud not ready; popup skipped.");
				return;
			}
			try
			{
				instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false);
			}
			catch (Exception ex)
			{
				Plugin.Debug("ShowMessage threw (non-fatal): " + ex.Message);
			}
		}

		private static void PlayAlertSfx(Player local)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = ResolveAlertSfxPrefab();
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Debug("Alert SFX prefab '" + Plugin.AlertSfxName.Value + "' not found in ZNetScene; sound skipped.");
				return;
			}
			try
			{
				GameObject val2 = Object.Instantiate<GameObject>(val, ((Component)local).transform.position, Quaternion.identity);
				AudioSource componentInChildren = val2.GetComponentInChildren<AudioSource>();
				if ((Object)(object)componentInChildren == (Object)null)
				{
					Plugin.Debug("SFX prefab '" + ((Object)val).name + "' has no AudioSource; sound skipped.");
					Object.Destroy((Object)(object)val2);
				}
				else
				{
					float num = (((Object)(object)componentInChildren.clip != (Object)null) ? (componentInChildren.clip.length + 0.5f) : 5f);
					Object.Destroy((Object)(object)val2, num);
				}
			}
			catch (Exception ex)
			{
				Plugin.Debug("PlayAlertSfx threw (non-fatal): " + ex.Message);
			}
		}

		private static GameObject ResolveAlertSfxPrefab()
		{
			if (_alertSfxResolved)
			{
				return _alertSfxPrefab;
			}
			string value = Plugin.AlertSfxName.Value;
			_alertSfxResolved = true;
			if (string.IsNullOrEmpty(value))
			{
				return null;
			}
			ZNetScene instance = ZNetScene.instance;
			if ((Object)(object)instance == (Object)null)
			{
				_alertSfxResolved = false;
				return null;
			}
			_alertSfxPrefab = instance.GetPrefab(value);
			return _alertSfxPrefab;
		}
	}
	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).");
			}
		}
	}
}