Decompiled source of ValheimServerGuide v0.16.1

ValheimServerGuide.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Managers;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;
using ValheimServerGuide.Commands;
using ValheimServerGuide.Config;
using ValheimServerGuide.Discord;
using ValheimServerGuide.Display;
using ValheimServerGuide.Net;
using ValheimServerGuide.Rewards;
using ValheimServerGuide.State;
using ValheimServerGuide.Triggers;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("ValheimServerGuide")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.16.1.0")]
[assembly: AssemblyInformationalVersion("0.16.1")]
[assembly: AssemblyProduct("ValheimServerGuide")]
[assembly: AssemblyTitle("ValheimServerGuide")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.16.1.0")]
[module: UnverifiableCode]
namespace ValheimServerGuide
{
	[BepInPlugin("com.valheimserverguide", "ValheimServerGuide", "0.16.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "com.valheimserverguide";

		public const string PluginName = "ValheimServerGuide";

		public const string PluginVersion = "0.16.1";

		private Harmony _harmony;

		private static GuidanceConfigLoader _loader;

		private static readonly object _loaderLock = new object();

		private static string _configDir;

		public static Plugin Instance { get; private set; }

		public static ManualLogSource Log { get; private set; }

		public static GuidanceConfig CurrentConfig { get; internal set; } = GuidanceConfig.Empty;

		public static ConfigEntry<bool> RavenEnabled { get; private set; }

		public static ConfigEntry<string> IntroMusicName { get; private set; }

		public static ConfigEntry<float> IntroMusicDuration { get; private set; }

		public static ConfigEntry<float> IntroFadeInDuration { get; private set; }

		public static ConfigEntry<float> IntroPreDelay { get; private set; }

		public static ConfigEntry<float> IntroDisplaySeconds { get; private set; }

		public static ConfigEntry<float> IntroFadeOutDuration { get; private set; }

		public static ConfigEntry<string> ChatColor { get; private set; }

		public static ConfigEntry<bool> CodexEnabled { get; private set; }

		public static ConfigEntry<string> CodexKey { get; private set; }

		public static ConfigEntry<bool> TrackerEnabled { get; private set; }

		public static ConfigEntry<string> TrackerPosition { get; private set; }

		public static ConfigEntry<int> TrackerMaxVisible { get; private set; }

		public static ConfigEntry<string> TrackerHotkey { get; private set; }

		public static ConfigEntry<bool> TrackerBadgeEnabled { get; private set; }

		public static ConfigEntry<string> ProgressPath { get; private set; }

		public static ConfigEntry<string> DiscordWebhookUrl { get; private set; }

		public static ConfigEntry<string> DiscordDefaultTemplate { get; private set; }

		public static ConfigEntry<string> DiscordBotUsername { get; private set; }

		public static ConfigEntry<bool> DiscordGuideEnabled { get; private set; }

		public static ConfigEntry<string> DiscordGuideFormat { get; private set; }

		public static ConfigEntry<bool> DiscordQuestStartEnabled { get; private set; }

		public static ConfigEntry<string> DiscordQuestStartWebhookUrl { get; private set; }

		private void Awake()
		{
			//IL_0337: Unknown result type (might be due to invalid IL or missing references)
			//IL_0341: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			RavenEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "RavenEnabled", true, "Enable raven (Hugin) popup mode. Independent of Valheim's 'Tutorials' setting — this mod's raven popups will fire even when vanilla raven hints are turned off in game options.");
			IntroMusicName = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "IntroMusicName", "intro", "Music track to play while a guidance is shown in 'intro' display mode. 'intro' is the vanilla Valkyrie-intro track.");
			IntroMusicDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroMusicDuration", 60f, "Seconds the intro music stays pinned once it starts. The music plays for at least this long even if the player dismisses the on-screen text early. After the duration elapses, vanilla MusicMan resumes normal environment-based selection.");
			IntroFadeInDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroFadeInDuration", 3f, "Seconds to fade the screen to black before the intro text + music start. Uses vanilla Hud.m_loadingScreen so no custom assets are needed. Set 0 to disable.");
			IntroPreDelay = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroPreDelay", 1f, "Seconds to hold on a black screen after the fade-in, before the intro text appears. Adds dramatic weight to the transition.");
			IntroDisplaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroDisplaySeconds", 15f, "How long the intro text stays on screen (seconds) before it auto-fades out. The player stays frozen and invulnerable for this whole time, or until they press Use/Escape to skip early. Range 1-300.");
			IntroFadeOutDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroFadeOutDuration", 1f, "Seconds to fade the intro text out when it ends (on skip via Use/Escape or when IntroDisplaySeconds elapses). Set 0 for an instant cut.");
			ChatColor = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "ChatColor", "#E0C078", "Hex color (with or without leading '#') applied to chat-mode guidance messages, so they read distinct from regular say (white) and shout (yellow). Set to empty string to disable coloring.");
			TrackerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("HudTracker", "TrackerEnabled", true, "Show the objective tracker widget on the HUD. Set false to hide it entirely (the widget GameObject remains in the scene — just inactive).");
			TrackerPosition = ((BaseUnityPlugin)this).Config.Bind<string>("HudTracker", "TrackerPosition", "TopRight", "Corner the tracker widget anchors to: TopRight | TopLeft | BottomRight | BottomLeft. Takes effect on next session start (Hud.Awake).");
			TrackerMaxVisible = ((BaseUnityPlugin)this).Config.Bind<int>("HudTracker", "TrackerMaxVisible", 3, "Maximum number of active guide chains shown simultaneously. Chains beyond this limit are collapsed into a '+N more' label.");
			TrackerHotkey = ((BaseUnityPlugin)this).Config.Bind<string>("HudTracker", "TrackerHotkey", "F10", "KeyCode name for the tracker toggle hotkey (e.g. F9, F10, H). See UnityEngine.KeyCode enum for valid values. YAML tracker.hotkey wins when set.");
			TrackerBadgeEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("HudTracker", "TrackerBadgeEnabled", true, "Show the persistent corner hint badge (e.g. '[F9] Quests (2)') even when the main tracker panel is hidden. YAML tracker.badge_enabled wins when set.");
			CodexEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Codex", "CodexEnabled", true, "Enable the in-game Guide Codex panel. Set false to disable the keybind and skip instantiating the panel entirely.");
			CodexKey = ((BaseUnityPlugin)this).Config.Bind<string>("Codex", "CodexKey", "F3", "KeyCode name for the Codex toggle hotkey (e.g. F2, F3). See UnityEngine.KeyCode enum for valid values.");
			ProgressPath = ((BaseUnityPlugin)this).Config.Bind<string>("PlayerProgress", "ProgressPath", "", "Folder holding one quest-progress file per character (server-side save data). Leave empty for the default: <BepInEx>/config/ValheimServerGuide/PlayerProgress. Set an absolute path outside the config tree if your mod manager rewrites config/ on update — these files ARE your players' quest progress, so back them up. Only the authoritative process uses this (dedicated server, host, or single-player).");
			DiscordWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL. Set on the server only — never share this with clients. Leave empty to disable all discord announcements.");
			DiscordDefaultTemplate = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "DefaultTemplate", "**{playerName}** triggered **{topic}**", "Default message template when a guidance entry has `announce: { discord: \"\" }` (empty string = use default). Tokens: {playerName}, {id}, {topic}, {text}.");
			DiscordBotUsername = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "BotUsername", "ValheimServerGuide", "Username shown for webhook messages in Discord.");
			DiscordGuideEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "DiscordGuideEnabled", true, "Enable guide-completion webhook POSTs (discord_on_complete). Set false to suppress these without affecting kill/event POSTs.");
			DiscordGuideFormat = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "DiscordGuideFormat", "plain", "Format for guide-completion messages: 'plain' (content string) or 'embed' (rich embed).");
			DiscordQuestStartEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "QuestStartLogEnabled", true, "Enable the quest-start debug log: a separate webhook POST every time a player starts a new quest (a chain's first step, or the first fire of any single entry). Intended for verifying quests trigger correctly. Requires QuestStartWebhookUrl to be set.");
			DiscordQuestStartWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "QuestStartWebhookUrl", "", "Discord webhook URL for the quest-start debug log (separate channel from WebhookUrl). Server-side only — never share with clients. Leave empty to disable the quest-start log regardless of QuestStartLogEnabled.");
			_harmony = new Harmony("com.valheimserverguide");
			_harmony.PatchAll(Assembly.GetExecutingAssembly());
			foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods())
			{
				Log.LogInfo((object)("Harmony patched: " + patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name));
			}
			_configDir = Path.Combine(Paths.ConfigPath, "ValheimServerGuide");
			GuidanceSync.Register();
			GuidanceDisplay.Initialize();
			AdminCommands.Register();
			TimeTrigger.Start();
			if (Application.isBatchMode)
			{
				Log.LogInfo((object)"Running in batch mode (dedicated server). Loading guidance YAML now.");
				EnsureLoaderStarted();
			}
			else
			{
				Log.LogInfo((object)"Client process. Guidance YAML will be loaded only if this session hosts a world.");
			}
			Log.LogInfo((object)"ValheimServerGuide v0.16.1 loaded.");
		}

		public static void EnsureLoaderStarted()
		{
			lock (_loaderLock)
			{
				if (_loader == null)
				{
					Directory.CreateDirectory(_configDir);
					_loader = new GuidanceConfigLoader(_configDir);
					_loader.ConfigChanged += Instance.OnConfigChanged;
					_loader.Start();
					Log.LogInfo((object)("Guidance YAML loader started (" + _configDir + ")."));
				}
			}
		}

		public static void ShutdownLoader()
		{
			lock (_loaderLock)
			{
				if (_loader != null)
				{
					_loader.Dispose();
					_loader = null;
					Log.LogInfo((object)"Guidance YAML loader stopped.");
				}
			}
		}

		private void OnConfigChanged(GuidanceConfig newConfig)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				Log.LogInfo((object)"Local YAML edit ignored — remote server's config takes priority.");
				return;
			}
			CurrentConfig = newConfig;
			GuidanceDisplay.RegisterTutorials(newConfig);
			TimedTrigger.OnConfigChanged(newConfig);
			if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
			{
				GuidanceSync.BroadcastToClients(newConfig);
			}
			GuidanceHudTracker.Instance?.ApplyLayout();
			GuidanceHudTracker.Instance?.Refresh();
			ItemAcquiredTrigger.CheckAllCountGoals();
			if ((Object)(object)Player.m_localPlayer != (Object)null && (Object)(object)MessageHud.instance != (Object)null)
			{
				SynchronizationManager instance = SynchronizationManager.Instance;
				if (instance != null && instance.PlayerIsAdmin)
				{
					MessageHud.instance.ShowMessage((MessageType)1, $"[VSG] Guide config reloaded — {newConfig.Guidances.Count} entries loaded.", 0, (Sprite)null, false, true);
				}
			}
		}

		private void Update()
		{
			GuidanceConfigLoader loader;
			lock (_loaderLock)
			{
				loader = _loader;
			}
			loader?.Tick();
			if ((Object)(object)Player.m_localPlayer != (Object)null && PlayerProgress.CharacterId == 0L && PlayerProgress.Mode == PlayerProgress.StoreMode.Unbound)
			{
				GuidanceSync.EnsureProgressSession(Player.m_localPlayer);
			}
			PlayerProgress.Tick();
			PlayerProgressStore.Tick();
			GuidanceDisplay.Tick();
			CenterToast.Flush();
		}

		private void OnDestroy()
		{
			try
			{
				PlayerProgress.FlushNow();
			}
			catch (Exception ex)
			{
				Log.LogError((object)("[progress] shutdown flush failed: " + ex.Message));
			}
			PlayerProgressStore.SaveAll();
			ShutdownLoader();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
}
namespace ValheimServerGuide.Triggers
{
	[HarmonyPatch(typeof(Player), "Update")]
	internal static class BiomeTrigger
	{
		private const float CheckInterval = 2f;

		private static float _nextCheck;

		private static Biome _lastBiome;

		internal static void Reset()
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			_lastBiome = (Biome)0;
		}

		[HarmonyPostfix]
		private unsafe static void Postfix(Player __instance)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: 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)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: 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_005b: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer || Time.time < _nextCheck)
			{
				return;
			}
			_nextCheck = Time.time + 2f;
			Biome currentBiome = __instance.GetCurrentBiome();
			if (currentBiome != _lastBiome)
			{
				Biome lastBiome = _lastBiome;
				_lastBiome = currentBiome;
				if ((int)currentBiome != 0)
				{
					Plugin.Log.LogInfo((object)$"[biome] entered '{currentBiome}' (was '{lastBiome}').");
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "biome",
						Subject = ((object)(*(Biome*)(&currentBiome))/*cast due to .constrained prefix*/).ToString()
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnSpawned")]
	internal static class BiomeTriggerSpawnReset
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				BiomeTrigger.Reset();
			}
		}
	}
	[HarmonyPatch(typeof(Character), "OnDeath")]
	internal static class BossDefeatedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Character __instance)
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null || !__instance.IsBoss())
			{
				return;
			}
			object obj;
			if (__instance == null)
			{
				obj = null;
			}
			else
			{
				HitData lastHit = __instance.m_lastHit;
				obj = ((lastHit != null) ? lastHit.GetAttacker() : null);
			}
			if (!((Object)obj != (Object)(object)Player.m_localPlayer))
			{
				GameObject gameObject = ((Component)__instance).gameObject;
				string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (!string.IsNullOrEmpty(text))
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "boss_defeated",
						Subject = text,
						DisplayName = __instance.m_name
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "TryPlacePiece")]
	internal static class BuildTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance, Piece piece, bool __result)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && __result && !((Object)(object)piece == (Object)null))
			{
				GameObject gameObject = ((Component)piece).gameObject;
				string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (!string.IsNullOrEmpty(text))
				{
					Plugin.Log.LogInfo((object)("[build] subject='" + text + "' (display='" + piece.m_name + "')."));
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "build",
						Subject = text,
						DisplayName = piece.m_name
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(Container), "Interact")]
	internal static class ChestOpenedTrigger
	{
		private const string GuardKey = "chest_opened_fired";

		[HarmonyPostfix]
		private static void Postfix(Humanoid character, bool hold, bool __result)
		{
			if (!hold && __result && !((Object)(object)character != (Object)(object)Player.m_localPlayer))
			{
				Player localPlayer = Player.m_localPlayer;
				if (PlayerProgress.IsReady && !SeenTracker.HasFired(localPlayer, "chest_opened_fired"))
				{
					SeenTracker.MarkFired(localPlayer, "chest_opened_fired");
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "chest_opened",
						Subject = ""
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(CookingStation), "Interact")]
	internal static class CookingStationUsedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(CookingStation __instance, Humanoid user, bool hold)
		{
			if (!hold && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer))
			{
				TriggerEvent obj = new TriggerEvent
				{
					Type = "cooking_used"
				};
				GameObject gameObject = ((Component)__instance).gameObject;
				obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				obj.DisplayName = __instance.m_name;
				GuidanceDispatcher.Raise(obj);
			}
		}
	}
	[HarmonyPatch(typeof(Fireplace), "Interact")]
	internal static class FireplaceUsedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Fireplace __instance, Humanoid user, bool hold)
		{
			if (!hold && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer))
			{
				TriggerEvent obj = new TriggerEvent
				{
					Type = "cooking_used"
				};
				GameObject gameObject = ((Component)__instance).gameObject;
				obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				obj.DisplayName = __instance.m_name;
				GuidanceDispatcher.Raise(obj);
			}
		}
	}
	[HarmonyPatch(typeof(CraftingStation), "Interact")]
	internal static class CraftingTableUsedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(CraftingStation __instance, Humanoid user, bool repeat)
		{
			if (!repeat && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer))
			{
				TriggerEvent obj = new TriggerEvent
				{
					Type = "crafting_table_used"
				};
				GameObject gameObject = ((Component)__instance).gameObject;
				obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				obj.DisplayName = __instance.m_name;
				GuidanceDispatcher.Raise(obj);
			}
		}
	}
	[HarmonyPatch(typeof(InventoryGui), "DoCrafting")]
	internal static class CraftTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(InventoryGui __instance, Player player)
		{
			if ((Object)(object)player != (Object)(object)Player.m_localPlayer)
			{
				Plugin.Log.LogDebug((object)"[craft] postfix fired for non-local player; ignoring.");
				return;
			}
			Recipe craftRecipe = __instance.m_craftRecipe;
			object obj;
			if (craftRecipe == null)
			{
				obj = null;
			}
			else
			{
				ItemDrop item = craftRecipe.m_item;
				obj = ((item != null) ? ((Component)item).gameObject : null);
			}
			GameObject val = (GameObject)obj;
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Log.LogWarning((object)"[craft] DoCrafting completed but m_craftRecipe/m_item was null.");
				return;
			}
			string name = ((Object)val).name;
			Plugin.Log.LogInfo((object)("[craft] subject='" + name + "' (display='" + craftRecipe.m_item.m_itemData?.m_shared?.m_name + "')"));
			GuidanceDispatcher.Raise(new TriggerEvent
			{
				Type = "craft",
				Subject = name,
				DisplayName = craftRecipe.m_item.m_itemData?.m_shared?.m_name
			});
		}
	}
	[HarmonyPatch(typeof(Player), "Update")]
	internal static class DistanceTrigger
	{
		private const float CheckInterval = 5f;

		private const float DefaultRadius = 50f;

		private const string KeyPrefix = "dist_";

		private static float _nextCheck;

		private static readonly List<KeyValuePair<string, Vector3>> Candidates = new List<KeyValuePair<string, Vector3>>();

		private static readonly HashSet<string> SeenThisTick = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer || !PlayerProgress.IsReady || Time.time < _nextCheck)
			{
				return;
			}
			_nextCheck = Time.time + 5f;
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			float num = MaxConfiguredRadius(currentConfig);
			if (num <= 0f)
			{
				return;
			}
			Vector3 position = ((Component)__instance).transform.position;
			CollectCandidates(position, num);
			foreach (KeyValuePair<string, Vector3> candidate in Candidates)
			{
				string key = candidate.Key;
				string id = "dist_" + key;
				if (!SeenTracker.HasFired(__instance, id) && AnyEntryInRange(currentConfig, key, position, candidate.Value))
				{
					SeenTracker.MarkFired(__instance, id);
					Plugin.Log.LogInfo((object)("[distance] entered range of '" + key + "'."));
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "distance",
						Subject = key
					});
				}
			}
		}

		private static void CollectCandidates(Vector3 playerPos, float maxRadius)
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: 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_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_010c: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Unknown result type (might be due to invalid IL or missing references)
			//IL_0125: Unknown result type (might be due to invalid IL or missing references)
			//IL_01df: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_0191: Unknown result type (might be due to invalid IL or missing references)
			//IL_0193: Unknown result type (might be due to invalid IL or missing references)
			//IL_0245: Unknown result type (might be due to invalid IL or missing references)
			Candidates.Clear();
			SeenThisTick.Clear();
			float num = maxRadius * maxRadius;
			Vector3 val;
			foreach (Location s_allLocation in Location.s_allLocations)
			{
				if ((Object)(object)s_allLocation == (Object)null)
				{
					continue;
				}
				Vector3 position = ((Component)s_allLocation).transform.position;
				val = position - playerPos;
				if (!(((Vector3)(ref val)).sqrMagnitude > num))
				{
					string text = TriggerUtils.NormalizePrefabName(((Object)((Component)s_allLocation).gameObject).name);
					if (!string.IsNullOrEmpty(text) && SeenThisTick.Add(text))
					{
						Plugin.Log.LogDebug((object)("[distance] scene scan in range: '" + text + "'"));
						Candidates.Add(new KeyValuePair<string, Vector3>(text, position));
					}
				}
			}
			if ((Object)(object)ZoneSystem.instance == (Object)null)
			{
				return;
			}
			foreach (KeyValuePair<Vector2s, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances)
			{
				LocationInstance value = locationInstance.Value;
				if (!value.m_placed)
				{
					continue;
				}
				val = value.m_position - playerPos;
				if (!(((Vector3)(ref val)).sqrMagnitude > num))
				{
					string text2 = value.m_location?.m_prefabName;
					if (string.IsNullOrEmpty(text2))
					{
						text2 = value.m_location?.m_name;
					}
					if (!string.IsNullOrEmpty(text2) && SeenThisTick.Add(text2))
					{
						Plugin.Log.LogDebug((object)("[distance] ZoneSystem instance in range: '" + text2 + "'"));
						Candidates.Add(new KeyValuePair<string, Vector3>(text2, value.m_position));
					}
				}
			}
			foreach (KeyValuePair<Vector3, string> locationIcon in ZoneSystem.instance.m_locationIcons)
			{
				val = locationIcon.Key - playerPos;
				if (!(((Vector3)(ref val)).sqrMagnitude > num) && !string.IsNullOrEmpty(locationIcon.Value) && SeenThisTick.Add(locationIcon.Value))
				{
					Plugin.Log.LogDebug((object)("[distance] location icon in range: '" + locationIcon.Value + "'"));
					Candidates.Add(new KeyValuePair<string, Vector3>(locationIcon.Value, locationIcon.Key));
				}
			}
		}

		private static float MaxConfiguredRadius(GuidanceConfig config)
		{
			float max = 0f;
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				Consider(guidance.Trigger, ref max);
				if (guidance.Steps == null)
				{
					continue;
				}
				foreach (GuidanceStep step in guidance.Steps)
				{
					Consider(step?.Trigger, ref max);
				}
			}
			return max;
		}

		private static void Consider(TriggerSpec t, ref float max)
		{
			if (IsDistance(t))
			{
				float num = ((t.Radius > 0f) ? t.Radius : 50f);
				if (num > max)
				{
					max = num;
				}
			}
		}

		private static bool AnyEntryInRange(GuidanceConfig config, string prefabName, Vector3 playerPos, Vector3 locPos)
		{
			//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)
			//IL_0054: 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)
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				if (CheckTrigger(guidance.Trigger, prefabName, playerPos, locPos))
				{
					return true;
				}
				if (guidance.Steps == null)
				{
					continue;
				}
				foreach (GuidanceStep step in guidance.Steps)
				{
					if (CheckTrigger(step?.Trigger, prefabName, playerPos, locPos))
					{
						return true;
					}
				}
			}
			return false;
		}

		private static bool CheckTrigger(TriggerSpec t, string prefabName, Vector3 playerPos, Vector3 locPos)
		{
			//IL_0035: 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)
			if (!IsDistance(t))
			{
				return false;
			}
			if (!LocationMatches(t.Location, prefabName))
			{
				return false;
			}
			float num = ((t.Radius > 0f) ? t.Radius : 50f);
			return Vector3.Distance(playerPos, locPos) <= num;
		}

		private static bool IsDistance(TriggerSpec t)
		{
			if (t != null)
			{
				return string.Equals(t.Type, "distance", StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		private static bool LocationMatches(string pattern, string value)
		{
			if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value))
			{
				return false;
			}
			if (pattern.EndsWith("*"))
			{
				return value.StartsWith(pattern.Substring(0, pattern.Length - 1), StringComparison.OrdinalIgnoreCase);
			}
			return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase);
		}
	}
	[HarmonyPatch(typeof(Humanoid), "EquipItem")]
	internal static class EquipTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Humanoid __instance, ItemData item, bool __result)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && __result && item != null)
			{
				string text = ResolveItemName(item);
				if (!string.IsNullOrEmpty(text))
				{
					Plugin.Log.LogInfo((object)("[equip] subject='" + text + "' (token='" + item.m_shared?.m_name + "')."));
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "equip",
						Subject = text,
						DisplayName = item.m_shared?.m_name
					});
				}
			}
		}

		private static string ResolveItemName(ItemData item)
		{
			GameObject dropPrefab = item.m_dropPrefab;
			string text = ((dropPrefab != null) ? ((Object)dropPrefab).name : null);
			if (!string.IsNullOrEmpty(text))
			{
				return TriggerUtils.NormalizePrefabName(text);
			}
			return TriggerUtils.NormalizePrefabName(item.m_shared?.m_name ?? "");
		}
	}
	[HarmonyPatch(typeof(Player), "OnSpawned")]
	internal static class FirstLoginTrigger
	{
		private const string GuardKey = "first_login_fired";

		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				RunIfNeeded(__instance);
			}
		}

		internal static void RunIfNeeded(Player player)
		{
			if (!((Object)(object)player == (Object)null) && PlayerProgress.IsReady && !SeenTracker.HasFired(player, "first_login_fired"))
			{
				SeenTracker.MarkFired(player, "first_login_fired");
				GuidanceDispatcher.Raise(new TriggerEvent
				{
					Type = "first_login",
					Subject = ""
				});
			}
		}
	}
	public static class GuidanceDispatcher
	{
		private static GuidanceConfig _indexedConfig;

		private static readonly Dictionary<string, List<GuidanceEntry>> _candidates = new Dictionary<string, List<GuidanceEntry>>(StringComparer.OrdinalIgnoreCase);

		private static List<GuidanceEntry> CandidatesFor(GuidanceConfig config, string eventType)
		{
			if (_indexedConfig != config)
			{
				_indexedConfig = config;
				_candidates.Clear();
			}
			string text = eventType ?? string.Empty;
			if (_candidates.TryGetValue(text, out var value))
			{
				return value;
			}
			List<GuidanceEntry> list = new List<GuidanceEntry>();
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				if (guidance == null)
				{
					continue;
				}
				if (guidance.Steps != null && guidance.Steps.Count > 0)
				{
					list.Add(guidance);
					continue;
				}
				string text2 = guidance.Trigger?.Type;
				if (!string.IsNullOrEmpty(text2) && string.Equals(text2, text, StringComparison.OrdinalIgnoreCase))
				{
					list.Add(guidance);
				}
			}
			_candidates[text] = list;
			return list;
		}

		public static void Raise(TriggerEvent evt)
		{
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null)
			{
				Plugin.Log.LogDebug((object)("[dispatch] " + evt.Type + "/" + evt.Subject + " ignored: no local player."));
				return;
			}
			if (!PlayerProgress.IsReady)
			{
				Plugin.Log.LogDebug((object)("[dispatch] " + evt.Type + "/" + evt.Subject + " deferred: progress not loaded yet."));
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null || currentConfig.Guidances.Count == 0)
			{
				Plugin.Log.LogDebug((object)("[dispatch] " + evt.Type + "/" + evt.Subject + " ignored: empty config."));
				return;
			}
			int num = 0;
			List<string> list = new List<string>();
			foreach (GuidanceEntry entry in CandidatesFor(currentConfig, evt.Type))
			{
				if (entry.Steps != null && entry.Steps.Count > 0)
				{
					if (HandleChain(entry, evt, player, list))
					{
						num++;
					}
				}
				else
				{
					if (!Matches(entry, evt))
					{
						continue;
					}
					if (string.Equals(evt.Type, "item_acquired", StringComparison.OrdinalIgnoreCase) && entry.Trigger != null && ItemAcquiredTrigger.GetEffectiveGoals(entry.Trigger) != null)
					{
						Plugin.Log.LogDebug((object)("[dispatch] '" + entry.Id + "' item_acquired count-goal — delegated to count path."));
						continue;
					}
					if (string.Equals(evt.Type, "kill", StringComparison.OrdinalIgnoreCase) && entry.Trigger != null && entry.Trigger.Count > 1)
					{
						Plugin.Log.LogDebug((object)("[dispatch] '" + entry.Id + "' kill count-goal — delegated to count path."));
						continue;
					}
					if (!RequirementsMet(entry, player))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + entry.Id + "' skipped: requires not met."));
						continue;
					}
					if (StopConditionMet(entry, player))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + entry.Id + "' skipped: stop_when met."));
						continue;
					}
					if (entry.Once && SeenTracker.HasFired(player, entry.Id, entry.Scope))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + entry.Id + "' skipped: already fired (once)."));
						continue;
					}
					if (!SeenTracker.CooldownReady(entry.Id, entry.Cooldown, Time.time))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + entry.Id + "' skipped: cooldown."));
						continue;
					}
					int num2 = entry.Trigger?.MaxFires ?? 0;
					if (num2 > 0 && SeenTracker.GetFireCount(player, entry.Id) >= num2)
					{
						Plugin.Log.LogInfo((object)$"[dispatch] '{entry.Id}' skipped: max_fires ({num2}) reached.");
						continue;
					}
					if (SeenTracker.IsGlobalScope(entry.Scope))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + entry.Id + "' (global) -> server."));
						GuidanceSync.SendTriggerGlobal(entry.Id, player.GetPlayerName());
						MaybeLogQuestStart(player, entry);
						SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time);
						num++;
						continue;
					}
					Plugin.Log.LogInfo((object)("[dispatch] firing '" + entry.Id + "' via mode '" + entry.Display?.Mode + "'."));
					string template = ((!string.IsNullOrEmpty(entry.Message)) ? entry.Message : entry.Display?.Text);
					string renderedText = RenderDisplay(entry, null, template, evt, player.GetPlayerName());
					GuidanceDisplay.Show(entry, renderedText);
					if (entry.Once)
					{
						SeenTracker.MarkFired(player, entry.Id, entry.Scope);
					}
					if (num2 > 0)
					{
						SeenTracker.IncrementFireCount(player, entry.Id);
					}
					SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time);
					DebugFireLog.Record(player.GetPlayerName(), entry.Id);
					MaybeLogQuestStart(player, entry);
					if (entry.Announce?.Discord != null)
					{
						GuidanceSync.SendAnnounceRequest(entry.Id, player.GetPlayerName());
					}
					if (entry.DiscordOnComplete)
					{
						GuidanceSync.SendCompleteAnnounce(entry.Id, player.GetPlayerName());
					}
					if (entry.Rewards != null && entry.Rewards.Count > 0)
					{
						RewardDispatcher.Grant(entry.Rewards, player, (string s) => TemplateText(s, evt, player.GetPlayerName()), (string s) => TextHighlighter.Apply(s, entry));
					}
					list.Add(entry.Id);
					num++;
				}
			}
			if (num == 0)
			{
				Plugin.Log.LogInfo((object)("[dispatch] " + evt.Type + "/" + evt.Subject + " matched no guidance entries."));
			}
			foreach (string item in list)
			{
				Raise(new TriggerEvent
				{
					Type = "entry_finished",
					Subject = item
				});
			}
		}

		private static bool HandleChain(GuidanceEntry entry, TriggerEvent evt, Player player, List<string> completedIds)
		{
			if (ChainState.IsComplete(player, entry.Id))
			{
				return false;
			}
			if (!PrerequisiteChecker.AllSatisfied(entry.Requires, player, Plugin.CurrentConfig))
			{
				Plugin.Log.LogDebug((object)("[chain] '" + entry.Id + "' prerequisites not met."));
				return false;
			}
			int step = ChainState.GetStep(player, entry.Id);
			if (step >= entry.Steps.Count)
			{
				ChainState.MarkComplete(player, entry.Id);
				return false;
			}
			GuidanceStep guidanceStep = entry.Steps[step];
			if (guidanceStep?.Trigger == null)
			{
				return false;
			}
			if (guidanceStep.ProgressGoal > 0)
			{
				return HandleCounterStep(entry, guidanceStep, step, evt, player, completedIds);
			}
			return HandleNormalStep(entry, guidanceStep, step, evt, player, completedIds);
		}

		private static bool HandleNormalStep(GuidanceEntry entry, GuidanceStep step, int stepIndex, TriggerEvent evt, Player player, List<string> completedIds)
		{
			if (!MatchesTrigger(step.Trigger, evt))
			{
				return false;
			}
			if (stepIndex == 0)
			{
				MaybeLogQuestStart(player, entry);
			}
			FireStepDisplay(entry, step, stepIndex, evt, player);
			AdvanceChain(entry, stepIndex, player, completedIds);
			return true;
		}

		private static bool HandleCounterStep(GuidanceEntry entry, GuidanceStep step, int stepIndex, TriggerEvent evt, Player player, List<string> completedIds)
		{
			if (step.ProgressTrigger == null)
			{
				Plugin.Log.LogWarning((object)($"[chain] '{entry.Id}' step {stepIndex} has progress_goal " + "but no progress_trigger — treating as normal step."));
				return HandleNormalStep(entry, step, stepIndex, evt, player, completedIds);
			}
			int counter = ChainState.GetCounter(player, entry.Id, stepIndex);
			if (counter < 0)
			{
				if (!MatchesTrigger(step.Trigger, evt))
				{
					return false;
				}
				if (stepIndex == 0)
				{
					MaybeLogQuestStart(player, entry);
				}
				int num = 0;
				if (step.ProgressTrigger != null && string.Equals(step.ProgressTrigger.Type, "item_acquired", StringComparison.OrdinalIgnoreCase))
				{
					num = Math.Min(ItemAcquiredTrigger.CountInInventory(player, step.ProgressTrigger.Item), step.ProgressGoal);
				}
				ChainState.SetCounter(player, entry.Id, stepIndex, num);
				Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' step {stepIndex} counter activated (seed: {num}/{step.ProgressGoal}).");
				GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
				if (num >= step.ProgressGoal)
				{
					FireStepDisplay(entry, step, stepIndex, evt, player);
					ChainState.ClearCounter(player, entry.Id, stepIndex);
					AdvanceChain(entry, stepIndex, player, completedIds);
				}
				return true;
			}
			if (!MatchesTrigger(step.ProgressTrigger, evt))
			{
				return false;
			}
			int num2 = Math.Min(counter + 1, step.ProgressGoal);
			ChainState.SetCounter(player, entry.Id, stepIndex, num2);
			Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' step {stepIndex} counter: {num2}/{step.ProgressGoal}.");
			GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
			if (num2 >= step.ProgressGoal)
			{
				FireStepDisplay(entry, step, stepIndex, evt, player);
				ChainState.ClearCounter(player, entry.Id, stepIndex);
				AdvanceChain(entry, stepIndex, player, completedIds);
			}
			return true;
		}

		private static void FireStepDisplay(GuidanceEntry entry, GuidanceStep step, int stepIndex, TriggerEvent evt, Player player)
		{
			DisplaySpec displaySpec = step.Display ?? entry.Display ?? new DisplaySpec();
			string template = ((!string.IsNullOrEmpty(step.Message)) ? step.Message : displaySpec.Text);
			string text = RenderDisplay(entry, step, template, evt, player.GetPlayerName(), stepIndex + 1, entry.Steps.Count);
			string id = entry.Id + "_s" + stepIndex;
			GuidanceEntry entry2 = new GuidanceEntry
			{
				Id = id,
				Display = new DisplaySpec
				{
					Mode = displaySpec.Mode,
					Topic = displaySpec.Topic,
					Text = text,
					Position = displaySpec.Position
				},
				Scope = entry.Scope,
				Once = false
			};
			Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' step {stepIndex} firing via '{displaySpec.Mode}'.");
			GuidanceDisplay.Show(entry2, text);
			if (entry.Announce?.Discord != null)
			{
				GuidanceSync.SendAnnounceRequest(entry.Id, player.GetPlayerName());
			}
		}

		private static void AdvanceChain(GuidanceEntry entry, int stepIndex, Player player, List<string> completedIds)
		{
			int num = stepIndex + 1;
			if (num >= entry.Steps.Count)
			{
				ChainState.MarkComplete(player, entry.Id);
				ChainState.SetCompletedVersion(player, entry.Id, entry.Version);
				Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' complete (all {entry.Steps.Count} steps done).");
				GuidanceHudTracker.Instance?.FlashCompletion(entry.Id);
				DebugFireLog.Record(player.GetPlayerName(), entry.Id);
				if (entry.DiscordOnComplete)
				{
					GuidanceSync.SendCompleteAnnounce(entry.Id, player.GetPlayerName());
				}
				if (entry.Rewards != null && entry.Rewards.Count > 0)
				{
					RewardDispatcher.Grant(entry.Rewards, player);
				}
				completedIds.Add(entry.Id);
			}
			else
			{
				ChainState.SetStep(player, entry.Id, num);
				Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' advanced to step {num}/{entry.Steps.Count}.");
				GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
			}
		}

		public static void PlayGlobalReceived(string entryId, string sourcePlayerName)
		{
			GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId);
			if (guidanceEntry == null)
			{
				Plugin.Log.LogWarning((object)("[global] received play for unknown id '" + entryId + "'."));
				return;
			}
			Plugin.Log.LogInfo((object)("[global] showing '" + entryId + "' (triggered by " + sourcePlayerName + ")."));
			string template = ((!string.IsNullOrEmpty(guidanceEntry.Message)) ? guidanceEntry.Message : guidanceEntry.Display?.Text);
			string renderedText = RenderDisplay(guidanceEntry, null, template, null, sourcePlayerName);
			GuidanceDisplay.Show(guidanceEntry, renderedText);
			if ((Object)(object)Player.m_localPlayer != (Object)null)
			{
				DebugFireLog.Record(Player.m_localPlayer.GetPlayerName(), guidanceEntry.Id);
			}
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entryId
			});
		}

		internal static bool CheckGates(GuidanceEntry entry, Player player)
		{
			if (!PlayerProgress.IsReady)
			{
				return false;
			}
			if (!RequirementsMet(entry, player))
			{
				return false;
			}
			if (StopConditionMet(entry, player))
			{
				return false;
			}
			if (entry.Once && SeenTracker.HasFired(player, entry.Id, entry.Scope))
			{
				return false;
			}
			if (!SeenTracker.CooldownReady(entry.Id, entry.Cooldown, Time.time))
			{
				return false;
			}
			int num = entry.Trigger?.MaxFires ?? 0;
			if (num > 0 && SeenTracker.GetFireCount(player, entry.Id) >= num)
			{
				return false;
			}
			return true;
		}

		internal static void FireById(string entryId)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => string.Equals(g.Id, entryId, StringComparison.OrdinalIgnoreCase));
			if (guidanceEntry == null)
			{
				Plugin.Log.LogWarning((object)("[dispatch] FireById: entry '" + entryId + "' not found."));
				return;
			}
			if (!CheckGates(guidanceEntry, localPlayer))
			{
				Plugin.Log.LogInfo((object)("[dispatch] FireById: '" + entryId + "' gates blocked."));
				return;
			}
			Plugin.Log.LogInfo((object)("[dispatch] FireById firing '" + entryId + "'."));
			string template = ((!string.IsNullOrEmpty(guidanceEntry.Message)) ? guidanceEntry.Message : guidanceEntry.Display?.Text);
			string renderedText = RenderDisplay(guidanceEntry, null, template, null, localPlayer.GetPlayerName());
			GuidanceDisplay.Show(guidanceEntry, renderedText);
			if (guidanceEntry.Once)
			{
				SeenTracker.MarkFired(localPlayer, guidanceEntry.Id, guidanceEntry.Scope);
			}
			if ((guidanceEntry.Trigger?.MaxFires ?? 0) > 0)
			{
				SeenTracker.IncrementFireCount(localPlayer, guidanceEntry.Id);
			}
			SeenTracker.MarkCooldown(guidanceEntry.Id, guidanceEntry.Cooldown, Time.time);
			DebugFireLog.Record(localPlayer.GetPlayerName(), guidanceEntry.Id);
			MaybeLogQuestStart(localPlayer, guidanceEntry);
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entryId
			});
		}

		internal static void FireEntry(GuidanceEntry entry, TriggerEvent evt)
		{
			Player player = Player.m_localPlayer;
			if ((Object)(object)player == (Object)null || entry == null)
			{
				return;
			}
			if (!PlayerProgress.IsReady)
			{
				Plugin.Log.LogDebug((object)("[dispatch] FireEntry '" + entry.Id + "' deferred: progress not loaded yet."));
				return;
			}
			if (SeenTracker.IsGlobalScope(entry.Scope))
			{
				Plugin.Log.LogInfo((object)("[dispatch] FireEntry '" + entry.Id + "' (global) -> server."));
				GuidanceSync.SendTriggerGlobal(entry.Id, player.GetPlayerName());
				MaybeLogQuestStart(player, entry);
				SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time);
				return;
			}
			Plugin.Log.LogInfo((object)("[dispatch] FireEntry firing '" + entry.Id + "' via mode '" + entry.Display?.Mode + "'."));
			string template = ((!string.IsNullOrEmpty(entry.Message)) ? entry.Message : entry.Display?.Text);
			string renderedText = RenderDisplay(entry, null, template, evt, player.GetPlayerName());
			GuidanceDisplay.Show(entry, renderedText);
			if (entry.Once)
			{
				SeenTracker.MarkFired(player, entry.Id, entry.Scope);
			}
			if ((entry.Trigger?.MaxFires ?? 0) > 0)
			{
				SeenTracker.IncrementFireCount(player, entry.Id);
			}
			SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time);
			DebugFireLog.Record(player.GetPlayerName(), entry.Id);
			MaybeLogQuestStart(player, entry);
			if (entry.Announce?.Discord != null)
			{
				GuidanceSync.SendAnnounceRequest(entry.Id, player.GetPlayerName());
			}
			if (entry.DiscordOnComplete)
			{
				GuidanceSync.SendCompleteAnnounce(entry.Id, player.GetPlayerName());
			}
			if (entry.Rewards != null && entry.Rewards.Count > 0)
			{
				RewardDispatcher.Grant(entry.Rewards, player, (string s) => TemplateText(s, evt, player.GetPlayerName()), (string s) => TextHighlighter.Apply(s, entry));
			}
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entry.Id
			});
		}

		private static void MaybeLogQuestStart(Player player, GuidanceEntry entry)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)player == (Object)null) && entry != null && !string.IsNullOrEmpty(entry.Id) && !QuestStartLogState.WasLogged(player, entry.Id))
			{
				QuestStartLogState.MarkLogged(player, entry.Id);
				Vector3 position = ((Component)player).transform.position;
				string biome = ((object)player.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString();
				GuidanceSync.SendQuestStartLog(entry.Id, player.GetPlayerName(), biome, position);
			}
		}

		private static bool Matches(GuidanceEntry entry, TriggerEvent evt)
		{
			if (entry.Trigger == null)
			{
				return false;
			}
			if (Eq(entry.Trigger.Type, "timed") && string.IsNullOrEmpty(entry.Trigger.Id))
			{
				if (Eq(evt.Type, "timed"))
				{
					return Eq(entry.Id, evt.Subject);
				}
				return false;
			}
			return MatchesTrigger(entry.Trigger, evt);
		}

		internal static bool MatchesTrigger(TriggerSpec t, TriggerEvent evt)
		{
			if (t == null)
			{
				return false;
			}
			if (!string.Equals(t.Type, evt.Type, StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			switch (evt.Type.ToLowerInvariant())
			{
			case "craft":
				return Eq(t.Item, evt.Subject);
			case "pickup":
				return Eq(t.Item, evt.Subject);
			case "kill":
				return Eq(t.Creature, evt.Subject);
			case "tamed_creature":
				if (!string.IsNullOrEmpty(t.Creature))
				{
					return Eq(t.Creature, evt.Subject);
				}
				return true;
			case "crafting_table_used":
				if (!string.IsNullOrEmpty(t.Station))
				{
					return Eq(t.Station, evt.Subject);
				}
				return true;
			case "cooking_used":
				if (!string.IsNullOrEmpty(t.Station))
				{
					return Eq(t.Station, evt.Subject);
				}
				return true;
			case "portal_used":
				if (!string.IsNullOrEmpty(t.Tag))
				{
					return Eq(t.Tag, evt.Subject);
				}
				return true;
			case "ward_activated":
			case "sign_read":
			case "tombstone_picked":
			case "ship_sailed":
				return true;
			case "build":
				return Eq(t.Piece, evt.Subject);
			case "biome":
				return Eq(t.Biome, evt.Subject);
			case "equip":
				return Eq(t.Item, evt.Subject);
			case "boss_defeated":
				return Eq(t.Creature, evt.Subject);
			case "item_acquired":
				return WildcardMatch(t.Item, evt.Subject);
			case "location_entered":
				return WildcardMatch(t.Location, evt.Subject);
			case "distance":
				return WildcardMatch(t.Location, evt.Subject);
			case "npc_interacted":
			case "npc_conversation":
				return Eq(t.Npc, evt.Subject);
			case "npc_item_submit":
			{
				if (!Eq(t.Npc, evt.Subject))
				{
					return false;
				}
				if (string.IsNullOrEmpty(t.Item))
				{
					return true;
				}
				string b = ((evt.Extra == null || !evt.Extra.ContainsKey("item")) ? null : evt.Extra["item"]?.ToString());
				return Eq(t.Item, b);
			}
			case "skill_level":
				return MatchSkillLevel(t, evt.Subject);
			case "timed":
				return Eq(t.Id, evt.Subject);
			case "entry_finished":
				return Eq(t.Entry, evt.Subject);
			case "dvergr_recruited":
			case "dvergr_duel_won":
			case "dvergr_rank_changed":
			case "dvergr_rank_first":
				if (!string.IsNullOrEmpty(t.Caste))
				{
					return Eq(t.Caste, evt.Subject);
				}
				return true;
			case "dvergr_level_up":
				return MatchDvergrLevelUp(t, evt.Subject);
			case "dvergr_party_duel_won":
			case "dvergr_party_rank_changed":
			case "dvergr_party_rank_first":
				return true;
			case "dvergr_tournament_joined":
			case "dvergr_tournament_match":
			case "dvergr_tournament_won":
				if (!string.IsNullOrEmpty(t.Caste))
				{
					return Eq(t.Caste, evt.Subject);
				}
				return true;
			case "dvergr_bounty_resolved":
			case "dvergr_bounty_valcoin":
				if (t.Tier > 0)
				{
					return Eq(t.Tier.ToString(), evt.Subject);
				}
				return true;
			case "first_login":
			case "chest_opened":
			case "player_death":
				return true;
			default:
				return true;
			}
		}

		private static bool Eq(string a, string b)
		{
			if (!string.IsNullOrEmpty(a))
			{
				return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		private static bool WildcardMatch(string pattern, string value)
		{
			if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value))
			{
				return false;
			}
			if (pattern.EndsWith("*"))
			{
				string value2 = pattern.Substring(0, pattern.Length - 1);
				return value.StartsWith(value2, StringComparison.OrdinalIgnoreCase);
			}
			return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase);
		}

		private static bool MatchSkillLevel(TriggerSpec t, string subject)
		{
			if (string.IsNullOrEmpty(t.Skill) || string.IsNullOrEmpty(subject))
			{
				return false;
			}
			int num = subject.IndexOf(':');
			if (num < 0)
			{
				return false;
			}
			string b = subject.Substring(0, num);
			if (!int.TryParse(subject.Substring(num + 1), out var result))
			{
				return false;
			}
			if (string.Equals(t.Skill, b, StringComparison.OrdinalIgnoreCase))
			{
				return t.Level == result;
			}
			return false;
		}

		private static bool MatchDvergrLevelUp(TriggerSpec t, string subject)
		{
			if (string.IsNullOrEmpty(subject))
			{
				return false;
			}
			int num = subject.IndexOf(':');
			if (num < 0)
			{
				return false;
			}
			string b = subject.Substring(0, num);
			if (!int.TryParse(subject.Substring(num + 1), out var result))
			{
				return false;
			}
			if (!string.IsNullOrEmpty(t.Caste) && !Eq(t.Caste, b))
			{
				return false;
			}
			if (t.Level > 0 && t.Level != result)
			{
				return false;
			}
			return true;
		}

		private static bool RequirementsMet(GuidanceEntry entry, Player player)
		{
			return PrerequisiteChecker.AllSatisfied(entry.Requires, player, Plugin.CurrentConfig);
		}

		private static bool StopConditionMet(GuidanceEntry entry, Player player)
		{
			if (entry.StopWhen == null || entry.StopWhen.Count == 0)
			{
				return false;
			}
			foreach (string item in entry.StopWhen)
			{
				if (SeenTracker.HasFired(player, item, "player"))
				{
					return true;
				}
			}
			return false;
		}

		internal static string TemplateText(string template, TriggerEvent evt, string playerName, int step = -1, int total = -1)
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			if (string.IsNullOrEmpty(template))
			{
				return template;
			}
			string text = "";
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null)
			{
				text = ((object)localPlayer.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString();
			}
			string newValue = "";
			string newValue2 = "";
			if (evt != null && string.Equals(evt.Type, "skill_level", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(evt.Subject))
			{
				int num = evt.Subject.IndexOf(':');
				if (num >= 0)
				{
					newValue = evt.Subject.Substring(0, num);
					newValue2 = evt.Subject.Substring(num + 1);
				}
			}
			string newValue3 = TriggerUtils.LocalizeName(evt?.DisplayName ?? evt?.Subject ?? "");
			string text2 = template.Replace("{playerName}", playerName ?? "").Replace("{player_name}", playerName ?? "").Replace("{itemName}", newValue3)
				.Replace("{creatureName}", newValue3)
				.Replace("{biome}", (!string.IsNullOrEmpty(text)) ? text : (evt?.Subject ?? ""))
				.Replace("{skill}", newValue)
				.Replace("{level}", newValue2)
				.Replace("{rank}", Extra("rank"))
				.Replace("{rating}", Extra("rating"))
				.Replace("{companionName}", Extra("companionName"))
				.Replace("{ownerName}", Extra("ownerName"))
				.Replace("{partyName}", Extra("partyName"))
				.Replace("{winSize}", Extra("winSize"))
				.Replace("{opponentOwner}", Extra("opponentOwner"))
				.Replace("{mvpCaste}", Extra("mvpCaste"))
				.Replace("{round}", Extra("round"))
				.Replace("{opponent}", Extra("opponent"))
				.Replace("{mode}", Extra("mode"))
				.Replace("{bracketSize}", Extra("bracketSize"))
				.Replace("{tier}", Extra("tier"))
				.Replace("{tierName}", Extra("tierName"))
				.Replace("{method}", Extra("method"))
				.Replace("{bountyBiome}", Extra("bountyBiome"))
				.Replace("{caste}", evt?.Subject ?? "");
			if (step >= 0)
			{
				text2 = text2.Replace("{step}", step.ToString());
			}
			if (total >= 0)
			{
				text2 = text2.Replace("{total}", total.ToString());
			}
			return text2;
			string Extra(string key)
			{
				if (evt?.Extra != null && evt.Extra.TryGetValue(key, out var value))
				{
					return value?.ToString() ?? "";
				}
				return "";
			}
		}

		internal static string RenderDisplay(GuidanceEntry entry, GuidanceStep step, string template, TriggerEvent evt, string playerName, int stepNum = -1, int total = -1)
		{
			return TextHighlighter.Apply(TemplateText(template, evt, playerName, stepNum, total), entry, step);
		}

		internal static string RenderLocal(GuidanceEntry entry, string template, GuidanceStep step = null)
		{
			return TextHighlighter.Apply(TemplateLocal(template), entry, step);
		}

		internal static string TemplateLocal(string template)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			if (string.IsNullOrEmpty(template))
			{
				return template;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return template;
			}
			string playerName = localPlayer.GetPlayerName();
			if (!string.IsNullOrEmpty(playerName))
			{
				template = template.Replace("{playerName}", playerName).Replace("{player_name}", playerName);
			}
			string text = ((object)localPlayer.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString();
			if (!string.IsNullOrEmpty(text))
			{
				template = template.Replace("{biome}", text);
			}
			return template;
		}

		public static void CheckVersionUpdates(Player player, GuidanceConfig config)
		{
			if ((Object)(object)player == (Object)null || config?.Guidances == null || !PlayerProgress.IsReady)
			{
				return;
			}
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				if (guidance.Steps == null || guidance.Steps.Count == 0 || !ChainState.IsComplete(player, guidance.Id))
				{
					continue;
				}
				int completedVersion = ChainState.GetCompletedVersion(player, guidance.Id);
				if (guidance.Version > completedVersion)
				{
					GuidanceStep guidanceStep = guidance.Steps[guidance.Steps.Count - 1];
					string template = ((!string.IsNullOrEmpty(guidanceStep.Message)) ? guidanceStep.Message : (guidanceStep.Display?.Text ?? guidance.Title ?? guidance.Id));
					string text = RenderDisplay(guidance, guidanceStep, template, null, player.GetPlayerName());
					if ((Object)(object)MessageHud.instance != (Object)null)
					{
						GuidanceDisplay.EnsureTopLeftMessageWraps();
						MessageHud.instance.ShowMessage((MessageType)1, text, 0, (Sprite)null, false, true);
					}
					Plugin.Log.LogInfo((object)("[dispatch] Version update for '" + guidance.Id + "': " + $"seen v{completedVersion}, current v{guidance.Version}. Re-delivered notification."));
					ChainState.SetCompletedVersion(player, guidance.Id, guidance.Version);
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnSpawned")]
	internal static class PlayerOnSpawnedDispatchPatch
	{
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				GuidanceDispatcher.CheckVersionUpdates(__instance, Plugin.CurrentConfig);
				ItemAcquiredTrigger.CheckAllCountGoals();
				SkillLevelTrigger.CheckAllSkillLevels();
			}
		}
	}
	public class TriggerEvent
	{
		public string Type;

		public string Subject;

		public string DisplayName;

		public Dictionary<string, object> Extra;
	}
	[HarmonyPatch(typeof(Humanoid), "Pickup")]
	internal static class ItemAcquiredTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Humanoid __instance, GameObject go)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)go == (Object)null))
			{
				string text = TriggerUtils.NormalizePrefabName(((Object)go).name);
				if (!string.IsNullOrEmpty(text))
				{
					string displayName = go.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_name;
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "item_acquired",
						Subject = text,
						DisplayName = displayName
					});
					CheckCountGoals(text, displayName);
				}
			}
		}

		internal static List<ItemGoalSpec> GetEffectiveGoals(TriggerSpec trigger)
		{
			if (trigger == null)
			{
				return null;
			}
			if (trigger.Goals != null && trigger.Goals.Count > 0)
			{
				return trigger.Goals;
			}
			if (!string.IsNullOrEmpty(trigger.Item) && trigger.Count > 1)
			{
				return new List<ItemGoalSpec>
				{
					new ItemGoalSpec
					{
						Item = trigger.Item,
						Count = trigger.Count
					}
				};
			}
			return null;
		}

		private static bool IsCountGoalEntry(GuidanceEntry entry)
		{
			if (entry.Trigger == null)
			{
				return false;
			}
			if (!string.Equals(entry.Trigger.Type, "item_acquired", StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			return GetEffectiveGoals(entry.Trigger) != null;
		}

		internal static void CheckAllCountGoals()
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (!IsCountGoalEntry(guidance) || !GuidanceDispatcher.CheckGates(guidance, localPlayer))
				{
					continue;
				}
				List<ItemGoalSpec> effectiveGoals = GetEffectiveGoals(guidance.Trigger);
				bool flag = true;
				bool flag2 = false;
				foreach (ItemGoalSpec item in effectiveGoals)
				{
					int num = CountInInventory(localPlayer, item.Item);
					Plugin.Log.LogInfo((object)$"[item_acquired] '{guidance.Id}' seed {item.Item}: {num}/{item.Count}.");
					if (num < item.Count)
					{
						flag = false;
					}
					if (num > 0)
					{
						flag2 = true;
					}
				}
				if (flag)
				{
					Plugin.Log.LogInfo((object)("[item_acquired] '" + guidance.Id + "' all goals already met — firing."));
					GoalStartedState.Clear(localPlayer, guidance.Id);
					GuidanceDispatcher.FireEntry(guidance, new TriggerEvent
					{
						Type = "item_acquired",
						Subject = effectiveGoals[0].Item
					});
					GuidanceHudTracker.Instance?.FlashCompletion(guidance.Id);
				}
				else if (flag2 || GoalStartedState.IsStarted(localPlayer, guidance.Id))
				{
					if (flag2)
					{
						GoalStartedState.MarkStarted(localPlayer, guidance.Id);
					}
					GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
				}
			}
		}

		internal static void CheckCountGoals(string prefabName, string displayName)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (!IsCountGoalEntry(guidance))
				{
					continue;
				}
				List<ItemGoalSpec> effectiveGoals = GetEffectiveGoals(guidance.Trigger);
				bool flag = false;
				foreach (ItemGoalSpec item in effectiveGoals)
				{
					if (ItemWildcardMatch(item.Item, prefabName))
					{
						flag = true;
						break;
					}
				}
				if (!flag || !GuidanceDispatcher.CheckGates(guidance, localPlayer))
				{
					continue;
				}
				bool flag2 = true;
				bool flag3 = false;
				foreach (ItemGoalSpec item2 in effectiveGoals)
				{
					int num = CountInInventory(localPlayer, item2.Item);
					Plugin.Log.LogInfo((object)$"[item_acquired] '{guidance.Id}' {item2.Item}: {num}/{item2.Count}.");
					if (num < item2.Count)
					{
						flag2 = false;
					}
					if (num > 0)
					{
						flag3 = true;
					}
				}
				if (flag2)
				{
					Plugin.Log.LogInfo((object)("[item_acquired] '" + guidance.Id + "' all goals reached — firing."));
					GoalStartedState.Clear(localPlayer, guidance.Id);
					GuidanceDispatcher.FireEntry(guidance, new TriggerEvent
					{
						Type = "item_acquired",
						Subject = prefabName,
						DisplayName = displayName
					});
					GuidanceHudTracker.Instance?.FlashCompletion(guidance.Id);
				}
				else
				{
					if (flag3)
					{
						GoalStartedState.MarkStarted(localPlayer, guidance.Id);
					}
					GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
				}
			}
		}

		internal static int CountInInventory(Player player, string prefabPattern)
		{
			if (string.IsNullOrEmpty(prefabPattern))
			{
				return 0;
			}
			Inventory inventory = ((Humanoid)player).GetInventory();
			if (inventory == null)
			{
				return 0;
			}
			int num = 0;
			foreach (ItemData allItem in inventory.GetAllItems())
			{
				if (!((Object)(object)allItem?.m_dropPrefab == (Object)null))
				{
					string value = TriggerUtils.NormalizePrefabName(((Object)allItem.m_dropPrefab).name);
					if (ItemWildcardMatch(prefabPattern, value))
					{
						num += allItem.m_stack;
					}
				}
			}
			return num;
		}

		internal static string BuildGoalProgressText(Player player, List<ItemGoalSpec> goals)
		{
			if ((Object)(object)player == (Object)null || goals == null || goals.Count == 0)
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder();
			foreach (ItemGoalSpec goal in goals)
			{
				int value = Math.Min(CountInInventory(player, goal.Item), goal.Count);
				if (stringBuilder.Length > 0)
				{
					stringBuilder.Append('\n');
				}
				stringBuilder.Append(goal.Item).Append(": ").Append(value)
					.Append('/')
					.Append(goal.Count);
			}
			return stringBuilder.ToString();
		}

		internal static bool ItemWildcardMatch(string pattern, string value)
		{
			if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value))
			{
				return false;
			}
			if (pattern.EndsWith("*"))
			{
				string value2 = pattern.Substring(0, pattern.Length - 1);
				return value.StartsWith(value2, StringComparison.OrdinalIgnoreCase);
			}
			return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase);
		}
	}
	[HarmonyPatch(typeof(InventoryGui), "DoCrafting")]
	internal static class ItemAcquiredCraftPatch
	{
		[HarmonyPostfix]
		private static void Postfix(InventoryGui __instance, Player player)
		{
			if ((Object)(object)player != (Object)(object)Player.m_localPlayer)
			{
				return;
			}
			Recipe craftRecipe = __instance.m_craftRecipe;
			if (!((Object)(object)craftRecipe?.m_item == (Object)null))
			{
				string text = TriggerUtils.NormalizePrefabName(((Object)((Component)craftRecipe.m_item).gameObject).name);
				if (!string.IsNullOrEmpty(text))
				{
					string displayName = craftRecipe.m_item.m_itemData?.m_shared?.m_name;
					ItemAcquiredTrigger.CheckCountGoals(text, displayName);
				}
			}
		}
	}
	[HarmonyPatch(typeof(Character), "OnDeath")]
	internal static class KillTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Character __instance)
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			object obj;
			if (__instance == null)
			{
				obj = null;
			}
			else
			{
				HitData lastHit = __instance.m_lastHit;
				obj = ((lastHit != null) ? lastHit.GetAttacker() : null);
			}
			Character val = (Character)obj;
			if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer))
			{
				GameObject gameObject = ((Component)__instance).gameObject;
				string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (!string.IsNullOrEmpty(text))
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "kill",
						Subject = text,
						DisplayName = __instance.m_name
					});
					KillCountTracker.CheckKillCount(text, __instance.m_name);
				}
			}
		}
	}
	internal static class KillCountTracker
	{
		internal const float ShareProgressRadius = 50f;

		internal static void CheckKillCount(string creature, string displayName)
		{
			//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				TriggerSpec trigger = guidance.Trigger;
				if (trigger != null && string.Equals(trigger.Type, "kill", StringComparison.OrdinalIgnoreCase) && trigger.Count > 1 && !string.IsNullOrEmpty(trigger.Creature) && string.Equals(trigger.Creature, creature, StringComparison.OrdinalIgnoreCase) && GuidanceDispatcher.CheckGates(guidance, localPlayer))
				{
					ApplyIncrement(guidance, localPlayer, creature, displayName);
					if (trigger.ShareProgress)
					{
						GuidanceSync.SendShareKillProgress(guidance.Id, localPlayer.GetPlayerName(), ((Component)localPlayer).transform.position);
					}
				}
			}
		}

		private static void ApplyIncrement(GuidanceEntry entry, Player player, string creature, string displayName)
		{
			int count = entry.Trigger.Count;
			int num = KillCountState.Get(player, entry.Id) + 1;
			Plugin.Log.LogInfo((object)$"[kill] '{entry.Id}' progress {num}/{count} ({creature}).");
			if (num >= count)
			{
				KillCountState.Clear(player, entry.Id);
				Plugin.Log.LogInfo((object)$"[kill] '{entry.Id}' complete ({count}/{count}) — firing.");
				GuidanceDispatcher.FireEntry(entry, new TriggerEvent
				{
					Type = "kill",
					Subject = creature,
					DisplayName = displayName
				});
				GuidanceHudTracker.Instance?.FlashCompletion(entry.Id);
			}
			else
			{
				KillCountState.Set(player, entry.Id, num);
				string arg = ((!string.IsNullOrEmpty(entry.Title)) ? entry.Title : TriggerUtils.LocalizeName(displayName ?? creature));
				CenterToast.Queue($"{arg}: {num}/{count}");
				GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
			}
		}

		internal static void ApplySharedIncrement(string entryId, Player localPlayer, Vector3 killerPosition)
		{
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: 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)
			if (string.IsNullOrEmpty(entryId) || (Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			Vector3 val = ((Component)localPlayer).transform.position - killerPosition;
			if (!(((Vector3)(ref val)).sqrMagnitude > 2500f))
			{
				GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId);
				if (guidanceEntry?.Trigger != null && string.Equals(guidanceEntry.Trigger.Type, "kill", StringComparison.OrdinalIgnoreCase) && guidanceEntry.Trigger.Count > 1 && GuidanceDispatcher.CheckGates(guidanceEntry, localPlayer))
				{
					Plugin.Log.LogInfo((object)("[kill] '" + entryId + "' shared credit from nearby party kill."));
					ApplyIncrement(guidanceEntry, localPlayer, guidanceEntry.Trigger.Creature, guidanceEntry.Trigger.Creature);
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "Update")]
	internal static class LocationEnteredTrigger
	{
		private const float CheckInterval = 5f;

		private const float DetectRadius = 40f;

		private const string KeyPrefix = "loc_";

		private static float _nextCheck;

		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: 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)
			//IL_0176: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Unknown result type (might be due to invalid IL or missing references)
			//IL_017d: Unknown result type (might be due to invalid IL or missing references)
			//IL_019b: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d7: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer || (Object)(object)ZoneSystem.instance == (Object)null || !PlayerProgress.IsReady || Time.time < _nextCheck)
			{
				return;
			}
			_nextCheck = Time.time + 5f;
			Vector3 position = ((Component)__instance).transform.position;
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (Location s_allLocation in Location.s_allLocations)
			{
				if ((Object)(object)s_allLocation == (Object)null || Vector3.Distance(position, ((Component)s_allLocation).transform.position) > 40f)
				{
					continue;
				}
				string text = ((Object)((Component)s_allLocation).gameObject).name;
				if (text.EndsWith("(Clone)"))
				{
					text = text.Substring(0, text.Length - 7).TrimEnd(Array.Empty<char>());
				}
				if (!string.IsNullOrEmpty(text))
				{
					Plugin.Log.LogDebug((object)("[location_entered] Scene scan in range: '" + text + "'"));
					string id = "loc_" + text;
					if (!SeenTracker.HasFired(__instance, id))
					{
						SeenTracker.MarkFired(__instance, id);
						hashSet.Add(text);
						GuidanceDispatcher.Raise(new TriggerEvent
						{
							Type = "location_entered",
							Subject = text
						});
					}
				}
			}
			foreach (KeyValuePair<Vector2s, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances)
			{
				LocationInstance value = locationInstance.Value;
				string text2 = value.m_location?.m_prefabName;
				if (string.IsNullOrEmpty(text2))
				{
					text2 = value.m_location?.m_name;
				}
				if (string.IsNullOrEmpty(text2))
				{
					continue;
				}
				float num = Vector3.Distance(position, value.m_position);
				if (num > 40f)
				{
					continue;
				}
				if (!value.m_placed)
				{
					Plugin.Log.LogDebug((object)$"[location_entered] ZoneSystem in range but unplaced: '{text2}' dist={num:F0}");
					continue;
				}
				Plugin.Log.LogDebug((object)$"[location_entered] ZoneSystem in range (placed): '{text2}' dist={num:F0}");
				if (!hashSet.Contains(text2))
				{
					string id2 = "loc_" + text2;
					if (!SeenTracker.HasFired(__instance, id2))
					{
						SeenTracker.MarkFired(__instance, id2);
						GuidanceDispatcher.Raise(new TriggerEvent
						{
							Type = "location_entered",
							Subject = text2
						});
					}
				}
			}
		}
	}
	[HarmonyPatch(typeof(Trader), "Interact")]
	internal static class NpcConversationTrigger
	{
		[HarmonyPrefix]
		private static bool Prefix(Trader __instance, Humanoid character, bool hold, ref bool __result)
		{
			Player val = (Player)(object)((character is Player) ? character : null);
			if ((Object)(object)val == (Object)null || (Object)(object)val != (Object)(object)Player.m_localPlayer)
			{
				return true;
			}
			if (hold)
			{
				return true;
			}
			if (!ConversationModifierHeld())
			{
				return true;
			}
			GameObject gameObject = ((Component)__instance).gameObject;
			string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
			List<GuidanceEntry> list = FindAllEntries(text, val);
			if (list.Count == 0)
			{
				return true;
			}
			OpenConversation(__instance, val, text, list);
			__result = true;
			return false;
		}

		private static bool ConversationModifierHeld()
		{
			if (Input.GetKey((KeyCode)304) || Input.GetKey((KeyCode)303))
			{
				return true;
			}
			if (!ZInput.GetButton("Run"))
			{
				return ZInput.GetButton("JoyRun");
			}
			return true;
		}

		internal static void OpenConversation(Trader trader, Player player, string subject, List<GuidanceEntry> entries)
		{
			if (entries.Count == 1)
			{
				GuidanceEntry guidanceEntry = entries[0];
				string template = ((!string.IsNullOrEmpty(guidanceEntry.Message)) ? guidanceEntry.Message : guidanceEntry.Display?.Text);
				string renderedText = GuidanceDispatcher.RenderDisplay(guidanceEntry, null, template, null, player.GetPlayerName());
				GuidanceDisplay.Show(guidanceEntry, renderedText);
			}
			else
			{
				NpcConversationPanel.Get().OpenSelection(trader.m_name, entries, subject);
			}
		}

		internal static GuidanceEntry FindEntry(string npcSubject, Player player)
		{
			if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null)
			{
				return null;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return null;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (guidance.Trigger != null && string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) && string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) && GuidanceDispatcher.CheckGates(guidance, player))
				{
					return guidance;
				}
			}
			return null;
		}

		internal static List<GuidanceEntry> FindAllEntries(string npcSubject, Player player)
		{
			List<GuidanceEntry> list = new List<GuidanceEntry>();
			if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null)
			{
				return list;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return list;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (guidance.Trigger != null && string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) && string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) && GuidanceDispatcher.CheckGates(guidance, player))
				{
					list.Add(guidance);
				}
			}
			return list;
		}
	}
	[HarmonyPatch(typeof(Trader), "GetHoverText")]
	internal static class TraderHoverTextPatch
	{
		internal const string ConversationHint = "[Shift + E] Quest";

		[HarmonyPostfix]
		private static void Postfix(Trader __instance, ref string __result)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GameObject gameObject = ((Component)__instance).gameObject;
			string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
			GuidanceEntry guidanceEntry = NpcConversationTrigger.FindEntry(npcSubject, localPlayer);
			if (guidanceEntry != null && guidanceEntry.HoverText?.Default != null)
			{
				__result = __result + "\n" + GuidanceDispatcher.RenderLocal(guidanceEntry, guidanceEntry.HoverText.Default);
				return;
			}
			GuidanceEntry guidanceEntry2 = FindFiredEntryWithAfterFireHover(npcSubject, localPlayer);
			if (guidanceEntry2 != null)
			{
				__result = __result + "\n" + GuidanceDispatcher.RenderLocal(guidanceEntry2, guidanceEntry2.HoverText.AfterFire);
			}
			else if (guidanceEntry != null)
			{
				__result += "\n[Shift + E] Quest";
			}
		}

		private static GuidanceEntry FindFiredEntryWithAfterFireHover(string npcSubject, Player player)
		{
			if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null)
			{
				return null;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return null;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (guidance.Trigger != null && string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) && string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(guidance.HoverText?.AfterFire) && guidance.Once && SeenTracker.HasFired(player, guidance.Id, guidance.Scope))
				{
					return guidance;
				}
			}
			return null;
		}
	}
	[HarmonyPatch(typeof(StoreGui), "Show")]
	internal static class NpcInteractedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Trader trader)
		{
			if (!((Object)(object)trader == (Object)null) && !((Object)(object)Player.m_localPlayer == (Object)null))
			{
				GameObject gameObject = ((Component)trader).gameObject;
				string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (!string.IsNullOrEmpty(text))
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "npc_interacted",
						Subject = text,
						DisplayName = trader.m_name
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(Trader), "UseItem")]
	internal static class NpcItemSubmitTrigger
	{
		[HarmonyPrefix]
		private static bool Prefix(Trader __instance, Humanoid user, ItemData item, ref bool __result)
		{
			if (item == null)
			{
				return true;
			}
			if ((Object)(object)user == (Object)null || (Object)(object)user != (Object)(object)Player.m_localPlayer)
			{
				return true;
			}
			GameObject gameObject = ((Component)__instance).gameObject;
			string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
			if (string.IsNullOrEmpty(text))
			{
				return true;
			}
			string text2 = ResolveItemName(item);
			Plugin.Log.LogInfo((object)("[item_submit] '" + ((object)user).GetType().Name + "' used '" + text2 + "' (token '" + item.m_shared?.m_name + "') on '" + text + "'."));
			if (IsVanillaUseItem(__instance, item))
			{
				Plugin.Log.LogInfo((object)("[item_submit] '" + text2 + "' is a vanilla quest item — deferring to vanilla."));
				return true;
			}
			Player val = (Player)(object)((user is Player) ? user : null);
			if ((Object)(object)val == (Object)null)
			{
				return true;
			}
			GuidanceEntry guidanceEntry = FindEntry(text, text2, val);
			if (guidanceEntry != null)
			{
				HandleSubmission(__instance, val, item, text2, text, guidanceEntry);
				__result = true;
				return false;
			}
			if (__instance.m_useItems != null && __instance.m_useItems.Count > 0)
			{
				Plugin.Log.LogInfo((object)"[item_submit] no entry; NPC has vanilla useItems — deferring to vanilla rejection.");
				return true;
			}
			if (NpcHasConfiguredEntries(text))
			{
				Plugin.Log.LogInfo((object)("[item_submit] no entry; suppressing vanilla 'can't use' on owned NPC '" + text + "'."));
				__result = true;
				return false;
			}
			return true;
		}

		private static void HandleSubmission(Trader trader, Player player, ItemData item, string itemPrefabName, string npcSubject, GuidanceEntry entry)
		{
			int num = ((entry.Trigger.Count <= 0) ? 1 : entry.Trigger.Count);
			bool consume = entry.Trigger.Consume;
			string text = Localized(item);
			TriggerEvent evt = new TriggerEvent
			{
				Type = "npc_item_submit",
				Subject = npcSubject,
				DisplayName = text,
				Extra = new Dictionary<string, object> { { "item", itemPrefabName } }
			};
			if (num <= 1)
			{
				if (consume)
				{
					ConsumeItems(player, item, 1);
				}
				Plugin.Log.LogInfo((object)("[item_submit] firing entry '" + entry.Id + "' (single) for '" + itemPrefabName + "' -> '" + npcSubject + "'."));
				GuidanceDispatcher.FireEntry(entry, evt);
				return;
			}
			int num2 = SubmitState.Get(player, entry.Id);
			int num3 = num - num2;
			if (num3 <= 0)
			{
				num3 = num;
				num2 = 0;
			}
			int val = Math.Max(1, item.m_stack);
			int num4 = Math.Min(num3, val);
			if (consume)
			{
				ConsumeItems(player, item, num4);
			}
			int num5 = num2 + num4;
			Plugin.Log.LogInfo((object)($"[item_submit] '{entry.Id}' progress {num5}/{num} " + $"(+{num4} {itemPrefabName}, consume={consume})."));
			if (num5 >= num)
			{
				SubmitState.Clear(player, entry.Id);
				Plugin.Log.LogInfo((object)$"[item_submit] '{entry.Id}' complete ({num}/{num}) — firing.");
				GuidanceDispatcher.FireEntry(entry, evt);
				GuidanceHudTracker.Instance?.FlashCompletion(entry.Id);
				return;
			}
			SubmitState.Set(player, entry.Id, num5);
			string text2 = ((!string.IsNullOrEmpty(entry.Title)) ? entry.Title : text);
			CenterToast.Queue($"{text2}: {num5}/{num} {text}");
			GuidanceHudTracker.Instance?.Refresh(fromProgress: true);
		}

		private static void ConsumeItems(Player player, ItemData item, int amount)
		{
			if (amount > 0)
			{
				Inventory inventory = ((Humanoid)player).GetInventory();
				if (inventory != null)
				{
					inventory.RemoveItem(item, amount);
					((Character)player).ShowRemovedMessage(item, amount);
				}
			}
		}

		private static string Localized(ItemData item)
		{
			string text = item.m_shared?.m_name ?? "";
			if (Localization.instance == null)
			{
				return text;
			}
			return Localization.instance.Localize(text);
		}

		private static string ResolveItemName(ItemData item)
		{
			GameObject dropPrefab = item.m_dropPrefab;
			string text = ((dropPrefab != null) ? ((Object)dropPrefab).name : null);
			if (!string.IsNullOrEmpty(text))
			{
				return TriggerUtils.NormalizePrefabName(text);
			}
			return TriggerUtils.NormalizePrefabName(item.m_shared?.m_name ?? "");
		}

		private static bool IsVanillaUseItem(Trader trader, ItemData item)
		{
			if (trader.m_useItems == null || trader.m_useItems.Count == 0)
			{
				return false;
			}
			string text = item.m_shared?.m_name;
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			foreach (TraderUseItem useItem in trader.m_useItems)
			{
				string text2 = useItem?.m_prefab?.m_itemData?.m_shared?.m_name;
				if (!string.IsNullOrEmpty(text2) && string.Equals(text, text2, StringComparison.Ordinal))
				{
					return true;
				}
			}
			return false;
		}

		internal static GuidanceEntry FindEntry(string npcSubject, string itemPrefabName, Player player)
		{
			if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null)
			{
				return null;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return null;
			}
			GuidanceEntry guidanceEntry = null;
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "npc_item_submit", StringComparison.OrdinalIgnoreCase) || !string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) || !GuidanceDispatcher.CheckGates(guidance, player))
				{
					continue;
				}
				if (string.IsNullOrEmpty(guidance.Trigger.Item))
				{
					if (guidanceEntry == null)
					{
						guidanceEntry = guidance;
					}
				}
				else if (string.Equals(guidance.Trigger.Item, itemPrefabName, StringComparison.OrdinalIgnoreCase))
				{
					return guidance;
				}
			}
			return guidanceEntry;
		}

		internal static bool NpcHasConfiguredEntries(string npcSubject)
		{
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return false;
			}
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				if (guidance.Trigger != null && string.Equals(guidance.Trigger.Type, "npc_item_submit", StringComparison.OrdinalIgnoreCase) && string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(Trader), "GetHoverText")]
	internal static class NpcItemSubmitHoverPatch
	{
		private const string GiveItemLine = "\n[<color=yellow><b>1-8</b></color>] $npc_giveitem";

		[HarmonyPostfix]
		private static void Postfix(Trader __instance, ref string __result)
		{
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && (__instance.m_useItems == null || __instance.m_useItems.Count <= 0))
			{
				GameObject gameObject = ((Component)__instance).gameObject;
				if (NpcItemSubmitTrigger.NpcHasConfiguredEntries(TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null)))
				{
					string text = ((Localization.instance != null) ? Localization.instance.Localize("\n[<color=yellow><b>1-8</b></color>] $npc_giveitem") : "\n[<color=yellow><b>1-8</b></color>] $npc_giveitem");
					__result += text;
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnDeath")]
	internal static class PlayerDeathTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				GuidanceDispatcher.Raise(new TriggerEvent
				{
					Type = "player_death",
					Subject = ""
				});
			}
		}
	}
	[HarmonyPatch(typeof(TeleportWorld), "Teleport")]
	internal static class PortalUsedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(TeleportWorld __instance, Player player)
		{
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer))
			{
				string text = __instance.GetText() ?? "";
				GuidanceDispatcher.Raise(new TriggerEvent
				{
					Type = "portal_used",
					Subject = text,
					DisplayName = text
				});
			}
		}
	}
	[HarmonyPatch(typeof(ShipControlls), "Interact")]
	internal static class ShipSailedTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(ShipControlls __instance, Humanoid character, bool repeat)
		{
			if (!repeat)
			{
				Player val = (Player)(object)((character is Player) ? character : null);
				if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && !((Object)(object)((Character)val).GetStandingOnShip() != (Object)(object)__instance.m_ship))
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "ship_sailed"
					});
				}
			}
		}
	}
	[HarmonyPatch(typeof(Sign), "Interact")]
	internal static class SignReadTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(bool hold, bool __result)
		{
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && !hold && __result)
			{
				GuidanceDispatcher.Raise(new TriggerEvent
				{
					Type = "sign_read"
				});
			}
		}
	}
	[HarmonyPatch(typeof(Skills), "RaiseSkill")]
	internal static class SkillLevelTrigger
	{
		private static int _prevLevel;

		[HarmonyPrefix]
		private static void Prefix(Skills __instance, SkillType skillType)
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && !((Object)(object)Player.m_localPlayer.m_skills != (Object)(object)__instance))
			{
				_prevLevel = (int)__instance.GetSkillLevel(skillType);
			}
		}

		[HarmonyPostfix]
		private static void Postfix(Skills __instance, SkillType skillType)
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && !((Object)(object)Player.m_localPlayer.m_skills != (Object)(object)__instance))
			{
				int num = (int)__instance.GetSkillLevel(skillType);
				for (int i = _prevLevel + 1; i <= num; i++)
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "skill_level",
						Subject = $"{skillType}:{i}"
					});
				}
			}
		}

		internal static void CheckAllSkillLevels()
		{
			//IL_0116: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			List<(string, int)> list = new List<(string, int)>();
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				CollectThreshold(guidance.Trigger, list);
				if (guidance.Steps == null)
				{
					continue;
				}
				foreach (GuidanceStep step in guidance.Steps)
				{
					CollectThreshold(step?.Trigger, list);
				}
			}
			if (list.Count == 0)
			{
				return;
			}
			list.Sort(delegate((string skill, int level) a, (string skill, int level) b)
			{
				int num3 = string.Compare(a.skill, b.skill, StringComparison.OrdinalIgnoreCase);
				return (num3 == 0) ? a.level.CompareTo(b.level) : num3;
			});
			foreach (var (text, num) in list)
			{
				if (Enum.TryParse<SkillType>(text, ignoreCase: true, out SkillType result))
				{
					int num2 = (int)localPlayer.m_skills.GetSkillLevel(result);
					if (num2 >= num)
					{
						Plugin.Log.LogInfo((object)$"[skill_level] Login scan: {text}:{num} (player {num2}) — raising.");
						GuidanceDispatcher.Raise(new TriggerEvent
						{
							Type = "skill_level",
							Subject = $"{text}:{num}"
						});
					}
				}
			}
		}

		private static void CollectThreshold(TriggerSpec t, List<(string, int)> list)
		{
			if (t != null && string.Equals(t.Type, "skill_level", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(t.Skill) && t.Level > 0)
			{
				list.Add((t.Skill, t.Level));
			}
		}
	}
	[HarmonyPatch(typeof(Tameable), "Tame")]
	internal static class TamedCreatureTrigger
	{
		[HarmonyPostfix]
		private static void Postfix(Tameable __instance)
		{
			if (!((Object)(object)Player.m_localPlayer == (Object)null))
			{
				GameObject gameObject = ((Component)__instance).gameObject;
				string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (!string.IsNullOrEmpty(text))
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "tamed_creature",
						Subject = text,
						DisplayName = __instance.m_character?.m_name
					});
				}
			}
		}
	}
	internal static class TimedTrigger
	{
		private class Timer
		{
			public Coroutine Routine;

			public string TriggerId;

			public float Interval;

			public bool IsGlobal;
		}

		private static readonly Dictionary<string, Timer> _timers = new Dictionary<string, Timer>();

		public static void OnConfigChanged(GuidanceConfig config)
		{
			if ((Object)(object)Plugin.Instance == (Object)null)
			{
				return;
			}
			if (config?.Guidances == null)
			{
				StopAll();
				return;
			}
			bool flag = Application.isBatchMode && IsServerOrHost();
			bool flag2 = !IsServerOrHost();
			Dictionary<string, Timer> dictionary = new Dictionary<string, Timer>();
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				WarnIfTimedStep(guidance);
				if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "timed", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(guidance.Id))
				{
					continue;
				}
				float num = ParseInterval(guidance.Trigger.Interval);
				if (num <= 0f)
				{
					Plugin.Log.LogWarning((object)("[timed] '" + guidance.Id + "' has invalid interval '" + guidance.Trigger.Interval + "'; skipping. Use seconds (900), a suffixed value (15m / 2h / 1d), or daily / hourly."));
					continue;
				}
				bool flag3 = SeenTracker.IsGlobalScope(guidance.Scope);
				if ((!flag || flag3) && !(flag2 && flag3))
				{
					dictionary[guidance.Id] = new Timer
					{
						TriggerId = (guidance.Trigger.Id ?? guidance.Id),
						Interval = num,
						IsGlobal = flag3
					};
				}
			}
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, Timer> timer in _timers)
			{
				if (!dictionary.TryGetValue(timer.Key, out var value) || value.Interval != timer.Value.Interval || value.IsGlobal != timer.Value.IsGlobal || !string.Equals(value.TriggerId, timer.Value.TriggerId, StringComparison.Ordinal))
				{
					list.Add(timer.Key);
				}
			}
			foreach (string item in list)
			{
				if (_timers[item].Routine != null)
				{
					((MonoBehaviour)Plugin.Instance).StopCoroutine(_timers[item].Routine);
				}
				_timers.Remove(item);
			}
			foreach (KeyValuePair<string, Timer> item2 in dictionary)
			{
				if (!_timers.ContainsKey(item2.Key))
				{
					Timer value2 = item2.Value;
					value2.Routine = ((MonoBehaviour)Plugin.Instance).StartCoroutine(TimerRoutine(item2.Key, value2.TriggerId, value2.Interval, value2.IsGlobal));
					_timers[item2.Key] = value2;
					Plugin.Log.LogInfo((object)($"[timed] scheduled '{item2.Key}' every {value2.Interval}s " + string.Format("({0}; first fire in {1}s).", value2.IsGlobal ? "global" : "player", value2.Interval)));
				}
			}
		}

		private static void WarnIfTimedStep(GuidanceEntry entry)
		{
			if (entry?.Steps == null)
			{
				return;
			}
			for (int i = 0; i < entry.Steps.Count; i++)
			{
				TriggerSpec triggerSpec = entry.Steps[i]?.Trigger;
				if (triggerSpec != null && string.Equals(triggerSpec.Type, "timed", StringComparison.OrdinalIgnoreCase))
				{
					Plugin.Log.LogWarning((object)($"[timed] '{entry.Id}' step {i + 1} uses type: timed, which is " + "only supported on a top-level entry trigger. That step will never fire."));
				}
			}
		}

		internal static void StopAll()
		{
			if ((Object)(object)Plugin.Instance == (Object)null)
			{
				_timers.Clear();
				return;
			}
			foreach (Timer value in _timers.Values)
			{
				if (value?.Routine != null)
				{
					((MonoBehaviour)Plugin.Instance).StopCoroutine(value.Routine);
				}
			}
			_timers.Clear();
		}

		private static IEnumerator TimerRoutine(string entryId, string triggerId, float interval, bool isGlobal)
		{
			yield return (object)new WaitForSeconds(interval);
			while (true)
			{
				Plugin.Log.LogInfo((object)("[timed] '" + entryId + "' firing (subject '" + triggerId + "')."));
				if (isGlobal && Application.isBatchMode)
				{
					GuidanceSync.BroadcastTimedGuidance(entryId);
				}
				else
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "timed",
						Subject = triggerId
					});
				}
				yield return (object)new WaitForSeconds(interval);
			}
		}

		internal static float ParseInterval(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return 0f;
			}
			s = s.Trim();
			if (string.Equals(s, "daily", StringComparison.OrdinalIgnoreCase))
			{
				return 86400f;
			}
			if (string.Equals(s, "hourly", StringComparison.OrdinalIgnoreCase))
			{
				return 3600f;
			}
			float num = 1f;
			num = char.ToLowerInvariant(s[s.Length - 1]) switch
			{
				's' => 1f, 
				'm' => 60f, 
				'h' => 3600f, 
				'd' => 86400f, 
				_ => 0f, 
			};
			if (num > 0f)
			{
				s = s.Substring(0, s.Length - 1).Trim();
			}
			else
			{
				num = 1f;
			}
			if (!float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || !(result > 0f))
			{
				return 0f;
			}
			return result * num;
		}

		private static bool IsServerOrHost()
		{
			if (!((Object)(object)ZNet.instance == (Object)null))
			{
				return ZNet.instance.IsServer();
			}
			return true;
		}
	}
	internal static class TimeTrigger
	{
		private const float PollInterval = 30f;

		private static Coroutine _coroutine;

		public static void Start()
		{
			if (_coroutine == null)
			{
				_coroutine = ((MonoBehaviour)Plugin.Instance).StartCoroutine(PollRoutine());
			}
		}

		private static IEnumerator PollRoutine()
		{
			WaitForSeconds wait = new WaitForSeconds(30f);
			while (true)
			{
				yield return wait;
				Tick();
			}
		}

		private static void Tick()
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			DateTime utcNow = DateTime.UtcNow;
			EnvMan instance = EnvMan.instance;
			foreach (GuidanceEntry guidance in currentConfig.Guidances)
			{
				TriggerSpec trigger = guidance.Trigger;
				if (trigger == null)
				{
					continue;
				}
				bool flag;
				switch (trigger.Type?.ToLowerInvariant())
				{
				case "time_of_day":
					if ((Object)(object)instance == (Object)null)
					{
						continue;
					}
					flag = MatchesTimeOfDay(instance, trigger);
					break;
				case "day_number":
					if ((Object)(object)instance == (Object)null)
					{
						continue;
					}
					flag = instance.GetDay() == ParseDay(trigger.Day) && instance.GetDayFraction() >= 0.25f;
					break;
				case "real_world_time":
					flag = utcNow.Hour == trigger.UtcHour && utcNow.Minute == trigger.UtcMinute;
					break;
				case "day_of_week":
					flag = string.Equals(utcNow.DayOfWeek.ToString(), trigger.Day, StringComparison.OrdinalIgnoreCase);
					break;
				default:
					continue;
				}
				if (flag && GuidanceDispatcher.CheckGates(guidance, localPlayer))
				{
					Plugin.Log.LogInfo((object)("[time] '" + guidance.Id + "' condition matched (" + trigger.Type + ")."));
					GuidanceDispatcher.FireEntry(guidance, new TriggerEvent
					{
						Type = trigger.Type
					});
				}
			}
		}

		private static bool MatchesTimeOfDay(EnvMan envMan, TriggerSpec t)
		{
			float num = Mathf.Abs(envMan.GetDayFraction() - t.GameTimeFraction);
			num = Mathf.Min(num, 1f - num);
			return num <= t.Window;
		}

		private static int ParseDay(string s)
		{
			if (!int.TryParse(s, out var result))