Decompiled source of ProgressionGater v0.1.1

ProgressionGater.dll

Decompiled 8 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("Catosaur")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Admin-controlled server-wide boss and crafting progression for Valheim.")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+ff36e656ddd2abc4421a653bfe3fc76f2ce6e498")]
[assembly: AssemblyProduct("ProgressionGater")]
[assembly: AssemblyTitle("ProgressionGater")]
[assembly: AssemblyVersion("0.1.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ProgressionGater
{
	[HarmonyPatch]
	internal static class AdminCommands
	{
		private const string CommandRpc = "ProgressionGater.AdminCommand.v1";

		private const string ReplyRpc = "ProgressionGater.AdminReply.v1";

		private static bool _registered;

		private static Terminal _pendingTerminal;

		[HarmonyPatch(typeof(Terminal), "InitTerminal")]
		[HarmonyPostfix]
		private static void RegisterCommands()
		{
			if (!_registered)
			{
				_registered = true;
				Add("pg_unlock", "<next|boss> - allow a boss to be summoned", "unlock");
				Add("pg_lock", "<boss> - prevent a boss from being summoned", "lock");
				Add("pg_status", "show server-wide progression", "status");
				Add("pg_defeat", "<boss> - administratively mark a boss defeated", "defeat");
				Add("pg_undefeat", "<boss> - remove a recorded defeat", "undefeat");
				Add("pg_reset", "confirm - clear all Progression Gater state", "reset");
				Add("pg_webhook_test", "send a test Discord webhook", "webhook_test");
			}
		}

		private static void Add(string name, string description, string command)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Expected O, but got Unknown
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			new ConsoleCommand(name, description, (ConsoleEvent)delegate(ConsoleEventArgs args)
			{
				Send(args, command);
			}, true, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
		}

		internal static void RegisterRpcHandlers(ZRoutedRpc rpc)
		{
			rpc.Register<ZPackage>("ProgressionGater.AdminCommand.v1", (Action<long, ZPackage>)HandleServerCommand);
			rpc.Register<ZPackage>("ProgressionGater.AdminReply.v1", (Action<long, ZPackage>)delegate(long sender, ZPackage package)
			{
				if (NetworkManager.IsFromServer(sender))
				{
					HandleClientReply(package);
				}
			});
		}

		private static void Send(ConsoleEventArgs args, string command)
		{
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Expected O, but got Unknown
			string[] array = args.Args.Skip(1).ToArray();
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				args.Context.AddString("Not connected.");
				return;
			}
			if (ZNet.instance.IsServer())
			{
				Player localPlayer = Player.m_localPlayer;
				string actor = ((localPlayer != null) ? localPlayer.GetPlayerName() : null) ?? "Server console";
				{
					foreach (string item in Run(command, array, isAdmin: true, actor))
					{
						args.Context.AddString(item);
					}
					return;
				}
			}
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (serverPeer == null || ZRoutedRpc.instance == null)
			{
				args.Context.AddString("Server connection is not ready.");
				return;
			}
			ZPackage val = new ZPackage();
			val.Write(command);
			val.Write(array.Length);
			string[] array2 = array;
			foreach (string text in array2)
			{
				val.Write(text);
			}
			_pendingTerminal = args.Context;
			ZRoutedRpc.instance.InvokeRoutedRPC(serverPeer.m_uid, "ProgressionGater.AdminCommand.v1", new object[1] { val });
		}

		private static void HandleServerCommand(long sender, ZPackage package)
		{
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Expected O, but got Unknown
			if (!ProgressionService.IsServer)
			{
				return;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			bool isAdmin = NetworkManager.IsAdmin(peer);
			string command;
			string[] array;
			try
			{
				command = package.ReadString();
				int num = package.ReadInt();
				if (num < 0 || num > 32)
				{
					throw new InvalidOperationException("Invalid argument count.");
				}
				array = new string[num];
				for (int i = 0; i < num; i++)
				{
					array[i] = package.ReadString();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"Rejected malformed admin command from {sender}: {ex.Message}");
				return;
			}
			string actor = peer?.m_playerName ?? $"peer {sender}";
			List<string> list = Run(command, array, isAdmin, actor);
			ZPackage val = new ZPackage();
			val.Write(list.Count);
			foreach (string item in list)
			{
				val.Write(item ?? "");
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(sender, "ProgressionGater.AdminReply.v1", new object[1] { val });
		}

		private static void HandleClientReply(ZPackage package)
		{
			try
			{
				int num = package.ReadInt();
				if (num < 0 || num > 256)
				{
					throw new InvalidOperationException("Invalid reply count.");
				}
				for (int i = 0; i < num; i++)
				{
					string text = package.ReadString();
					if ((Object)(object)_pendingTerminal != (Object)null)
					{
						_pendingTerminal.AddString(text);
					}
					else
					{
						Plugin.Log.LogInfo((object)text);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Invalid admin-command reply: " + ex.Message));
			}
			finally
			{
				_pendingTerminal = null;
			}
		}

		private static List<string> Run(string command, string[] args, bool isAdmin, string actor)
		{
			if (!isAdmin)
			{
				return new List<string> { "Admin only." };
			}
			return command switch
			{
				"unlock" => Unlock(args, actor), 
				"lock" => Lock(args), 
				"status" => Status(), 
				"defeat" => Defeat(args), 
				"undefeat" => Undefeat(args), 
				"reset" => Reset(args), 
				"webhook_test" => TestWebhook(actor), 
				_ => new List<string> { "Unknown command '" + command + "'." }, 
			};
		}

		private static List<string> Unlock(string[] args, string actor)
		{
			if (args.Length < 1)
			{
				return new List<string> { "Usage: pg_unlock <next|boss>" };
			}
			BossDefinition bossDefinition = (string.Equals(args[0], "next", StringComparison.OrdinalIgnoreCase) ? ProgressionService.NextBoss() : ProgressionService.Rules.Resolve(args[0]));
			if (bossDefinition == null)
			{
				return new List<string> { "Unknown boss '" + args[0] + "'." };
			}
			if (!ProgressionService.Unlock(bossDefinition))
			{
				return new List<string>
				{
					bossDefinition.DisplayName + " is already summon-unlocked.",
					ProgressionService.Status(bossDefinition)
				};
			}
			Plugin.Log.LogInfo((object)(actor + " unlocked boss summon: " + bossDefinition.DisplayName));
			DiscordWebhook.PostBossUnlocked(bossDefinition, actor);
			return new List<string>
			{
				bossDefinition.DisplayName + " may now be summoned.",
				"Its gated recipes remain locked until it is defeated."
			};
		}

		private static List<string> Lock(string[] args)
		{
			List<string> error;
			BossDefinition bossDefinition = ResolveArgument(args, "Usage: pg_lock <boss>", out error);
			if (bossDefinition == null)
			{
				return error;
			}
			if (!ProgressionService.Lock(bossDefinition))
			{
				return new List<string> { bossDefinition.DisplayName + " is already summon-locked." };
			}
			return new List<string> { bossDefinition.DisplayName + " can no longer be summoned. Its recorded defeat was not changed." };
		}

		private static List<string> Defeat(string[] args)
		{
			List<string> error;
			BossDefinition bossDefinition = ResolveArgument(args, "Usage: pg_defeat <boss>", out error);
			if (bossDefinition == null)
			{
				return error;
			}
			if (!ProgressionService.MarkDefeated(bossDefinition))
			{
				return new List<string> { bossDefinition.DisplayName + " is already recorded as defeated." };
			}
			Plugin.Log.LogInfo((object)("Boss administratively marked defeated: " + bossDefinition.DisplayName));
			return new List<string> { bossDefinition.DisplayName + " marked defeated. No kill webhook was posted." };
		}

		private static List<string> Undefeat(string[] args)
		{
			List<string> error;
			BossDefinition bossDefinition = ResolveArgument(args, "Usage: pg_undefeat <boss>", out error);
			if (bossDefinition == null)
			{
				return error;
			}
			if (!ProgressionService.UnmarkDefeated(bossDefinition))
			{
				return new List<string> { bossDefinition.DisplayName + " was not recorded as defeated." };
			}
			return new List<string> { "Removed the recorded defeat for " + bossDefinition.DisplayName + "." };
		}

		private static List<string> Reset(string[] args)
		{
			if (args.Length != 1 || !string.Equals(args[0], "confirm", StringComparison.OrdinalIgnoreCase))
			{
				return new List<string> { "This clears all summon unlocks and defeats. Run: pg_reset confirm" };
			}
			ProgressionService.Reset();
			Plugin.Log.LogWarning((object)"All Progression Gater state was reset by an administrator.");
			return new List<string> { "All Progression Gater state has been cleared." };
		}

		private static List<string> TestWebhook(string actor)
		{
			if (string.IsNullOrWhiteSpace(ModConfig.WebhookUrl.Value))
			{
				return new List<string> { "WebhookUrl is empty in the server config." };
			}
			DiscordWebhook.PostTest(actor);
			return new List<string> { "Webhook test queued. Check Discord and the BepInEx log." };
		}

		private static List<string> Status()
		{
			List<string> list = new List<string>();
			list.Add("=== Progression Gater (" + (ProgressionService.Enabled ? "enabled" : "disabled") + ") ===");
			list.Add("Boss summons: " + (ProgressionService.GateBossSummons ? "gated" : "open") + "; crafting: " + (ProgressionService.GateCrafting ? "gated" : "open"));
			list.Add("Discord webhook: " + (string.IsNullOrWhiteSpace(ModConfig.WebhookUrl.Value) ? "not configured" : "configured"));
			list.AddRange(ProgressionService.Rules.Bosses.Select((BossDefinition boss) => "  " + ProgressionService.Status(boss)));
			return list;
		}

		private static BossDefinition ResolveArgument(string[] args, string usage, out List<string> error)
		{
			if (args.Length < 1)
			{
				error = new List<string> { usage };
				return null;
			}
			BossDefinition bossDefinition = ProgressionService.Rules.Resolve(args[0]);
			error = ((bossDefinition == null) ? new List<string> { "Unknown boss '" + args[0] + "'." } : null);
			return bossDefinition;
		}
	}
	internal sealed class BossDefinition
	{
		internal string Id;

		internal string DisplayName;

		internal string PrefabName;

		internal string GlobalKey;

		internal string[] Aliases = Array.Empty<string>();

		internal readonly HashSet<string> Keywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		internal readonly HashSet<string> ExactPrefabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
	}
	internal static class DiscordWebhook
	{
		private static readonly HttpClient Client = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(10.0)
		};

		internal static void PostBossUnlocked(BossDefinition boss, string admin)
		{
			if (ProgressionService.IsServer && ModConfig.WebhookPostUnlocks.Value)
			{
				string text = ModConfig.UnlockMessage.Value ?? "";
				Post(Expand(text, boss, admin), text.IndexOf("{server}", StringComparison.OrdinalIgnoreCase) < 0);
			}
		}

		internal static void PostBossDefeated(BossDefinition boss)
		{
			if (ProgressionService.IsServer && ModConfig.WebhookPostDefeats.Value)
			{
				string text = ModConfig.DefeatMessage.Value ?? "";
				Post(Expand(text, boss, ""), text.IndexOf("{server}", StringComparison.OrdinalIgnoreCase) < 0);
			}
		}

		internal static void PostTest(string admin)
		{
			if (ProgressionService.IsServer)
			{
				Post("✅ Progression Gater webhook test requested by **" + SafeMentionText(admin ?? "Server admin") + "**.", prefixServer: true);
			}
		}

		private static string Expand(string template, BossDefinition boss, string admin)
		{
			string value = ModConfig.WebhookServerLabel.Value ?? "";
			return (template ?? "").Replace("{boss}", SafeMentionText(boss?.DisplayName ?? "Unknown boss")).Replace("{bossId}", SafeMentionText(boss?.Id ?? "unknown")).Replace("{admin}", SafeMentionText(admin ?? "Server admin"))
				.Replace("{server}", SafeMentionText(value));
		}

		private static void Post(string message, bool prefixServer)
		{
			string text = (ModConfig.WebhookUrl.Value ?? "").Trim();
			if (text.Length != 0 && !string.IsNullOrWhiteSpace(message) && TryValidateUrl(text, out var url))
			{
				string text2 = (ModConfig.WebhookServerLabel.Value ?? "").Trim();
				if (prefixServer && text2.Length > 0)
				{
					message = "**[" + SafeMentionText(text2) + "]** " + message;
				}
				if (message.Length > 1900)
				{
					message = message.Substring(0, 1900);
				}
				PostAsync(url, message);
			}
		}

		private static async Task PostAsync(Uri url, string message)
		{
			try
			{
				string text = "{\"username\":\"Progression Gater\",\"allowed_mentions\":{\"parse\":[]},\"content\":\"" + JsonEscape(message) + "\"}";
				StringContent content = new StringContent(text, Encoding.UTF8, "application/json");
				try
				{
					HttpResponseMessage val = await Client.PostAsync(url, (HttpContent)(object)content).ConfigureAwait(continueOnCapturedContext: false);
					try
					{
						if (!val.IsSuccessStatusCode)
						{
							Plugin.Log.LogWarning((object)$"Discord webhook returned HTTP {(int)val.StatusCode} ({val.ReasonPhrase}).");
						}
					}
					finally
					{
						((IDisposable)val)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)content)?.Dispose();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Discord webhook post failed: " + ex.Message));
			}
		}

		private static bool TryValidateUrl(string raw, out Uri url)
		{
			url = null;
			if (!Uri.TryCreate(raw, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps)
			{
				Plugin.Log.LogWarning((object)"Discord WebhookUrl must be an absolute HTTPS URL.");
				return false;
			}
			if (!ModConfig.AllowNonDiscordWebhookHosts.Value)
			{
				string text = result.Host.ToLowerInvariant();
				if (!(text == "discord.com") && !text.EndsWith(".discord.com", StringComparison.Ordinal) && !(text == "discordapp.com") && !text.EndsWith(".discordapp.com", StringComparison.Ordinal))
				{
					Plugin.Log.LogWarning((object)"WebhookUrl was not posted because its host is not Discord. Enable AllowNonDiscordWebhookHosts only for a trusted compatible endpoint.");
					return false;
				}
			}
			url = result;
			return true;
		}

		private static string SafeMentionText(string value)
		{
			return (value ?? "").Replace("@", "@\u200b");
		}

		private static string JsonEscape(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value.Length + 16);
			foreach (char c in value)
			{
				switch (c)
				{
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\b':
					stringBuilder.Append("\\b");
					continue;
				case '\f':
					stringBuilder.Append("\\f");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
	internal static class ModConfig
	{
		internal const string DefaultBossDefinitions = "eikthyr|Eikthyr|Eikthyr|defeated_eikthyr|deer;elder|The Elder|gd_king|defeated_gdking|gdking;bonemass|Bonemass|Bonemass|defeated_bonemass|;moder|Moder|Dragon|defeated_dragon|;yagluth|Yagluth|GoblinKing|defeated_goblinking|goblinking,defeated_gdking_varguts;queen|The Queen|SeekerQueen|defeated_queen|seekerqueen;fader|Fader|Fader|defeated_fader|";

		internal const string DefaultKeywordRules = "eikthyr=Bronze;elder=Iron;bonemass=Silver,Obsidian;moder=BlackMetal;yagluth=BlackCore,BlackMarble,Carapace,Mandible,Softtissue,YggdrasilWood,Eitr,ScaleHide,RoyalJelly,Sap,JotunPuffs,Magecap;queen=Flametal,Grausten,AskHide,Asksvin,Morgen,CharredBone,CelestialFeather,CeramicPlate,BellFragment,Blackwood,Proustite,Sulfur,Fiddlehead,Vineberry,SmokePuff,Volture,Bonemaw";

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> GateBossSummons;

		internal static ConfigEntry<bool> GateCrafting;

		internal static ConfigEntry<bool> MatchRecipeIngredients;

		internal static ConfigEntry<bool> AdminBypass;

		internal static ConfigEntry<string> BossDefinitions;

		internal static ConfigEntry<string> KeywordRules;

		internal static ConfigEntry<string> ExactPrefabRules;

		internal static ConfigEntry<string> SummonUnlocks;

		internal static ConfigEntry<string> DefeatedBosses;

		internal static ConfigEntry<string> WebhookUrl;

		internal static ConfigEntry<string> WebhookServerLabel;

		internal static ConfigEntry<bool> WebhookPostUnlocks;

		internal static ConfigEntry<bool> WebhookPostDefeats;

		internal static ConfigEntry<bool> AllowNonDiscordWebhookHosts;

		internal static ConfigEntry<string> UnlockMessage;

		internal static ConfigEntry<string> DefeatMessage;

		internal static void Bind(ConfigFile config, Action changed)
		{
			Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch. State continues to remain in this config while disabled.");
			GateBossSummons = config.Bind<bool>("Gates", "GateBossSummons", true, "Require an admin unlock before a configured boss can be summoned.");
			GateCrafting = config.Bind<bool>("Gates", "GateCrafting", true, "Block configured crafting recipes until their boss has been defeated.");
			MatchRecipeIngredients = config.Bind<bool>("Gates", "MatchRecipeIngredients", true, "Apply crafting rules to both recipe output and ingredient prefab names. False checks only the output.");
			AdminBypass = config.Bind<bool>("Gates", "AdminBypass", true, "Allow server admins to bypass local crafting and boss-summon gates.");
			BossDefinitions = config.Bind<string>("Rules", "BossDefinitions", "eikthyr|Eikthyr|Eikthyr|defeated_eikthyr|deer;elder|The Elder|gd_king|defeated_gdking|gdking;bonemass|Bonemass|Bonemass|defeated_bonemass|;moder|Moder|Dragon|defeated_dragon|;yagluth|Yagluth|GoblinKing|defeated_goblinking|goblinking,defeated_gdking_varguts;queen|The Queen|SeekerQueen|defeated_queen|seekerqueen;fader|Fader|Fader|defeated_fader|", "Semicolon-separated records: id|display name|boss prefab|defeat global key|comma-separated aliases. Order defines pg_unlock next.");
			KeywordRules = config.Bind<string>("Rules", "KeywordRules", "eikthyr=Bronze;elder=Iron;bonemass=Silver,Obsidian;moder=BlackMetal;yagluth=BlackCore,BlackMarble,Carapace,Mandible,Softtissue,YggdrasilWood,Eitr,ScaleHide,RoyalJelly,Sap,JotunPuffs,Magecap;queen=Flametal,Grausten,AskHide,Asksvin,Morgen,CharredBone,CelestialFeather,CeramicPlate,BellFragment,Blackwood,Proustite,Sulfur,Fiddlehead,Vineberry,SmokePuff,Volture,Bonemaw", "Semicolon-separated bossId=keyword,keyword rules. A case-insensitive substring match gates a recipe.");
			ExactPrefabRules = config.Bind<string>("Rules", "ExactPrefabRules", "", "Semicolon-separated bossId=prefab,prefab rules. Exact, case-insensitive recipe output/ingredient matching; useful for modded items.");
			SummonUnlocks = config.Bind<string>("State", "SummonUnlocks", "", "Managed by pg_unlock/pg_lock. Comma-separated boss IDs.");
			DefeatedBosses = config.Bind<string>("State", "DefeatedBosses", "", "Managed by boss kills and pg_defeat/pg_undefeat. Comma-separated boss IDs.");
			WebhookUrl = config.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL. Empty disables webhook posting.");
			WebhookServerLabel = config.Bind<string>("Discord", "ServerLabel", "", "Optional server name included in webhook messages.");
			WebhookPostUnlocks = config.Bind<bool>("Discord", "PostBossUnlocks", true, "Post when an admin unlocks a boss summon.");
			WebhookPostDefeats = config.Bind<bool>("Discord", "PostBossDefeats", true, "Post when the game records a configured boss defeat.");
			AllowNonDiscordWebhookHosts = config.Bind<bool>("Discord", "AllowNonDiscordWebhookHosts", false, "Permit HTTPS webhook URLs outside discord.com/discordapp.com.");
			UnlockMessage = config.Bind<string>("Discord", "UnlockMessage", "\ud83d\udd13 **{boss}** has been unlocked by **{admin}**.", "Tokens: {boss}, {bossId}, {admin}, {server}");
			DefeatMessage = config.Bind<string>("Discord", "DefeatMessage", "⚔\ufe0f **{boss}** has been defeated. Its progression tier is now unlocked.", "Tokens: {boss}, {bossId}, {server}");
			Watch<bool>(Enabled, changed);
			Watch<bool>(GateBossSummons, changed);
			Watch<bool>(GateCrafting, changed);
			Watch<bool>(MatchRecipeIngredients, changed);
			Watch<bool>(AdminBypass, changed);
			Watch<string>(BossDefinitions, changed);
			Watch<string>(KeywordRules, changed);
			Watch<string>(ExactPrefabRules, changed);
			Watch<string>(WebhookUrl, changed);
			Watch<string>(WebhookServerLabel, changed);
			Watch<bool>(WebhookPostUnlocks, changed);
			Watch<bool>(WebhookPostDefeats, changed);
			Watch<bool>(AllowNonDiscordWebhookHosts, changed);
			Watch<string>(UnlockMessage, changed);
			Watch<string>(DefeatMessage, changed);
		}

		private static void Watch<T>(ConfigEntry<T> entry, Action changed)
		{
			entry.SettingChanged += delegate
			{
				changed();
			};
		}
	}
	internal static class NetworkManager
	{
		internal const string SyncRpc = "ProgressionGater.Sync.v1";

		internal const string MessageRpc = "ProgressionGater.Message.v1";

		internal static void Register(ZRoutedRpc rpc)
		{
			rpc.Register<ZPackage>("ProgressionGater.Sync.v1", (Action<long, ZPackage>)delegate(long sender, ZPackage package)
			{
				if (!IsFromServer(sender))
				{
					Plugin.Log.LogWarning((object)$"Rejected progression snapshot from non-server peer {sender}.");
					return;
				}
				try
				{
					ProgressionService.ApplyServerSnapshot(package);
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("Invalid progression snapshot: " + ex.Message));
				}
			});
			rpc.Register<string>("ProgressionGater.Message.v1", (Action<long, string>)delegate(long sender, string message)
			{
				if (IsFromServer(sender))
				{
					Player localPlayer = Player.m_localPlayer;
					if (localPlayer != null)
					{
						((Character)localPlayer).Message((MessageType)2, message, 0, (Sprite)null);
					}
				}
			});
			AdminCommands.RegisterRpcHandlers(rpc);
		}

		internal static bool IsAdmin(ZNetPeer peer)
		{
			if (peer == null || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return false;
			}
			ISocket socket = peer.m_socket;
			string text = ((socket != null) ? socket.GetHostName() : null) ?? "";
			if (text.Length == 0)
			{
				return false;
			}
			try
			{
				return ZNet.instance.IsAdmin(text);
			}
			catch
			{
				return false;
			}
		}

		internal static bool CanPeerBypass(long peerId)
		{
			if (!ModConfig.AdminBypass.Value || (Object)(object)ZNet.instance == (Object)null)
			{
				return false;
			}
			if (!ZNet.instance.IsServer())
			{
				return ProgressionService.LocalCanBypass;
			}
			return IsAdmin(ZNet.instance.GetPeer(peerId));
		}

		internal static void SendMessage(long peerId, string message)
		{
			if (ZRoutedRpc.instance != null)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "ProgressionGater.Message.v1", new object[1] { message });
			}
		}

		internal static bool IsFromServer(long sender)
		{
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				return false;
			}
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (serverPeer != null && serverPeer.m_uid == sender)
			{
				return true;
			}
			try
			{
				long value = Traverse.Create((object)ZRoutedRpc.instance).Field("m_serverPeerID").GetValue<long>();
				return value != 0L && sender == value;
			}
			catch
			{
				return false;
			}
		}
	}
	[BepInPlugin("com.catosaur.progressiongater", "Progression Gater", "0.1.1")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.catosaur.progressiongater";

		public const string Name = "Progression Gater";

		public const string Version = "0.1.1";

		private Harmony _harmony;

		private FileSystemWatcher _configWatcher;

		private DateTime _lastWatcherEventUtc = DateTime.MinValue;

		private volatile bool _reloadRequested;

		internal static Plugin Instance { get; private set; }

		internal static ManualLogSource Log { get; private set; }

		private void Awake()
		{
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ModConfig.Bind(((BaseUnityPlugin)this).Config, RequestConfigReload);
			ProgressionService.Initialize();
			SetupConfigWatcher();
			_harmony = new Harmony("com.catosaur.progressiongater");
			_harmony.PatchAll(typeof(ProgressionPatches));
			_harmony.PatchAll(typeof(AdminCommands));
			Log.LogInfo((object)string.Format("{0} {1} loaded with {2} progression bosses.", "Progression Gater", "0.1.1", ProgressionService.Rules.Bosses.Count));
		}

		private void Update()
		{
			if (!_reloadRequested)
			{
				return;
			}
			_reloadRequested = false;
			try
			{
				((BaseUnityPlugin)this).Config.Reload();
				ProgressionService.ReloadRules();
				ProgressionService.Broadcast();
				Log.LogInfo((object)"Configuration reloaded and synchronized to connected players.");
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"Configuration reload failed: {arg}");
			}
		}

		private void RequestConfigReload()
		{
			_reloadRequested = true;
		}

		private void SetupConfigWatcher()
		{
			try
			{
				string configFilePath = ((BaseUnityPlugin)this).Config.ConfigFilePath;
				_configWatcher = new FileSystemWatcher(Path.GetDirectoryName(configFilePath), Path.GetFileName(configFilePath))
				{
					NotifyFilter = (NotifyFilters.Size | NotifyFilters.LastWrite),
					EnableRaisingEvents = true
				};
				_configWatcher.Changed += delegate
				{
					DateTime utcNow = DateTime.UtcNow;
					if (!((utcNow - _lastWatcherEventUtc).TotalMilliseconds < 500.0))
					{
						_lastWatcherEventUtc = utcNow;
						RequestConfigReload();
					}
				};
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Config live-reload watcher could not be started: " + ex.Message));
			}
		}

		internal static void SaveConfig()
		{
			Plugin instance = Instance;
			if (instance != null)
			{
				((BaseUnityPlugin)instance).Config.Save();
			}
		}

		private void OnDestroy()
		{
			_configWatcher?.Dispose();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchAll("com.catosaur.progressiongater");
			}
		}
	}
	[HarmonyPatch]
	internal static class ProgressionPatches
	{
		[HarmonyPatch(/*Could not decode attribute arguments.*/)]
		[HarmonyPostfix]
		private static void OnRoutedRpcCreated(ZRoutedRpc __instance)
		{
			if (__instance != null)
			{
				ProgressionService.ClearServerSnapshot();
				NetworkManager.Register(__instance);
			}
		}

		[HarmonyPatch(typeof(ZRoutedRpc), "AddPeer")]
		[HarmonyPostfix]
		private static void OnPeerAdded(ZNetPeer peer)
		{
			if (ProgressionService.IsServer && peer != null)
			{
				ProgressionService.SendToPeer(peer);
			}
		}

		[HarmonyPatch(typeof(ZoneSystem), "RPC_SetGlobalKey")]
		[HarmonyPostfix]
		private static void OnGlobalKeySet(string name)
		{
			if (!ProgressionService.IsServer || !ProgressionService.Enabled || string.IsNullOrWhiteSpace(name))
			{
				return;
			}
			try
			{
				BossDefinition bossDefinition = ProgressionService.Rules.ResolveGlobalKey(name);
				if (bossDefinition != null && ProgressionService.MarkDefeated(bossDefinition))
				{
					Plugin.Log.LogInfo((object)("Boss defeat recorded from global key '" + name + "': " + bossDefinition.DisplayName));
					DiscordWebhook.PostBossDefeated(bossDefinition);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Boss-defeat processing failed for key '{name}': {arg}");
			}
		}

		[HarmonyPatch(typeof(Player), "HaveRequirements", new Type[]
		{
			typeof(Recipe),
			typeof(bool),
			typeof(int),
			typeof(int)
		})]
		[HarmonyPrefix]
		private static bool OnHaveRequirements(Player __instance, Recipe recipe, ref bool __result)
		{
			if (!ProgressionService.GateCrafting || ProgressionService.LocalCanBypass || (Object)(object)__instance == (Object)null || (Object)(object)recipe?.m_item == (Object)null)
			{
				return true;
			}
			if (ProgressionService.Rules.RequiredBosses(recipe, ProgressionService.MatchIngredients).FirstOrDefault((BossDefinition boss) => !ProgressionService.IsDefeated(boss)) == null)
			{
				return true;
			}
			__result = false;
			return false;
		}

		[HarmonyPatch(typeof(OfferingBowl), "UseItem")]
		[HarmonyPrefix]
		private static bool OnOfferingBowlUseItem(OfferingBowl __instance, Humanoid user, ref bool __result)
		{
			if (!ProgressionService.GateBossSummons || ProgressionService.LocalCanBypass)
			{
				return true;
			}
			BossDefinition bossDefinition = ProgressionService.Rules.Resolve(__instance);
			if (bossDefinition == null || ProgressionService.IsSummonUnlocked(bossDefinition))
			{
				return true;
			}
			if (user != null)
			{
				((Character)user).Message((MessageType)2, bossDefinition.DisplayName + " is locked. A server admin must unlock this boss before it can be summoned.", 0, (Sprite)null);
			}
			__result = false;
			return false;
		}

		[HarmonyPatch(typeof(OfferingBowl), "RPC_SpawnBoss")]
		[HarmonyPrefix]
		private static bool OnOfferingBowlSpawnBoss(OfferingBowl __instance, long senderId)
		{
			if (!ProgressionService.GateBossSummons)
			{
				return true;
			}
			BossDefinition bossDefinition = ProgressionService.Rules.Resolve(__instance);
			if (bossDefinition == null || ProgressionService.IsSummonUnlocked(bossDefinition) || NetworkManager.CanPeerBypass(senderId))
			{
				return true;
			}
			Plugin.Log.LogWarning((object)$"Blocked locked boss summon: boss={bossDefinition.Id}, sender={senderId}");
			NetworkManager.SendMessage(senderId, bossDefinition.DisplayName + " is locked. A server admin must unlock this boss before it can be summoned.");
			return false;
		}
	}
	internal static class ProgressionService
	{
		internal const int ProtocolVersion = 1;

		private static RuleCatalog _rules;

		private static HashSet<string> _clientSummonUnlocks = NewSet();

		private static HashSet<string> _clientDefeats = NewSet();

		private static bool _clientEnabled;

		private static bool _clientGateBossSummons;

		private static bool _clientGateCrafting;

		private static bool _clientMatchIngredients;

		private static bool _clientCanBypass;

		private static bool _hasServerSnapshot;

		internal static RuleCatalog Rules => _rules ?? (_rules = RuleCatalog.FromConfig());

		internal static bool IsServer
		{
			get
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					return ZNet.instance.IsServer();
				}
				return false;
			}
		}

		internal static bool Enabled
		{
			get
			{
				if (!IsServer && _hasServerSnapshot)
				{
					return _clientEnabled;
				}
				return ModConfig.Enabled.Value;
			}
		}

		internal static bool GateBossSummons
		{
			get
			{
				if (Enabled)
				{
					if (!IsServer && _hasServerSnapshot)
					{
						return _clientGateBossSummons;
					}
					return ModConfig.GateBossSummons.Value;
				}
				return false;
			}
		}

		internal static bool GateCrafting
		{
			get
			{
				if (Enabled)
				{
					if (!IsServer && _hasServerSnapshot)
					{
						return _clientGateCrafting;
					}
					return ModConfig.GateCrafting.Value;
				}
				return false;
			}
		}

		internal static bool MatchIngredients
		{
			get
			{
				if (!IsServer && _hasServerSnapshot)
				{
					return _clientMatchIngredients;
				}
				return ModConfig.MatchRecipeIngredients.Value;
			}
		}

		internal static bool LocalCanBypass
		{
			get
			{
				if (!IsServer)
				{
					if (_hasServerSnapshot)
					{
						return _clientCanBypass;
					}
					return false;
				}
				if (ModConfig.AdminBypass.Value)
				{
					if (!((Object)(object)Player.m_localPlayer == (Object)null))
					{
						return ZNet.instance.LocalPlayerIsAdminOrHost();
					}
					return true;
				}
				return false;
			}
		}

		internal static void Initialize()
		{
			_rules = RuleCatalog.FromConfig();
		}

		internal static void ReloadRules()
		{
			if (IsServer || (Object)(object)ZNet.instance == (Object)null)
			{
				_rules = RuleCatalog.FromConfig();
			}
		}

		internal static bool IsSummonUnlocked(BossDefinition boss)
		{
			if (boss != null)
			{
				return CurrentSummonUnlocks().Contains(boss.Id);
			}
			return false;
		}

		internal static bool IsDefeated(BossDefinition boss)
		{
			if (boss != null)
			{
				return CurrentDefeats().Contains(boss.Id);
			}
			return false;
		}

		internal static bool Unlock(BossDefinition boss)
		{
			if (!IsServer || boss == null)
			{
				return false;
			}
			HashSet<string> hashSet = ParseState(ModConfig.SummonUnlocks.Value);
			if (!hashSet.Add(boss.Id))
			{
				return false;
			}
			ModConfig.SummonUnlocks.Value = SerializeState(hashSet);
			Plugin.SaveConfig();
			Broadcast();
			return true;
		}

		internal static bool Lock(BossDefinition boss)
		{
			if (!IsServer || boss == null)
			{
				return false;
			}
			HashSet<string> hashSet = ParseState(ModConfig.SummonUnlocks.Value);
			if (!hashSet.Remove(boss.Id))
			{
				return false;
			}
			ModConfig.SummonUnlocks.Value = SerializeState(hashSet);
			Plugin.SaveConfig();
			Broadcast();
			return true;
		}

		internal static bool MarkDefeated(BossDefinition boss)
		{
			if (!IsServer || boss == null)
			{
				return false;
			}
			HashSet<string> hashSet = ParseState(ModConfig.DefeatedBosses.Value);
			if (!hashSet.Add(boss.Id))
			{
				return false;
			}
			ModConfig.DefeatedBosses.Value = SerializeState(hashSet);
			Plugin.SaveConfig();
			Broadcast();
			return true;
		}

		internal static bool UnmarkDefeated(BossDefinition boss)
		{
			if (!IsServer || boss == null)
			{
				return false;
			}
			HashSet<string> hashSet = ParseState(ModConfig.DefeatedBosses.Value);
			if (!hashSet.Remove(boss.Id))
			{
				return false;
			}
			ModConfig.DefeatedBosses.Value = SerializeState(hashSet);
			Plugin.SaveConfig();
			Broadcast();
			return true;
		}

		internal static void Reset()
		{
			if (IsServer)
			{
				ModConfig.SummonUnlocks.Value = "";
				ModConfig.DefeatedBosses.Value = "";
				Plugin.SaveConfig();
				Broadcast();
			}
		}

		internal static BossDefinition NextBoss()
		{
			return Rules.Bosses.FirstOrDefault((BossDefinition boss) => !IsDefeated(boss));
		}

		internal static string Status(BossDefinition boss)
		{
			return boss.DisplayName + ": summon " + (IsSummonUnlocked(boss) ? "unlocked" : "LOCKED") + "; recipes " + (IsDefeated(boss) ? "unlocked" : "locked");
		}

		internal static void SendToPeer(ZNetPeer peer)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected O, but got Unknown
			if (IsServer && peer != null && ZRoutedRpc.instance != null)
			{
				ZPackage val = new ZPackage();
				val.Write(1);
				val.Write(ModConfig.Enabled.Value);
				val.Write(ModConfig.GateBossSummons.Value);
				val.Write(ModConfig.GateCrafting.Value);
				val.Write(ModConfig.MatchRecipeIngredients.Value);
				val.Write(ModConfig.AdminBypass.Value && NetworkManager.IsAdmin(peer));
				Rules.Write(val);
				WriteSet(val, CurrentSummonUnlocks());
				WriteSet(val, CurrentDefeats());
				ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "ProgressionGater.Sync.v1", new object[1] { val });
			}
		}

		internal static void Broadcast()
		{
			if (!IsServer || (Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null)
			{
				return;
			}
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				SendToPeer(peer);
			}
		}

		internal static void ApplyServerSnapshot(ZPackage package)
		{
			int num = package.ReadInt();
			if (num != 1)
			{
				throw new InvalidOperationException($"Unsupported protocol {num}; expected {1}.");
			}
			_clientEnabled = package.ReadBool();
			_clientGateBossSummons = package.ReadBool();
			_clientGateCrafting = package.ReadBool();
			_clientMatchIngredients = package.ReadBool();
			_clientCanBypass = package.ReadBool();
			_rules = RuleCatalog.Read(package);
			_clientSummonUnlocks = ReadSet(package);
			_clientDefeats = ReadSet(package);
			_hasServerSnapshot = true;
			Plugin.Log.LogInfo((object)("Server progression synchronized: summon=[" + string.Join(",", _clientSummonUnlocks) + "], defeated=[" + string.Join(",", _clientDefeats) + "]"));
		}

		internal static void ClearServerSnapshot()
		{
			_hasServerSnapshot = false;
			_clientCanBypass = false;
			_rules = RuleCatalog.FromConfig();
		}

		private static HashSet<string> CurrentSummonUnlocks()
		{
			if (!IsServer && _hasServerSnapshot)
			{
				return _clientSummonUnlocks;
			}
			return ParseState(ModConfig.SummonUnlocks.Value);
		}

		private static HashSet<string> CurrentDefeats()
		{
			if (!IsServer && _hasServerSnapshot)
			{
				return _clientDefeats;
			}
			return ParseState(ModConfig.DefeatedBosses.Value);
		}

		private static HashSet<string> ParseState(string value)
		{
			HashSet<string> hashSet = NewSet();
			string[] array = (value ?? "").Split(new char[1] { ',' });
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length > 0)
				{
					hashSet.Add(text);
				}
			}
			return hashSet;
		}

		private static string SerializeState(HashSet<string> state)
		{
			return string.Join(",", from boss in Rules.Bosses
				where state.Contains(boss.Id)
				select boss.Id);
		}

		private static HashSet<string> NewSet()
		{
			return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		}

		private static void WriteSet(ZPackage package, HashSet<string> state)
		{
			package.Write(state.Count);
			foreach (string item in state)
			{
				package.Write(item);
			}
		}

		private static HashSet<string> ReadSet(ZPackage package)
		{
			int num = package.ReadInt();
			if (num < 0 || num > 1000)
			{
				throw new InvalidOperationException("Invalid state count in server snapshot.");
			}
			HashSet<string> hashSet = NewSet();
			for (int i = 0; i < num; i++)
			{
				hashSet.Add(package.ReadString());
			}
			return hashSet;
		}
	}
	internal sealed class RuleCatalog
	{
		internal List<BossDefinition> Bosses { get; } = new List<BossDefinition>();

		internal static RuleCatalog FromConfig()
		{
			RuleCatalog ruleCatalog = Parse(ModConfig.BossDefinitions.Value);
			ApplyRules(ruleCatalog, ModConfig.KeywordRules.Value, exact: false);
			ApplyRules(ruleCatalog, ModConfig.ExactPrefabRules.Value, exact: true);
			return ruleCatalog;
		}

		private static RuleCatalog Parse(string raw)
		{
			RuleCatalog ruleCatalog = new RuleCatalog();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (string item in Split(raw, ';'))
			{
				string[] array = item.Split(new char[1] { '|' });
				if (array.Length < 4)
				{
					Plugin.Log.LogWarning((object)("Ignoring malformed boss definition: " + item));
					continue;
				}
				string text = array[0].Trim();
				string text2 = array[1].Trim();
				string text3 = array[2].Trim();
				string text4 = array[3].Trim();
				if (text.Length == 0 || text2.Length == 0 || text3.Length == 0 || text4.Length == 0 || !hashSet.Add(text))
				{
					Plugin.Log.LogWarning((object)("Ignoring invalid or duplicate boss definition: " + item));
					continue;
				}
				ruleCatalog.Bosses.Add(new BossDefinition
				{
					Id = text,
					DisplayName = text2,
					PrefabName = text3,
					GlobalKey = text4,
					Aliases = ((array.Length >= 5) ? Split(array[4], ',').ToArray() : Array.Empty<string>())
				});
			}
			if (ruleCatalog.Bosses.Count == 0 && !string.Equals(raw, "eikthyr|Eikthyr|Eikthyr|defeated_eikthyr|deer;elder|The Elder|gd_king|defeated_gdking|gdking;bonemass|Bonemass|Bonemass|defeated_bonemass|;moder|Moder|Dragon|defeated_dragon|;yagluth|Yagluth|GoblinKing|defeated_goblinking|goblinking,defeated_gdking_varguts;queen|The Queen|SeekerQueen|defeated_queen|seekerqueen;fader|Fader|Fader|defeated_fader|", StringComparison.Ordinal))
			{
				Plugin.Log.LogError((object)"No valid boss definitions were configured; loading built-in defaults.");
				return Parse("eikthyr|Eikthyr|Eikthyr|defeated_eikthyr|deer;elder|The Elder|gd_king|defeated_gdking|gdking;bonemass|Bonemass|Bonemass|defeated_bonemass|;moder|Moder|Dragon|defeated_dragon|;yagluth|Yagluth|GoblinKing|defeated_goblinking|goblinking,defeated_gdking_varguts;queen|The Queen|SeekerQueen|defeated_queen|seekerqueen;fader|Fader|Fader|defeated_fader|");
			}
			return ruleCatalog;
		}

		private static void ApplyRules(RuleCatalog catalog, string raw, bool exact)
		{
			foreach (string item in Split(raw, ';'))
			{
				int num = item.IndexOf('=');
				if (num <= 0)
				{
					Plugin.Log.LogWarning((object)("Ignoring malformed " + (exact ? "exact" : "keyword") + " rule: " + item));
					continue;
				}
				string text = item.Substring(0, num).Trim();
				BossDefinition bossDefinition = catalog.Resolve(text);
				if (bossDefinition == null)
				{
					Plugin.Log.LogWarning((object)("Ignoring rule for unknown boss ID '" + text + "'."));
					continue;
				}
				foreach (string item2 in Split(item.Substring(num + 1), ','))
				{
					if (exact)
					{
						bossDefinition.ExactPrefabs.Add(item2);
					}
					else
					{
						bossDefinition.Keywords.Add(item2);
					}
				}
			}
		}

		private static IEnumerable<string> Split(string value, char separator)
		{
			return from part in (value ?? "").Split(new char[1] { separator }, StringSplitOptions.RemoveEmptyEntries)
				select part.Trim() into part
				where part.Length > 0
				select part;
		}

		internal BossDefinition Resolve(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return null;
			}
			string needle = Normalize(value);
			return Bosses.FirstOrDefault((BossDefinition boss) => EqualsIgnoreCase(boss.Id, needle) || EqualsIgnoreCase(boss.DisplayName, needle) || EqualsIgnoreCase(boss.PrefabName, needle) || EqualsIgnoreCase(boss.GlobalKey, needle) || boss.Aliases.Any((string alias) => EqualsIgnoreCase(alias, needle)));
		}

		internal BossDefinition ResolveGlobalKey(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return null;
			}
			string needle = Normalize(value);
			return Bosses.FirstOrDefault((BossDefinition boss) => EqualsIgnoreCase(boss.GlobalKey, needle) || boss.Aliases.Any((string alias) => alias.StartsWith("defeated_", StringComparison.OrdinalIgnoreCase) && EqualsIgnoreCase(alias, needle)));
		}

		internal BossDefinition Resolve(OfferingBowl bowl)
		{
			if ((Object)(object)bowl == (Object)null)
			{
				return null;
			}
			try
			{
				GameObject value = Traverse.Create((object)bowl).Field<GameObject>("m_bossPrefab").Value;
				return ((Object)(object)value == (Object)null) ? null : Resolve(((Object)value).name);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not identify offering-bowl boss: " + ex.Message));
				return null;
			}
		}

		internal IEnumerable<BossDefinition> RequiredBosses(Recipe recipe, bool includeIngredients)
		{
			HashSet<string> names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			if ((Object)(object)recipe?.m_item != (Object)null)
			{
				names.Add(Normalize(((Object)recipe.m_item).name));
			}
			if (includeIngredients && recipe?.m_resources != null)
			{
				Requirement[] resources = recipe.m_resources;
				foreach (Requirement val in resources)
				{
					if ((Object)(object)val?.m_resItem != (Object)null)
					{
						names.Add(Normalize(((Object)val.m_resItem).name));
					}
				}
			}
			foreach (BossDefinition boss in Bosses)
			{
				if (names.Any((string name) => boss.ExactPrefabs.Contains(name) || boss.Keywords.Any((string keyword) => name.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)))
				{
					yield return boss;
				}
			}
		}

		internal void Write(ZPackage package)
		{
			package.Write(Bosses.Count);
			foreach (BossDefinition boss in Bosses)
			{
				package.Write(boss.Id);
				package.Write(boss.DisplayName);
				package.Write(boss.PrefabName);
				package.Write(boss.GlobalKey);
				WriteStrings(package, boss.Aliases);
				WriteStrings(package, boss.Keywords);
				WriteStrings(package, boss.ExactPrefabs);
			}
		}

		internal static RuleCatalog Read(ZPackage package)
		{
			RuleCatalog ruleCatalog = new RuleCatalog();
			int num = package.ReadInt();
			if (num < 0 || num > 100)
			{
				throw new InvalidOperationException("Invalid boss count in server snapshot.");
			}
			for (int i = 0; i < num; i++)
			{
				BossDefinition bossDefinition = new BossDefinition
				{
					Id = package.ReadString(),
					DisplayName = package.ReadString(),
					PrefabName = package.ReadString(),
					GlobalKey = package.ReadString(),
					Aliases = ReadStrings(package)
				};
				string[] array = ReadStrings(package);
				foreach (string item in array)
				{
					bossDefinition.Keywords.Add(item);
				}
				array = ReadStrings(package);
				foreach (string item2 in array)
				{
					bossDefinition.ExactPrefabs.Add(item2);
				}
				ruleCatalog.Bosses.Add(bossDefinition);
			}
			return ruleCatalog;
		}

		private static void WriteStrings(ZPackage package, IEnumerable<string> values)
		{
			string[] array = values.ToArray();
			package.Write(array.Length);
			string[] array2 = array;
			foreach (string text in array2)
			{
				package.Write(text);
			}
		}

		private static string[] ReadStrings(ZPackage package)
		{
			int num = package.ReadInt();
			if (num < 0 || num > 1000)
			{
				throw new InvalidOperationException("Invalid rule count in server snapshot.");
			}
			string[] array = new string[num];
			for (int i = 0; i < num; i++)
			{
				array[i] = package.ReadString();
			}
			return array;
		}

		private static string Normalize(string value)
		{
			return (value ?? "").Replace("(Clone)", "").Trim();
		}

		private static bool EqualsIgnoreCase(string left, string right)
		{
			return string.Equals(Normalize(left), Normalize(right), StringComparison.OrdinalIgnoreCase);
		}
	}
}