Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ProgressionGater v0.1.1
ProgressionGater.dll
Decompiled 8 hours agousing 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); } } }