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 ValheimServerGuide v0.5.2
ValheimServerGuide.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
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