Decompiled source of ValheimServerGuide v0.5.2

ValheimServerGuide.dll

Decompiled 10 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
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 Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;
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.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("ValheimServerGuide")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+b1a17eae6dd79343542054ca652eae4f00be5557")]
[assembly: AssemblyProduct("ValheimServerGuide")]
[assembly: AssemblyTitle("ValheimServerGuide")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ValheimServerGuide
{
	[BepInPlugin("com.valheimserverguide", "ValheimServerGuide", "0.5.2")]
	[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.5.2";

		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<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> 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; }

		private void Awake()
		{
			//IL_029c: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a6: 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.");
			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.");
			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).");
			_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();
			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.5.2 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);
				}
			}
		}

		private void Update()
		{
			GuidanceConfigLoader loader;
			lock (_loaderLock)
			{
				loader = _loader;
			}
			loader?.Tick();
			GuidanceDisplay.Tick();
		}

		private void OnDestroy()
		{
			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 static void Postfix(Player __instance)
		{
			//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_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Invalid comparison between Unknown and I4
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: 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)(ref currentBiome)).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);
			}
			Character val = (Character)obj;
			if (!((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 = "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 (!SeenTracker.HasFired(localPlayer, "chest_opened_fired"))
				{
					SeenTracker.MarkFired(localPlayer, "chest_opened_fired");
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "chest_opened",
						Subject = ""
					});
				}
			}
		}
	}
	[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;

		[HarmonyPostfix]
		private static void Postfix(Player __instance)
		{
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: 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_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_0103: 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 || Time.time < _nextCheck)
			{
				return;
			}
			_nextCheck = Time.time + 5f;
			GuidanceConfig currentConfig = Plugin.CurrentConfig;
			if (currentConfig?.Guidances == null)
			{
				return;
			}
			Vector3 position = ((Component)__instance).transform.position;
			foreach (KeyValuePair<Vector2i, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances)
			{
				LocationInstance value = locationInstance.Value;
				if (!value.m_placed)
				{
					continue;
				}
				string text = value.m_location?.m_prefabName;
				if (!string.IsNullOrEmpty(text))
				{
					string id = "dist_" + text;
					if (!SeenTracker.HasFired(__instance, id) && AnyEntryInRange(currentConfig, text, position, value.m_position))
					{
						SeenTracker.MarkFired(__instance, id);
						Plugin.Log.LogInfo((object)("[distance] entered range of '" + text + "'."));
						GuidanceDispatcher.Raise(new TriggerEvent
						{
							Type = "distance",
							Subject = text
						});
					}
				}
			}
		}

		private static bool AnyEntryInRange(GuidanceConfig config, string prefabName, Vector3 playerPos, Vector3 locPos)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: 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_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			if (t == null)
			{
				return false;
			}
			if (!string.Equals(t.Type, "distance", StringComparison.OrdinalIgnoreCase))
			{
				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 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) && !SeenTracker.HasFired(__instance, "first_login_fired"))
			{
				SeenTracker.MarkFired(__instance, "first_login_fired");
				GuidanceDispatcher.Raise(new TriggerEvent
				{
					Type = "first_login",
					Subject = ""
				});
			}
		}
	}
	public static class GuidanceDispatcher
	{
		public static void Raise(TriggerEvent evt)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				Plugin.Log.LogDebug((object)("[dispatch] " + evt.Type + "/" + evt.Subject + " ignored: no local player."));
				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 guidance in currentConfig.Guidances)
			{
				if (guidance.Steps != null && guidance.Steps.Count > 0)
				{
					if (HandleChain(guidance, evt, localPlayer, list))
					{
						num++;
					}
				}
				else
				{
					if (!Matches(guidance, evt))
					{
						continue;
					}
					if (string.Equals(evt.Type, "item_acquired", StringComparison.OrdinalIgnoreCase) && guidance.Trigger != null && ItemAcquiredTrigger.GetEffectiveGoals(guidance.Trigger) != null)
					{
						Plugin.Log.LogDebug((object)("[dispatch] '" + guidance.Id + "' item_acquired count-goal — delegated to count path."));
						continue;
					}
					if (!RequirementsMet(guidance, localPlayer))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: requires not met."));
						continue;
					}
					if (StopConditionMet(guidance, localPlayer))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: stop_when met."));
						continue;
					}
					if (guidance.Once && SeenTracker.HasFired(localPlayer, guidance.Id, guidance.Scope))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: already fired (once)."));
						continue;
					}
					if (!SeenTracker.CooldownReady(guidance.Id, guidance.Cooldown, Time.time))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: cooldown."));
						continue;
					}
					int num2 = guidance.Trigger?.MaxFires ?? 0;
					if (num2 > 0 && SeenTracker.GetFireCount(localPlayer, guidance.Id) >= num2)
					{
						Plugin.Log.LogInfo((object)$"[dispatch] '{guidance.Id}' skipped: max_fires ({num2}) reached.");
						continue;
					}
					if (SeenTracker.IsGlobalScope(guidance.Scope))
					{
						Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' (global) -> server."));
						GuidanceSync.SendTriggerGlobal(guidance.Id, localPlayer.GetPlayerName());
						SeenTracker.MarkCooldown(guidance.Id, guidance.Cooldown, Time.time);
						num++;
						continue;
					}
					Plugin.Log.LogInfo((object)("[dispatch] firing '" + guidance.Id + "' via mode '" + guidance.Display?.Mode + "'."));
					string template = ((!string.IsNullOrEmpty(guidance.Message)) ? guidance.Message : guidance.Display?.Text);
					string renderedText = TemplateText(template, evt, localPlayer.GetPlayerName());
					GuidanceDisplay.Show(guidance, renderedText);
					if (guidance.Once)
					{
						SeenTracker.MarkFired(localPlayer, guidance.Id, guidance.Scope);
					}
					if (num2 > 0)
					{
						SeenTracker.IncrementFireCount(localPlayer, guidance.Id);
					}
					SeenTracker.MarkCooldown(guidance.Id, guidance.Cooldown, Time.time);
					if (guidance.Announce?.Discord != null)
					{
						GuidanceSync.SendAnnounceRequest(guidance.Id, localPlayer.GetPlayerName());
					}
					if (guidance.DiscordOnComplete)
					{
						GuidanceSync.SendCompleteAnnounce(guidance.Id, localPlayer.GetPlayerName());
					}
					if (guidance.Rewards != null && guidance.Rewards.Count > 0)
					{
						RewardDispatcher.Grant(guidance.Rewards, localPlayer);
					}
					list.Add(guidance.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;
			}
			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;
				}
				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}).");
				GuidanceSync.SendChainStepUpdate(player.GetPlayerName(), entry.Id + ":" + stepIndex, num.ToString());
				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}.");
			GuidanceSync.SendChainStepUpdate(player.GetPlayerName(), entry.Id + ":" + stepIndex, num2.ToString());
			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 = TemplateText(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).");
				GuidanceSync.SendChainStepUpdate(player.GetPlayerName(), entry.Id, "done");
				GuidanceHudTracker.Instance?.FlashCompletion(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}.");
				GuidanceSync.SendChainStepUpdate(player.GetPlayerName(), entry.Id, num.ToString());
				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 = TemplateText(template, null, sourcePlayerName);
			GuidanceDisplay.Show(guidanceEntry, renderedText);
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entryId
			});
		}

		internal static bool CheckGates(GuidanceEntry entry, Player player)
		{
			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 = TemplateText(template, null, localPlayer.GetPlayerName());
			GuidanceDisplay.Show(guidanceEntry, renderedText);
			if (guidanceEntry.Once)
			{
				SeenTracker.MarkFired(localPlayer, guidanceEntry.Id, guidanceEntry.Scope);
			}
			int num = guidanceEntry.Trigger?.MaxFires ?? 0;
			if (num > 0)
			{
				SeenTracker.IncrementFireCount(localPlayer, guidanceEntry.Id);
			}
			SeenTracker.MarkCooldown(guidanceEntry.Id, guidanceEntry.Cooldown, Time.time);
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entryId
			});
		}

		internal static void FireEntry(GuidanceEntry entry, TriggerEvent evt)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || entry == null)
			{
				return;
			}
			if (SeenTracker.IsGlobalScope(entry.Scope))
			{
				Plugin.Log.LogInfo((object)("[dispatch] FireEntry '" + entry.Id + "' (global) -> server."));
				GuidanceSync.SendTriggerGlobal(entry.Id, localPlayer.GetPlayerName());
				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 = TemplateText(template, evt, localPlayer.GetPlayerName());
			GuidanceDisplay.Show(entry, renderedText);
			if (entry.Once)
			{
				SeenTracker.MarkFired(localPlayer, entry.Id, entry.Scope);
			}
			int num = entry.Trigger?.MaxFires ?? 0;
			if (num > 0)
			{
				SeenTracker.IncrementFireCount(localPlayer, entry.Id);
			}
			SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time);
			if (entry.Announce?.Discord != null)
			{
				GuidanceSync.SendAnnounceRequest(entry.Id, localPlayer.GetPlayerName());
			}
			if (entry.DiscordOnComplete)
			{
				GuidanceSync.SendCompleteAnnounce(entry.Id, localPlayer.GetPlayerName());
			}
			if (entry.Rewards != null && entry.Rewards.Count > 0)
			{
				RewardDispatcher.Grant(entry.Rewards, localPlayer);
			}
			Raise(new TriggerEvent
			{
				Type = "entry_finished",
				Subject = entry.Id
			});
		}

		private static bool Matches(GuidanceEntry entry, TriggerEvent evt)
		{
			if (entry.Trigger == null)
			{
				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 "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 "first_login":
			case "chest_opened":
			case "player_death":
				return true;
			default:
				return true;
			}
		}

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

		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);
			string s = subject.Substring(num + 1);
			if (!int.TryParse(s, out var result))
			{
				return false;
			}
			return string.Equals(t.Skill, b, StringComparison.OrdinalIgnoreCase) && t.Level == result;
		}

		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_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: 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)
			{
				Biome currentBiome = localPlayer.GetCurrentBiome();
				text = ((object)(Biome)(ref currentBiome)).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 text2 = template.Replace("{playerName}", playerName ?? "").Replace("{player_name}", playerName ?? "").Replace("{itemName}", evt?.DisplayName ?? evt?.Subject ?? "")
				.Replace("{creatureName}", evt?.DisplayName ?? evt?.Subject ?? "")
				.Replace("{biome}", (!string.IsNullOrEmpty(text)) ? text : (evt?.Subject ?? ""))
				.Replace("{skill}", newValue)
				.Replace("{level}", newValue2);
			if (step >= 0)
			{
				text2 = text2.Replace("{step}", step.ToString());
			}
			if (total >= 0)
			{
				text2 = text2.Replace("{total}", total.ToString());
			}
			return text2;
		}

		public static void CheckVersionUpdates(Player player, GuidanceConfig config)
		{
			if ((Object)(object)player == (Object)null || config?.Guidances == null)
			{
				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 = TemplateText(template, null, player.GetPlayerName());
					if ((Object)(object)MessageHud.instance != (Object)null)
					{
						MessageHud.instance.ShowMessage((MessageType)1, text, 0, (Sprite)null, false);
					}
					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
					});
				}
			}
		}
	}
	[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_0056: 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)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01da: Unknown result type (might be due to invalid IL or missing references)
			//IL_0201: Unknown result type (might be due to invalid IL or missing references)
			//IL_0202: Unknown result type (might be due to invalid IL or missing references)
			//IL_0204: Unknown result type (might be due to invalid IL or missing references)
			//IL_0224: 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 || 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<Vector2i, 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
						});
					}
				}
			}
		}
	}
	internal static class NpcConvHoldState
	{
		internal const float HoldThreshold = 0.5f;

		internal static float HoldStart = -1f;

		internal static Trader PendingTrader = null;
	}
	[HarmonyPatch(typeof(Trader), "Interact")]
	internal static class NpcConversationTrigger
	{
		[HarmonyPrefix]
		private static bool Prefix(Trader __instance, Humanoid character, bool hold, ref bool __result)
		{
			NpcConversationHoldDetector.EnsureCreated();
			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;
			}
			GameObject gameObject = ((Component)__instance).gameObject;
			string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
			GuidanceEntry guidanceEntry = FindEntry(npcSubject, val);
			if (guidanceEntry == null)
			{
				return true;
			}
			if (!hold)
			{
				NpcConvHoldState.HoldStart = Time.time;
				NpcConvHoldState.PendingTrader = __instance;
				__result = true;
				return false;
			}
			__result = false;
			return false;
		}

		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))
				{
					continue;
				}
				return guidance;
			}
			return null;
		}
	}
	[HarmonyPatch(typeof(Trader), "GetHoverText")]
	internal static class TraderHoverTextPatch
	{
		[HarmonyPostfix]
		private static void Postfix(Trader __instance, ref string __result)
		{
			Player localPlayer = Player.m_localPlayer;
			if (!((Object)(object)localPlayer == (Object)null))
			{
				GameObject gameObject = ((Component)__instance).gameObject;
				string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (NpcConversationTrigger.FindEntry(npcSubject, localPlayer) != null)
				{
					__result += "\n[Hold E] Quest";
				}
			}
		}
	}
	internal class NpcConversationHoldDetector : MonoBehaviour
	{
		private static NpcConversationHoldDetector _instance;

		internal static void EnsureCreated()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if (!((Object)(object)_instance != (Object)null))
			{
				GameObject val = new GameObject("VSG_NpcConvHold");
				Object.DontDestroyOnLoad((Object)(object)val);
				_instance = val.AddComponent<NpcConversationHoldDetector>();
			}
		}

		private void Update()
		{
			if ((Object)(object)NpcConvHoldState.PendingTrader == (Object)null)
			{
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				Reset();
			}
			else if (!ZInput.GetButton("Use"))
			{
				Trader pendingTrader = NpcConvHoldState.PendingTrader;
				Reset();
				if ((Object)(object)StoreGui.instance != (Object)null)
				{
					StoreGui.instance.Show(pendingTrader);
				}
			}
			else
			{
				if (!(Time.time - NpcConvHoldState.HoldStart >= 0.5f))
				{
					return;
				}
				Trader pendingTrader2 = NpcConvHoldState.PendingTrader;
				Reset();
				GameObject gameObject = ((Component)pendingTrader2).gameObject;
				string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				GuidanceEntry guidanceEntry = NpcConversationTrigger.FindEntry(npcSubject, localPlayer);
				if (guidanceEntry == null)
				{
					if ((Object)(object)StoreGui.instance != (Object)null)
					{
						StoreGui.instance.Show(pendingTrader2);
					}
				}
				else
				{
					string template = ((!string.IsNullOrEmpty(guidanceEntry.Message)) ? guidanceEntry.Message : guidanceEntry.Display?.Text);
					string renderedText = GuidanceDispatcher.TemplateText(template, null, localPlayer.GetPlayerName());
					GuidanceDisplay.Show(guidanceEntry, renderedText);
				}
			}
		}

		private static void Reset()
		{
			NpcConvHoldState.HoldStart = -1f;
			NpcConvHoldState.PendingTrader = 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);
			((Character)player).Message((MessageType)2, $"{text2}: {num5}/{num} {text}", 0, (Sprite)null);
			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 ?? "";
			return (Localization.instance != null) ? Localization.instance.Localize(text) : 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))
				{
					continue;
				}
				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;
				string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null);
				if (NpcItemSubmitTrigger.NpcHasConfiguredEntries(npcSubject))
				{
					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(Skills), "RaiseSkill")]
	internal static class SkillLevelTrigger
	{
		private static int _prevLevel;

		[HarmonyPrefix]
		private static void Prefix(Skills __instance, SkillType skillType)
		{
			//IL_0029: 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_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: 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_0155: 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) ? num3 : a.level.CompareTo(b.level);
			});
			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));
			}
		}
	}
	internal static class TimedTrigger
	{
		[CompilerGenerated]
		private sealed class <TimerRoutine>d__3 : IEnumerator<object>, IDisposable, IEnumerator
		{
			private int <>1__state;

			private object <>2__current;

			public string entryId;

			public string triggerId;

			public float interval;

			public bool isGlobal;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <TimerRoutine>d__3(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_0035: Unknown result type (might be due to invalid IL or missing references)
				//IL_003f: Expected O, but got Unknown
				//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
				//IL_00ce: Expected O, but got Unknown
				switch (<>1__state)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					<>2__current = (object)new WaitForSeconds(interval);
					<>1__state = 1;
					return true;
				case 1:
					<>1__state = -1;
					break;
				case 2:
					<>1__state = -1;
					break;
				}
				Plugin.Log.LogInfo((object)("[timed] '" + entryId + "' firing."));
				if (isGlobal && Application.isBatchMode)
				{
					GuidanceSync.BroadcastTimedGuidance(entryId);
				}
				else
				{
					GuidanceDispatcher.Raise(new TriggerEvent
					{
						Type = "timed",
						Subject = triggerId
					});
				}
				<>2__current = (object)new WaitForSeconds(interval);
				<>1__state = 2;
				return true;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private static readonly Dictionary<string, Coroutine> _coroutines = new Dictionary<string, Coroutine>();

		public static void OnConfigChanged(GuidanceConfig config)
		{
			StopAll();
			if (config?.Guidances == null)
			{
				return;
			}
			bool flag = Application.isBatchMode && IsServerOrHost();
			bool flag2 = !IsServerOrHost();
			foreach (GuidanceEntry guidance in config.Guidances)
			{
				if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "timed", StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}
				float num = ParseInterval(guidance.Trigger.Interval);
				if (num <= 0f)
				{
					Plugin.Log.LogWarning((object)("[timed] '" + guidance.Id + "' has invalid interval '" + guidance.Trigger.Interval + "'; skipping."));
					continue;
				}
				bool flag3 = SeenTracker.IsGlobalScope(guidance.Scope);
				if ((!flag || flag3) && !(flag2 && flag3))
				{
					string id = guidance.Id;
					string triggerId = guidance.Trigger.Id ?? guidance.Id;
					Coroutine value = ((MonoBehaviour)Plugin.Instance).StartCoroutine(TimerRoutine(id, triggerId, num, flag3));
					_coroutines[id] = value;
					Plugin.Log.LogInfo((object)string.Format("[timed] scheduled '{0}' every {1}s ({2}).", id, num, flag3 ? "global" : "player"));
				}
			}
		}

		private static void StopAll()
		{
			if ((Object)(object)Plugin.Instance == (Object)null)
			{
				return;
			}
			foreach (Coroutine value in _coroutines.Values)
			{
				if (value != null)
				{
					((MonoBehaviour)Plugin.Instance).StopCoroutine(value);
				}
			}
			_coroutines.Clear();
		}

		[IteratorStateMachine(typeof(<TimerRoutine>d__3))]
		private static IEnumerator TimerRoutine(string entryId, string triggerId, float interval, bool isGlobal)
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <TimerRoutine>d__3(0)
			{
				entryId = entryId,
				triggerId = triggerId,
				interval = interval,
				isGlobal = isGlobal
			};
		}

		private static float ParseInterval(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return 0f;
			}
			if (string.Equals(s, "daily", StringComparison.OrdinalIgnoreCase))
			{
				return 86400f;
			}
			if (string.Equals(s, "hourly", StringComparison.OrdinalIgnoreCase))
			{
				return 3600f;
			}
			float result;
			return float.TryParse(s, out result) ? result : 0f;
		}

		private static bool IsServerOrHost()
		{
			return (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer();
		}
	}
	internal static class TriggerUtils
	{
		private const string CloneSuffix = "(Clone)";

		public static string NormalizePrefabName(string raw)
		{
			if (string.IsNullOrEmpty(raw))
			{
				return raw;
			}
			return raw.EndsWith("(Clone)") ? raw.Substring(0, raw.Length - "(Clone)".Length) : raw;
		}
	}
}
namespace ValheimServerGuide.State
{
	public static class ChainState
	{
		private const string StepPrefix = "VSG.cp.";

		private const string DonePrefix = "VSG.cd.";

		private const string CounterPrefix = "VSG.cc.";

		private const string VersionPrefix = "VSG.cv.";

		public static int GetStep(Player player, string chainId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(chainId))
			{
				return 0;
			}
			if (!player.m_customData.TryGetValue("VSG.cp." + chainId, out var value))
			{
				return 0;
			}
			int result;
			return int.TryParse(value, out result) ? result : 0;
		}

		public static void SetStep(Player player, string chainId, int step)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(chainId))
			{
				player.m_customData["VSG.cp." + chainId] = step.ToString();
			}
		}

		public static bool IsComplete(Player player, string chainId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(chainId))
			{
				return false;
			}
			string value;
			return player.m_customData.TryGetValue("VSG.cd." + chainId, out value) && value == "1";
		}

		public static void MarkComplete(Player player, string chainId)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(chainId))
			{
				player.m_customData["VSG.cd." + chainId] = "1";
				player.m_customData.Remove("VSG.cp." + chainId);
			}
		}

		public static void Reset(Player player, string chainId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(chainId))
			{
				return;
			}
			player.m_customData.Remove("VSG.cp." + chainId);
			player.m_customData.Remove("VSG.cd." + chainId);
			player.m_customData.Remove("VSG.cv." + chainId);
			string value = "VSG.cc." + chainId + ":";
			List<string> list = new List<string>();
			foreach (string key in player.m_customData.Keys)
			{
				if (key.StartsWith(value))
				{
					list.Add(key);
				}
			}
			foreach (string item in list)
			{
				player.m_customData.Remove(item);
			}
		}

		public static void ResetAll(Player player)
		{
			if (player?.m_customData == null)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (string key in player.m_customData.Keys)
			{
				if (key.StartsWith("VSG.cp.") || key.StartsWith("VSG.cd.") || key.StartsWith("VSG.cc.") || key.StartsWith("VSG.cv."))
				{
					list.Add(key);
				}
			}
			foreach (string item in list)
			{
				player.m_customData.Remove(item);
			}
		}

		public static int GetCompletedVersion(Player player, string chainId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(chainId))
			{
				return 0;
			}
			if (!player.m_customData.TryGetValue("VSG.cv." + chainId, out var value))
			{
				return 0;
			}
			int result;
			return int.TryParse(value, out result) ? result : 0;
		}

		public static void SetCompletedVersion(Player player, string chainId, int version)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(chainId))
			{
				player.m_customData["VSG.cv." + chainId] = version.ToString();
			}
		}

		private static string CounterKey(string chainId, int stepIndex)
		{
			return "VSG.cc." + chainId + ":" + stepIndex;
		}

		public static int GetCounter(Player player, string chainId, int stepIndex)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(chainId))
			{
				return -1;
			}
			if (!player.m_customData.TryGetValue(CounterKey(chainId, stepIndex), out var value))
			{
				return -1;
			}
			int result;
			return int.TryParse(value, out result) ? result : (-1);
		}

		public static void SetCounter(Player player, string chainId, int stepIndex, int value)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(chainId))
			{
				player.m_customData[CounterKey(chainId, stepIndex)] = value.ToString();
			}
		}

		public static void ClearCounter(Player player, string chainId, int stepIndex)
		{
			player?.m_customData?.Remove(CounterKey(chainId, stepIndex));
		}
	}
	public static class GoalStartedState
	{
		private const string StartedPrefix = "VSG.ig.";

		private static string Key(string entryId)
		{
			return "VSG.ig." + entryId;
		}

		public static bool IsStarted(Player player, string entryId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(entryId))
			{
				return false;
			}
			return player.m_customData.ContainsKey(Key(entryId));
		}

		public static void MarkStarted(Player player, string entryId)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(entryId))
			{
				player.m_customData[Key(entryId)] = "1";
			}
		}

		public static void Clear(Player player, string entryId)
		{
			player?.m_customData?.Remove(Key(entryId));
		}

		public static void ResetAll(Player player)
		{
			if (player?.m_customData == null)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (string key in player.m_customData.Keys)
			{
				if (key.StartsWith("VSG.ig."))
				{
					list.Add(key);
				}
			}
			foreach (string item in list)
			{
				player.m_customData.Remove(item);
			}
		}
	}
	public static class PrerequisiteChecker
	{
		public static bool AllSatisfied(List<string> requires, Player player, GuidanceConfig config)
		{
			if (requires == null || requires.Count == 0)
			{
				return true;
			}
			foreach (string require in requires)
			{
				if (!IsSatisfied(require, player, config))
				{
					return false;
				}
			}
			return true;
		}

		private static bool IsSatisfied(string reqId, Player player, GuidanceConfig config)
		{
			if (ChainState.IsComplete(player, reqId))
			{
				return true;
			}
			if (SeenTracker.HasFired(player, reqId, "player"))
			{
				return true;
			}
			if (!(config?.Guidances?.Exists((GuidanceEntry e) => e.Id == reqId)).GetValueOrDefault())
			{
				Plugin.Log.LogWarning((object)("[prereq] '" + reqId + "' not found in config — treating as unsatisfied."));
			}
			return false;
		}
	}
	public static class SeenTracker
	{
		private const string Key = "VSG.fired";

		private const string FireCountPrefix = "VSG.fc.";

		public const string GlobalKeyPrefix = "VSG.";

		private static readonly Dictionary<string, float> CooldownExpiry = new Dictionary<string, float>();

		public static string GlobalKeyFor(string id)
		{
			return "VSG." + id;
		}

		public static bool IsGlobalScope(string scope)
		{
			return string.Equals(scope, "global", StringComparison.OrdinalIgnoreCase);
		}

		public static bool HasFired(Player player, string id, string scope)
		{
			if (string.IsNullOrEmpty(id))
			{
				return false;
			}
			if (IsGlobalScope(scope))
			{
				return (Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.GetGlobalKey(GlobalKeyFor(id));
			}
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			return GetSet(player).Contains(id);
		}

		public static bool HasFired(Player player, string id)
		{
			return HasFired(player, id, "player");
		}

		public static void MarkFired(Player player, string id, string scope)
		{
			if (string.IsNullOrEmpty(id))
			{
				return;
			}
			if (IsGlobalScope(scope))
			{
				if ((Object)(object)ZoneSystem.instance != (Object)null && ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()))
				{
					ZoneSystem.instance.SetGlobalKey(GlobalKeyFor(id));
				}
			}
			else if (!((Object)(object)player == (Object)null))
			{
				HashSet<string> set = GetSet(player);
				if (set.Add(id))
				{
					player.m_customData["VSG.fired"] = string.Join(",", set);
				}
			}
		}

		public static void MarkFired(Player player, string id)
		{
			MarkFired(player, id, "player");
		}

		public static bool CooldownReady(string id, float cooldownSeconds, float now)
		{
			if (cooldownSeconds <= 0f)
			{
				return true;
			}
			if (!CooldownExpiry.TryGetValue(id, out var value))
			{
				return true;
			}
			return now >= value;
		}

		public static void MarkCooldown(string id, float cooldownSeconds, float now)
		{
			if (!(cooldownSeconds <= 0f))
			{
				CooldownExpiry[id] = now + cooldownSeconds;
			}
		}

		public static bool ClearFired(Player player, string id, string scope = "player")
		{
			if (string.IsNullOrEmpty(id))
			{
				return false;
			}
			if (IsGlobalScope(scope))
			{
				if ((Object)(object)ZoneSystem.instance == (Object)null)
				{
					return false;
				}
				if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
				{
					return false;
				}
				string text = GlobalKeyFor(id);
				if (!ZoneSystem.instance.GetGlobalKey(text))
				{
					return false;
				}
				ZoneSystem.instance.RemoveGlobalKey(text);
				CooldownExpiry.Remove(id);
				return true;
			}
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			bool result = ClearFireCount(player, id);
			CooldownExpiry.Remove(id);
			HashSet<string> set = GetSet(player);
			if (!set.Remove(id))
			{
				return result;
			}
			if (set.Count == 0)
			{
				player.m_customData.Remove("VSG.fired");
			}
			else
			{
				player.m_customData["VSG.fired"] = string.Join(",", set);
			}
			return true;
		}

		public static bool ClearFireCount(Player player, string id)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(id))
			{
				return false;
			}
			return player.m_customData.Remove("VSG.fc." + id);
		}

		public static int ClearAllFired(Player player)
		{
			if ((Object)(object)player == (Object)null)
			{
				return 0;
			}
			int count = GetSet(player).Count;
			player.m_customData.Remove("VSG.fired");
			List<string> list = player.m_customData.Keys.Where((string k) => k.StartsWith("VSG.fc.")).ToList();
			foreach (string item in list)
			{
				player.m_customData.Remove(item);
			}
			CooldownExpiry.Clear();
			return count;
		}

		public static int GetFireCount(Player player, string id)
		{
			if (player?.m_customData == null)
			{
				return 0;
			}
			string key = "VSG.fc." + id;
			if (!player.m_customData.TryGetValue(key, out var value))
			{
				return 0;
			}
			int result;
			return int.TryParse(value, out result) ? result : 0;
		}

		public static void IncrementFireCount(Player player, string id)
		{
			if (player?.m_customData != null)
			{
				string key = "VSG.fc." + id;
				player.m_customData[key] = (GetFireCount(player, id) + 1).ToString();
			}
		}

		public static IReadOnlyCollection<string> GetFiredIds(Player player)
		{
			if ((Object)(object)player == (Object)null)
			{
				return (IReadOnlyCollection<string>)(object)Array.Empty<string>();
			}
			return GetSet(player);
		}

		private static HashSet<string> GetSet(Player player)
		{
			if (!player.m_customData.TryGetValue("VSG.fired", out var value) || string.IsNullOrEmpty(value))
			{
				return new HashSet<string>();
			}
			return new HashSet<string>(value.Split(new char[1] { ',' }));
		}
	}
	public static class SubmitState
	{
		private const string ProgressPrefix = "VSG.is.";

		private static string Key(string entryId)
		{
			return "VSG.is." + entryId;
		}

		public static int Get(Player player, string entryId)
		{
			if (player?.m_customData == null || string.IsNullOrEmpty(entryId))
			{
				return 0;
			}
			if (!player.m_customData.TryGetValue(Key(entryId), out var value))
			{
				return 0;
			}
			int result;
			return int.TryParse(value, out result) ? result : 0;
		}

		public static void Set(Player player, string entryId, int value)
		{
			if (player?.m_customData != null && !string.IsNullOrEmpty(entryId))
			{
				player.m_customData[Key(entryId)] = value.ToString();
			}
		}

		public static void Clear(Player player, string entryId)
		{
			player?.m_customData?.Remove(Key(entryId));
		}

		public static void ResetAll(Player player)
		{
			if (player?.m_customData == null)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (string key in player.m_customData.Keys)
			{
				if (key.StartsWith("VSG.is."))
				{
					list.Add(key);
				}
			}
			foreach (string item in list)
			{
				player.m_customData.Remove(item);
			}
		}
	}
}
namespace ValheimServerGuide.Rewards
{
	public static class RewardDispatcher
	{
		public static void Grant(List<RewardSpec> rewards, Player player)
		{
			if (rewards == null || rewards.Count == 0 || (Object)(object)player == (Object)null)
			{
				return;
			}
			foreach (RewardSpec reward in rewards)
			{
				switch (reward.Type?.ToLowerInvariant())
				{
				case "item":
					GrantItem(reward, player);
					break;
				case "skill_exp":
					GrantSkillExp(reward, player);
					break;
				case "skill_level":
					GrantSkillLevel(reward, player);
					break;
				case "buff":
					GrantBuff(reward, player);
					break;
				default:
					Plugin.Log.LogWarning((object)("[rewards] Unknown reward type '" + reward.Type + "' — skipping."));
					break;
				}
			}
			RewardNotification.Show(rewards);
		}

		public static void ValidateRewards(List<RewardSpec> rewards, string context)
		{
			if (rewards == null)
			{
				return;
			}
			foreach (RewardSpec reward in rewards)
			{
				switch (reward.Type?.ToLowerInvariant())
				{
				case "item":
					if (!string.IsNullOrEmpty(reward.Item) && (Object)(object)ZNetScene.instance != (Object)null)
					{
						GameObject prefab = ZNetScene.instance.GetPrefab(reward.Item);
						if ((Object)(object)prefab == (Object)null)
						{
							Plugin.Log.LogWarning((object)("[rewards] " + context + ": item prefab '" + reward.Item + "' not found."));
						}
						else if ((Object)(object)prefab.GetComponent<ItemDrop>() == (Object)null)
						{
							Plugin.Log.LogWarning((object)("[rewards] " + context + ": prefab '" + reward.Item + "' has no ItemDrop."));
						}
					}
					break;
				case "skill_exp":
				case "skill_level":
				{
					if (!string.IsNullOrEmpty(reward.Skill) && !Enum.TryParse<SkillType>(reward.Skill, ignoreCase: true, out SkillType _))
					{
						Plugin.Log.LogWarning((object)("[rewards] " + context + ": unknown skill '" + reward.Skill + "'."));
					}
					break;
				}
				case "buff":
				{
					if (string.IsNullOrEmpty(reward.Effect) || ObjectDB.instance?.m_StatusEffects == null)
					{
						break;
					}
					string text = NormalizeEffectName(reward.Effect);
					bool flag = false;
					foreach (StatusEffect statusEffect in ObjectDB.instance.m_StatusEffects)
					{
						if ((Object)(object)statusEffect == (Object)null || (!(NormalizeEffectName(((Object)statusEffect).name) == text) && !(NormalizeEffectName(statusEffect.m_name) == text)))
						{
							continue;
						}
						flag = true;
						break;
					}
					if (!flag)
					{
						Plugin.Log.LogWarning((object)("[rewards] " + context + ": status effect '" + reward.Effect + "' not found in ObjectDB."));
					}
					break;
				}
				}
			}
		}

		private static void GrantItem(RewardSpec reward, Player player)
		{
			//IL_0101: 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_0116: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_0123: 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)
			if (string.IsNullOrEmpty(reward.Item))
			{
				Plugin.Log.LogWarning((object)"[rewards] item reward missing 'item' field — skipping.");
				return;
			}
			ZNetScene instance = ZNetScene.instance;
			GameObject val = ((instance != null) ? instance.GetPrefab(reward.Item) : null);
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[rewards] item prefab '" + reward.Item + "' not found — skipping."));
				return;
			}
			ItemDrop component = val.GetComponent<ItemDrop>();
			if ((Object)(object)component == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[rewards] prefab '" + reward.Item + "' has no ItemDrop — skipping."));
				return;
			}
			int num = Mathf.Clamp(reward.Quality, 1, component.m_itemData.m_shared.m_maxQuality);
			ItemData val2 = ((Humanoid)player).GetInventory().AddItem(reward.Item, reward.Amount, num, 0, 0L, player.GetPlayerName(), false);
			if (val2 == null)
			{
				Vector3 val3 = ((Component)player).transform.position + ((Component)player).transform.forward * 1.5f;
				GameObject val4 = Object.Instantiate<GameObject>(val, val3, Quaternion.identity);
				ItemDrop component2 = val4.GetComponent<ItemDrop>();
				if ((Object)(object)component2 != (Object)null)
				{
					component2.m_itemData.m_stack = reward.Amount;
					component2.m_itemData.m_quality = num;
				}
				Plugin.Log.LogInfo((object)$"[rewards] Inventory full — dropped '{reward.Item}' x{reward.Amount} Q{num} in front of player.");
			}
			else
			{
				Plugin.Log.LogInfo((object)$"[rewards] Granted '{reward.Item}' x{reward.Amount} Q{num}.");
			}
		}

		private static void GrantSkillExp(RewardSpec reward, Player player)
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			if (TryParseSkill(reward.Skill, out var skillType))
			{
				float num = ((reward.SkillExp > 0f) ? reward.SkillExp : ((float)reward.Amount));
				((Character)player).GetSkills().RaiseSkill(skillType, num);
				Plugin.Log.LogInfo((object)$"[rewards] Raised {reward.Skill} by {num} XP.");
			}
		}

		private static void GrantSkillLevel(RewardSpec reward, Player player)
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			if (!TryParseSkill(reward.Skill, out var skillType))
			{
				return;
			}
			Skill skill = ((Character)player).GetSkills().GetSkill(skillType);
			if (skill == null)
			{
				Plugin.Log.LogWarning((object)("[rewards] could not resolve skill '" + reward.Skill + "' — skipping."));
				return;
			}
			int num = Mathf.Clamp(reward.Level, 1, 100);
			if ((float)num <= skill.m_level)
			{
				Plugin.Log.LogInfo((object)$"[rewards] {reward.Skill} already at {skill.m_level} >= target {num}; skipping.");
				return;
			}
			skill.m_level = num;
			skill.m_accumulator = 0f;
			Plugin.Log.LogInfo((object)$"[rewards] Set {reward.Skill} to level {num}.");
		}

		private static void GrantBuff(RewardSpec reward, Player player)
		{
			if (string.IsNullOrEmpty(reward.Effect))
			{
				Plugin.Log.LogWarning((object)"[rewards] buff reward missing 'effect' field — skipping.");
				return;
			}
			ObjectDB instance = ObjectDB.instance;
			StatusEffect val = null;
			string text = NormalizeEffectName(reward.Effect);
			if (instance?.m_StatusEffects != null)
			{
				foreach (StatusEffect statusEffect in instance.m_StatusEffects)
				{
					if ((Object)(object)statusEffect == (Object)null || (!(NormalizeEffectName(((Object)statusEffect).name) == text) && !(NormalizeEffectName(statusEffect.m_name) == text)))
					{
						continue;
					}
					val = statusEffect;
					break;
				}
			}
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[rewards] status effect '" + reward.Effect + "' not found in ObjectDB — skipping."));
				return;
			}
			StatusEffect val2 = ((Character)player).GetSEMan().AddStatusEffect(val, true, 0, 0f);
			if (reward.DurationOverride.HasValue && (Object)(object)val2 != (Object)null)
			{
				val2.m_ttl = reward.DurationOverride.Value;
			}
			Plugin.Log.LogInfo((object)("[rewards] Applied buff '" + reward.Effect + "'" + (reward.DurationOverride.HasValue ? $" ({reward.DurationOverride.Value}s)" : "") + "."));
		}

		internal static string NormalizeEffectName(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			s = s.ToLowerInvariant();
			if (s.StartsWith("$"))
			{
				s = s.Substring(1);
			}
			if (s.StartsWith("se_"))
			{
				s = s.Substring(3);
			}
			return s;
		}

		private static bool TryParseSkill(string skill, out SkillType skillType)
		{
			if (string.IsNullOrEmpty(skill))
			{
				Plugin.Log.LogWarning((object)"[rewards] skill reward missing 'skill' field — skipping.");
				skillType = (SkillType)0;
				return false;
			}
			if (!Enum.TryParse<SkillType>(skill, ignoreCase: true, out skillType))
			{
				Plugin.Log.LogWarning((object)("[rewards] Unknown skill '" + skill + "' — skipping."));
				return false;
			}
			return true;
		}
	}
	public static class RewardNotification
	{
		public static void Show(List<RewardSpec> rewards)
		{
			if (rewards == null || rewards.Count == 0 || (Object)(object)MessageHud.instance == (Object)null)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (RewardSpec reward in rewards)
			{
				string text = Describe(reward);
				if (!string.IsNullOrEmpty(text))
				{
					list.Add(text);
				}
			}
			if (list.Count != 0)
			{
				string text2 = "Received: " + string.Join(", ", list);
				MessageHud.instance.ShowMessage((MessageType)2, text2, 0, (Sprite)null, false);
			}
		}

		private static string Describe(RewardSpec reward)
		{
			return reward.Type?.ToLowerInvariant() switch
			{
				"item" => DescribeItem(reward), 
				"skill_exp" => DescribeSkillExp(reward), 
				"skill_level" => DescribeSkillLevel(reward), 
				"buff" => DescribeBuff(reward), 
				_ => null, 
			};
		}

		private static string DescribeItem(RewardSpec reward)
		{
			if (string.IsNullOrEmpty(reward.Item))
			{
				return null;
			}
			string value = LocalizeItemName(reward.Item);
			StringBuilder stringBuilder = new StringBuilder(value);
			if (reward.Amount > 1)
			{
				stringBuilder.Append(" x").Append(reward.Amount);
			}
			if (reward.Quality > 1)
			{
				stringBuilder.Append(" (Q").Append(reward.Quality).Append(")");
			}
			return stringBuilder.ToString();
		}

		private static string DescribeSkillExp(RewardSpec reward)
		{
			if (string.IsNullOrEmpty(reward.Skill))
			{
				return null;
			}
			float num = ((reward.SkillExp > 0f) ? reward.SkillExp : ((float)reward.Amount));
			return $"+{num:0.#} {reward.Skill} XP";
		}

		private static string DescribeSkillLevel(RewardSpec reward)
		{
			if (string.IsNullOrEmpty(reward.Skill))
			{
				return null;
			}
			return $"{reward.Skill} level {reward.Level}";
		}

		private static string DescribeBuff(RewardSpec reward)
		{
			if (string.IsNullOrEmpty(reward.Effect))
			{
				return null;
			}
			string text = LocalizeBuffName(reward.Effect);
			if (reward.DurationOverride.HasValue)
			{
				float value = reward.DurationOverride.Value;
				string text2 = ((value >= 60f) ? $"{value / 60f:0.#} min" : $"{value:0}s");
				return text + " buff (" + text2 + ")";
			}
			return text + " buff";
		}

		private static string LocalizeItemName(string prefabName)
		{
			ZNetScene instance = ZNetScene.instance;
			GameObject val = ((instance != null) ? instance.GetPrefab(prefabName) : null);
			string text = (((Object)(object)val != (Object)null) ? val.GetComponent<ItemDrop>() : null)?.m_itemData?.m_shared?.m_name;
			if (!string.IsNullOrEmpty(text) && Localization.instance != null)
			{
				return Localization.instance.Localize(text);
			}
			return prefabName;
		}

		private static string LocalizeBuffName(string effect)
		{
			ObjectDB instance = ObjectDB.instance;
			if (instance?.m_StatusEffects != null)
			{
				string text = RewardDispatcher.NormalizeEffectName(effect);
				foreach (StatusEffect statusEffect in instance.m_StatusEffects)
				{
					if ((Object)(object)statusEffect == (Object)null || (!(RewardDispatcher.NormalizeEffectName(((Object)statusEffect).name) == text) && !(RewardDispatcher.NormalizeEffectName(statusEffect.m_name) == text)))
					{
						continue;
					}
					if (!string.IsNullOrEmpty(statusEffect.m_name) && Localization.instance != null)
					{
						return Localization.instance.Localize(statusEffect.m_name);
					}
					break;
				}
			}
			return effect;
		}
	}
}
namespace ValheimServerGuide.Net
{
	public static class GuidanceSync
	{
		[HarmonyPatch(typeof(ZNet), "Awake")]
		private static class ZNetAwakePatch
		{
			private static void Postfix(ZNet __instance)
			{
				EnsureRegistered();
				if (__instance.IsServer())
				{
					Plugin.Log.LogInfo((object)"ZNet started as server/host — loading guidance YAML.");
					Plugin.EnsureLoaderStarted();
				}
				else
				{
					Plugin.Log.LogInfo((object)"ZNet started as pure client — waiting for server config push.");
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "OnDestroy")]
		private static class ZNetOnDestroyPatch
		{
			private static void Postfix()
			{
				_rpcsBound = false;
				_playerChainData.Clear();
				if (!Application.isBatchMode)
				{
					Plugin.ShutdownLoader();
					Plugin.CurrentConfig = GuidanceConfig.Empty;
				}
			}
		}

		[HarmonyPatch(typeof(Player), "OnSpawned")]
		private static class PlayerSpawnedPatch
		{
			private static void Postfix(Player __instance)
			{
				if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
				{
					RequestChainState(__instance.GetPlayerName());
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		private static class PeerInfoPatch
		{
			private static void Postfix(ZNet __instance, ZRpc rpc)
			{
				if (__instance.IsServer())
				{
					ZNetPeer peer = __instance.GetPeer(rpc);
					if (peer != null)
					{
						EnsureRegistered();
						SendToPeer(peer.m_uid, Plugin.CurrentConfig);
					}
				}
			}
		}

		[CompilerGenerated]
		private seal