using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Managers; using Microsoft.CodeAnalysis; using Splatform; 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.9.0.0")] [assembly: AssemblyInformationalVersion("0.9.0+cca6ca3a555187c94df146e33c1d1f44da17ad22")] [assembly: AssemblyProduct("ValheimServerGuide")] [assembly: AssemblyTitle("ValheimServerGuide")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.9.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimServerGuide { [BepInPlugin("com.valheimserverguide", "ValheimServerGuide", "0.9.0")] [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.9.0"; 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 RavenEnabled { get; private set; } public static ConfigEntry IntroMusicName { get; private set; } public static ConfigEntry IntroMusicDuration { get; private set; } public static ConfigEntry IntroFadeInDuration { get; private set; } public static ConfigEntry IntroPreDelay { get; private set; } public static ConfigEntry IntroDisplaySeconds { get; private set; } public static ConfigEntry IntroFadeOutDuration { get; private set; } public static ConfigEntry ChatColor { get; private set; } public static ConfigEntry CodexEnabled { get; private set; } public static ConfigEntry CodexKey { get; private set; } public static ConfigEntry TrackerEnabled { get; private set; } public static ConfigEntry TrackerPosition { get; private set; } public static ConfigEntry TrackerMaxVisible { get; private set; } public static ConfigEntry TrackerHotkey { get; private set; } public static ConfigEntry TrackerBadgeEnabled { get; private set; } public static ConfigEntry DiscordWebhookUrl { get; private set; } public static ConfigEntry DiscordDefaultTemplate { get; private set; } public static ConfigEntry DiscordBotUsername { get; private set; } public static ConfigEntry DiscordGuideEnabled { get; private set; } public static ConfigEntry DiscordGuideFormat { get; private set; } public static ConfigEntry DiscordQuestStartEnabled { get; private set; } public static ConfigEntry DiscordQuestStartWebhookUrl { get; private set; } private void Awake() { //IL_032c: Unknown result type (might be due to invalid IL or missing references) //IL_0336: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; RavenEnabled = ((BaseUnityPlugin)this).Config.Bind("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("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("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("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("Display", "IntroPreDelay", 1f, "Seconds to hold on a black screen after the fade-in, before the intro text appears. Adds dramatic weight to the transition."); IntroDisplaySeconds = ((BaseUnityPlugin)this).Config.Bind("Display", "IntroDisplaySeconds", 15f, "How long the intro text stays on screen (seconds) before it auto-fades out. The player stays frozen and invulnerable for this whole time, or until they press Use/Escape to skip early. Range 1-300."); IntroFadeOutDuration = ((BaseUnityPlugin)this).Config.Bind("Display", "IntroFadeOutDuration", 1f, "Seconds to fade the intro text out when it ends (on skip via Use/Escape or when IntroDisplaySeconds elapses). Set 0 for an instant cut."); ChatColor = ((BaseUnityPlugin)this).Config.Bind("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("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("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("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("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("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("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("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("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("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("Discord", "BotUsername", "ValheimServerGuide", "Username shown for webhook messages in Discord."); DiscordGuideEnabled = ((BaseUnityPlugin)this).Config.Bind("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("Discord", "DiscordGuideFormat", "plain", "Format for guide-completion messages: 'plain' (content string) or 'embed' (rich embed)."); DiscordQuestStartEnabled = ((BaseUnityPlugin)this).Config.Bind("Discord", "QuestStartLogEnabled", true, "Enable the quest-start debug log: a separate webhook POST every time a player starts a new quest (a chain's first step, or the first fire of any single entry). Intended for verifying quests trigger correctly. Requires QuestStartWebhookUrl to be set."); DiscordQuestStartWebhookUrl = ((BaseUnityPlugin)this).Config.Bind("Discord", "QuestStartWebhookUrl", "", "Discord webhook URL for the quest-start debug log (separate channel from WebhookUrl). Server-side only — never share with clients. Leave empty to disable the quest-start log regardless of QuestStartLogEnabled."); _harmony = new Harmony("com.valheimserverguide"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods()) { Log.LogInfo((object)("Harmony patched: " + patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name)); } _configDir = Path.Combine(Paths.ConfigPath, "ValheimServerGuide"); GuidanceSync.Register(); GuidanceDisplay.Initialize(); AdminCommands.Register(); TimeTrigger.Start(); if (Application.isBatchMode) { Log.LogInfo((object)"Running in batch mode (dedicated server). Loading guidance YAML now."); EnsureLoaderStarted(); } else { Log.LogInfo((object)"Client process. Guidance YAML will be loaded only if this session hosts a world."); } Log.LogInfo((object)"ValheimServerGuide v0.9.0 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 unsafe 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*)(¤tBiome))/*cast due to .constrained prefix*/).ToString() }); } } } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class BiomeTriggerSpawnReset { [HarmonyPostfix] private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { BiomeTrigger.Reset(); } } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class BossDefeatedTrigger { [HarmonyPostfix] private static void Postfix(Character __instance) { if ((Object)(object)Player.m_localPlayer == (Object)null || !__instance.IsBoss()) { return; } object obj; if (__instance == null) { obj = null; } else { HitData lastHit = __instance.m_lastHit; obj = ((lastHit != null) ? lastHit.GetAttacker() : null); } 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(CookingStation), "Interact")] internal static class CookingStationUsedTrigger { [HarmonyPostfix] private static void Postfix(CookingStation __instance, Humanoid user, bool hold) { if (!hold && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer)) { TriggerEvent obj = new TriggerEvent { Type = "cooking_used" }; GameObject gameObject = ((Component)__instance).gameObject; obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); obj.DisplayName = __instance.m_name; GuidanceDispatcher.Raise(obj); } } } [HarmonyPatch(typeof(Fireplace), "Interact")] internal static class FireplaceUsedTrigger { [HarmonyPostfix] private static void Postfix(Fireplace __instance, Humanoid user, bool hold) { if (!hold && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer)) { TriggerEvent obj = new TriggerEvent { Type = "cooking_used" }; GameObject gameObject = ((Component)__instance).gameObject; obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); obj.DisplayName = __instance.m_name; GuidanceDispatcher.Raise(obj); } } } [HarmonyPatch(typeof(CraftingStation), "Interact")] internal static class CraftingTableUsedTrigger { [HarmonyPostfix] private static void Postfix(CraftingStation __instance, Humanoid user, bool repeat) { if (!repeat && !((Object)(object)user == (Object)null) && !((Object)(object)user != (Object)(object)Player.m_localPlayer)) { TriggerEvent obj = new TriggerEvent { Type = "crafting_table_used" }; GameObject gameObject = ((Component)__instance).gameObject; obj.Subject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); obj.DisplayName = __instance.m_name; GuidanceDispatcher.Raise(obj); } } } [HarmonyPatch(typeof(InventoryGui), "DoCrafting")] internal static class CraftTrigger { [HarmonyPostfix] private static void Postfix(InventoryGui __instance, Player player) { if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { Plugin.Log.LogDebug((object)"[craft] postfix fired for non-local player; ignoring."); return; } Recipe craftRecipe = __instance.m_craftRecipe; object obj; if (craftRecipe == null) { obj = null; } else { ItemDrop item = craftRecipe.m_item; obj = ((item != null) ? ((Component)item).gameObject : null); } GameObject val = (GameObject)obj; if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[craft] DoCrafting completed but m_craftRecipe/m_item was null."); return; } string name = ((Object)val).name; Plugin.Log.LogInfo((object)("[craft] subject='" + name + "' (display='" + craftRecipe.m_item.m_itemData?.m_shared?.m_name + "')")); GuidanceDispatcher.Raise(new TriggerEvent { Type = "craft", Subject = name, DisplayName = craftRecipe.m_item.m_itemData?.m_shared?.m_name }); } } [HarmonyPatch(typeof(Player), "Update")] internal static class DistanceTrigger { private const float CheckInterval = 5f; private const float DefaultRadius = 50f; private const string KeyPrefix = "dist_"; private static float _nextCheck; [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 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 player = Player.m_localPlayer; if ((Object)(object)player == (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 list = new List(); foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Steps != null && guidance.Steps.Count > 0) { if (HandleChain(guidance, evt, player, 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 (string.Equals(evt.Type, "kill", StringComparison.OrdinalIgnoreCase) && guidance.Trigger != null && guidance.Trigger.Count > 1) { Plugin.Log.LogDebug((object)("[dispatch] '" + guidance.Id + "' kill count-goal — delegated to count path.")); continue; } if (!RequirementsMet(guidance, player)) { Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: requires not met.")); continue; } if (StopConditionMet(guidance, player)) { Plugin.Log.LogInfo((object)("[dispatch] '" + guidance.Id + "' skipped: stop_when met.")); continue; } if (guidance.Once && SeenTracker.HasFired(player, 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(player, 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, player.GetPlayerName()); MaybeLogQuestStart(player, guidance); 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, player.GetPlayerName()); GuidanceDisplay.Show(guidance, renderedText); if (guidance.Once) { SeenTracker.MarkFired(player, guidance.Id, guidance.Scope); } if (num2 > 0) { SeenTracker.IncrementFireCount(player, guidance.Id); } SeenTracker.MarkCooldown(guidance.Id, guidance.Cooldown, Time.time); DebugFireLog.Record(player.GetPlayerName(), guidance.Id); MaybeLogQuestStart(player, guidance); if (guidance.Announce?.Discord != null) { GuidanceSync.SendAnnounceRequest(guidance.Id, player.GetPlayerName()); } if (guidance.DiscordOnComplete) { GuidanceSync.SendCompleteAnnounce(guidance.Id, player.GetPlayerName()); } if (guidance.Rewards != null && guidance.Rewards.Count > 0) { RewardDispatcher.Grant(guidance.Rewards, player, (string s) => TemplateText(s, evt, player.GetPlayerName())); } 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 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 completedIds) { if (!MatchesTrigger(step.Trigger, evt)) { return false; } if (stepIndex == 0) { MaybeLogQuestStart(player, entry); } FireStepDisplay(entry, step, stepIndex, evt, player); AdvanceChain(entry, stepIndex, player, completedIds); return true; } private static bool HandleCounterStep(GuidanceEntry entry, GuidanceStep step, int stepIndex, TriggerEvent evt, Player player, List completedIds) { if (step.ProgressTrigger == null) { Plugin.Log.LogWarning((object)($"[chain] '{entry.Id}' step {stepIndex} has progress_goal " + "but no progress_trigger — treating as normal step.")); return HandleNormalStep(entry, step, stepIndex, evt, player, completedIds); } int counter = ChainState.GetCounter(player, entry.Id, stepIndex); if (counter < 0) { if (!MatchesTrigger(step.Trigger, evt)) { return false; } if (stepIndex == 0) { MaybeLogQuestStart(player, entry); } int num = 0; if (step.ProgressTrigger != null && string.Equals(step.ProgressTrigger.Type, "item_acquired", StringComparison.OrdinalIgnoreCase)) { num = Math.Min(ItemAcquiredTrigger.CountInInventory(player, step.ProgressTrigger.Item), step.ProgressGoal); } ChainState.SetCounter(player, entry.Id, stepIndex, num); Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' step {stepIndex} counter activated (seed: {num}/{step.ProgressGoal})."); 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 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); DebugFireLog.Record(player.GetPlayerName(), entry.Id); if (entry.DiscordOnComplete) { GuidanceSync.SendCompleteAnnounce(entry.Id, player.GetPlayerName()); } if (entry.Rewards != null && entry.Rewards.Count > 0) { RewardDispatcher.Grant(entry.Rewards, player); } completedIds.Add(entry.Id); } else { ChainState.SetStep(player, entry.Id, num); Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' advanced to step {num}/{entry.Steps.Count}."); 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); if ((Object)(object)Player.m_localPlayer != (Object)null) { DebugFireLog.Record(Player.m_localPlayer.GetPlayerName(), guidanceEntry.Id); } Raise(new TriggerEvent { Type = "entry_finished", Subject = entryId }); } internal static bool CheckGates(GuidanceEntry entry, Player player) { if (!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); DebugFireLog.Record(localPlayer.GetPlayerName(), guidanceEntry.Id); MaybeLogQuestStart(localPlayer, guidanceEntry); Raise(new TriggerEvent { Type = "entry_finished", Subject = entryId }); } internal static void FireEntry(GuidanceEntry entry, TriggerEvent evt) { Player player = Player.m_localPlayer; if ((Object)(object)player == (Object)null || entry == null) { return; } if (SeenTracker.IsGlobalScope(entry.Scope)) { Plugin.Log.LogInfo((object)("[dispatch] FireEntry '" + entry.Id + "' (global) -> server.")); GuidanceSync.SendTriggerGlobal(entry.Id, player.GetPlayerName()); MaybeLogQuestStart(player, entry); SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time); return; } Plugin.Log.LogInfo((object)("[dispatch] FireEntry firing '" + entry.Id + "' via mode '" + entry.Display?.Mode + "'.")); string template = ((!string.IsNullOrEmpty(entry.Message)) ? entry.Message : entry.Display?.Text); string renderedText = TemplateText(template, evt, player.GetPlayerName()); GuidanceDisplay.Show(entry, renderedText); if (entry.Once) { SeenTracker.MarkFired(player, entry.Id, entry.Scope); } int num = entry.Trigger?.MaxFires ?? 0; if (num > 0) { SeenTracker.IncrementFireCount(player, entry.Id); } SeenTracker.MarkCooldown(entry.Id, entry.Cooldown, Time.time); DebugFireLog.Record(player.GetPlayerName(), entry.Id); MaybeLogQuestStart(player, entry); if (entry.Announce?.Discord != null) { GuidanceSync.SendAnnounceRequest(entry.Id, player.GetPlayerName()); } if (entry.DiscordOnComplete) { GuidanceSync.SendCompleteAnnounce(entry.Id, player.GetPlayerName()); } if (entry.Rewards != null && entry.Rewards.Count > 0) { RewardDispatcher.Grant(entry.Rewards, player, (string s) => TemplateText(s, evt, player.GetPlayerName())); } Raise(new TriggerEvent { Type = "entry_finished", Subject = entry.Id }); } private static void MaybeLogQuestStart(Player player, GuidanceEntry entry) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_004d: 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) //IL_006f: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)player == (Object)null) && entry != null && !string.IsNullOrEmpty(entry.Id) && !QuestStartLogState.WasLogged(player, entry.Id)) { QuestStartLogState.MarkLogged(player, entry.Id); Vector3 position = ((Component)player).transform.position; string biome = ((object)player.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); GuidanceSync.SendQuestStartLog(entry.Id, player.GetPlayerName(), biome, position); } } private static bool Matches(GuidanceEntry entry, TriggerEvent evt) { if (entry.Trigger == null) { return false; } return MatchesTrigger(entry.Trigger, evt); } internal static bool MatchesTrigger(TriggerSpec t, TriggerEvent evt) { if (t == null) { return false; } if (!string.Equals(t.Type, evt.Type, StringComparison.OrdinalIgnoreCase)) { return false; } switch (evt.Type.ToLowerInvariant()) { case "craft": return Eq(t.Item, evt.Subject); case "pickup": return Eq(t.Item, evt.Subject); case "kill": return Eq(t.Creature, evt.Subject); case "tamed_creature": return string.IsNullOrEmpty(t.Creature) || Eq(t.Creature, evt.Subject); case "crafting_table_used": return string.IsNullOrEmpty(t.Station) || Eq(t.Station, evt.Subject); case "cooking_used": return string.IsNullOrEmpty(t.Station) || Eq(t.Station, evt.Subject); case "portal_used": return string.IsNullOrEmpty(t.Tag) || Eq(t.Tag, evt.Subject); case "ward_activated": case "sign_read": case "tombstone_picked": case "ship_sailed": return true; case "build": return Eq(t.Piece, evt.Subject); case "biome": return Eq(t.Biome, evt.Subject); case "equip": return Eq(t.Item, evt.Subject); case "boss_defeated": return Eq(t.Creature, evt.Subject); case "item_acquired": return WildcardMatch(t.Item, evt.Subject); case "location_entered": return WildcardMatch(t.Location, evt.Subject); case "distance": return WildcardMatch(t.Location, evt.Subject); case "npc_interacted": case "npc_conversation": return Eq(t.Npc, evt.Subject); case "npc_item_submit": { if (!Eq(t.Npc, evt.Subject)) { return false; } if (string.IsNullOrEmpty(t.Item)) { return true; } string b = ((evt.Extra == null || !evt.Extra.ContainsKey("item")) ? null : evt.Extra["item"]?.ToString()); return Eq(t.Item, b); } case "skill_level": return MatchSkillLevel(t, evt.Subject); case "timed": return Eq(t.Id, evt.Subject); case "entry_finished": return Eq(t.Entry, evt.Subject); case "dvergr_recruited": case "dvergr_duel_won": case "dvergr_rank_changed": case "dvergr_rank_first": return string.IsNullOrEmpty(t.Caste) || Eq(t.Caste, evt.Subject); case "dvergr_level_up": return MatchDvergrLevelUp(t, evt.Subject); case "dvergr_party_duel_won": case "dvergr_party_rank_changed": case "dvergr_party_rank_first": return true; case "dvergr_tournament_joined": case "dvergr_tournament_match": case "dvergr_tournament_won": return string.IsNullOrEmpty(t.Caste) || Eq(t.Caste, 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 MatchDvergrLevelUp(TriggerSpec t, string subject) { if (string.IsNullOrEmpty(subject)) { return false; } int num = subject.IndexOf(':'); if (num < 0) { return false; } string b = subject.Substring(0, num); string s = subject.Substring(num + 1); if (!int.TryParse(s, out var result)) { return false; } if (!string.IsNullOrEmpty(t.Caste) && !Eq(t.Caste, b)) { return false; } if (t.Level > 0 && t.Level != result) { return false; } return true; } private static bool RequirementsMet(GuidanceEntry entry, Player player) { return PrerequisiteChecker.AllSatisfied(entry.Requires, player, Plugin.CurrentConfig); } private static bool StopConditionMet(GuidanceEntry entry, Player player) { if (entry.StopWhen == null || entry.StopWhen.Count == 0) { return false; } foreach (string item in entry.StopWhen) { if (SeenTracker.HasFired(player, item, "player")) { return true; } } return false; } internal static string TemplateText(string template, TriggerEvent evt, string playerName, int step = -1, int total = -1) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(template)) { return template; } string text = ""; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { text = ((object)localPlayer.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); } string newValue = ""; string newValue2 = ""; if (evt != null && string.Equals(evt.Type, "skill_level", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(evt.Subject)) { int num = evt.Subject.IndexOf(':'); if (num >= 0) { newValue = evt.Subject.Substring(0, num); newValue2 = evt.Subject.Substring(num + 1); } } string 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) .Replace("{rank}", Extra("rank")) .Replace("{rating}", Extra("rating")) .Replace("{companionName}", Extra("companionName")) .Replace("{ownerName}", Extra("ownerName")) .Replace("{partyName}", Extra("partyName")) .Replace("{winSize}", Extra("winSize")) .Replace("{opponentOwner}", Extra("opponentOwner")) .Replace("{mvpCaste}", Extra("mvpCaste")) .Replace("{round}", Extra("round")) .Replace("{opponent}", Extra("opponent")) .Replace("{mode}", Extra("mode")) .Replace("{bracketSize}", Extra("bracketSize")) .Replace("{caste}", evt?.Subject ?? ""); if (step >= 0) { text2 = text2.Replace("{step}", step.ToString()); } if (total >= 0) { text2 = text2.Replace("{total}", total.ToString()); } return text2; string Extra(string key) { object value; return (evt?.Extra == null || !evt.Extra.TryGetValue(key, out value)) ? "" : (value?.ToString() ?? ""); } } 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 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()?.m_itemData?.m_shared?.m_name; GuidanceDispatcher.Raise(new TriggerEvent { Type = "item_acquired", Subject = text, DisplayName = displayName }); CheckCountGoals(text, displayName); } } } internal static List 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 { 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 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 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 goals) { if ((Object)(object)player == (Object)null || goals == null || goals.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (ItemGoalSpec goal in goals) { int value = Math.Min(CountInInventory(player, goal.Item), goal.Count); if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(goal.Item).Append(": ").Append(value) .Append('/') .Append(goal.Count); } return stringBuilder.ToString(); } internal static bool ItemWildcardMatch(string pattern, string value) { if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value)) { return false; } if (pattern.EndsWith("*")) { string value2 = pattern.Substring(0, pattern.Length - 1); return value.StartsWith(value2, StringComparison.OrdinalIgnoreCase); } return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase); } } [HarmonyPatch(typeof(InventoryGui), "DoCrafting")] internal static class ItemAcquiredCraftPatch { [HarmonyPostfix] private static void Postfix(InventoryGui __instance, Player player) { if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } Recipe craftRecipe = __instance.m_craftRecipe; if (!((Object)(object)craftRecipe?.m_item == (Object)null)) { string text = TriggerUtils.NormalizePrefabName(((Object)((Component)craftRecipe.m_item).gameObject).name); if (!string.IsNullOrEmpty(text)) { string displayName = craftRecipe.m_item.m_itemData?.m_shared?.m_name; ItemAcquiredTrigger.CheckCountGoals(text, displayName); } } } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class KillTrigger { [HarmonyPostfix] private static void Postfix(Character __instance) { if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } object obj; if (__instance == null) { obj = null; } else { HitData lastHit = __instance.m_lastHit; obj = ((lastHit != null) ? lastHit.GetAttacker() : null); } Character val = (Character)obj; if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer)) { GameObject gameObject = ((Component)__instance).gameObject; string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); if (!string.IsNullOrEmpty(text)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "kill", Subject = text, DisplayName = __instance.m_name }); KillCountTracker.CheckKillCount(text, __instance.m_name); } } } } internal static class KillCountTracker { internal const float ShareProgressRadius = 50f; internal static void CheckKillCount(string creature, string displayName) { //IL_0108: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { TriggerSpec trigger = guidance.Trigger; if (trigger != null && string.Equals(trigger.Type, "kill", StringComparison.OrdinalIgnoreCase) && trigger.Count > 1 && !string.IsNullOrEmpty(trigger.Creature) && string.Equals(trigger.Creature, creature, StringComparison.OrdinalIgnoreCase) && GuidanceDispatcher.CheckGates(guidance, localPlayer)) { ApplyIncrement(guidance, localPlayer, creature, displayName); if (trigger.ShareProgress) { GuidanceSync.SendShareKillProgress(guidance.Id, localPlayer.GetPlayerName(), ((Component)localPlayer).transform.position); } } } } private static void ApplyIncrement(GuidanceEntry entry, Player player, string creature, string displayName) { int count = entry.Trigger.Count; int num = KillCountState.Get(player, entry.Id) + 1; Plugin.Log.LogInfo((object)$"[kill] '{entry.Id}' progress {num}/{count} ({creature})."); if (num >= count) { KillCountState.Clear(player, entry.Id); Plugin.Log.LogInfo((object)$"[kill] '{entry.Id}' complete ({count}/{count}) — firing."); GuidanceDispatcher.FireEntry(entry, new TriggerEvent { Type = "kill", Subject = creature, DisplayName = displayName }); GuidanceHudTracker.Instance?.FlashCompletion(entry.Id); } else { KillCountState.Set(player, entry.Id, num); string arg = ((!string.IsNullOrEmpty(entry.Title)) ? entry.Title : (displayName ?? creature)); ((Character)player).Message((MessageType)2, $"{arg}: {num}/{count}", 0, (Sprite)null); GuidanceHudTracker.Instance?.Refresh(fromProgress: true); } } internal static void ApplySharedIncrement(string entryId, Player localPlayer, Vector3 killerPosition) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(entryId) || (Object)(object)localPlayer == (Object)null) { return; } Vector3 val = ((Component)localPlayer).transform.position - killerPosition; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (!(sqrMagnitude > 2500f)) { GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry?.Trigger != null && string.Equals(guidanceEntry.Trigger.Type, "kill", StringComparison.OrdinalIgnoreCase) && guidanceEntry.Trigger.Count > 1 && GuidanceDispatcher.CheckGates(guidanceEntry, localPlayer)) { Plugin.Log.LogInfo((object)("[kill] '" + entryId + "' shared credit from nearby party kill.")); ApplyIncrement(guidanceEntry, localPlayer, guidanceEntry.Trigger.Creature, guidanceEntry.Trigger.Creature); } } } } [HarmonyPatch(typeof(Player), "Update")] internal static class LocationEnteredTrigger { private const float CheckInterval = 5f; private const float DetectRadius = 40f; private const string KeyPrefix = "loc_"; private static float _nextCheck; [HarmonyPostfix] private static void Postfix(Player __instance) { //IL_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 hashSet = new HashSet(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()); } 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 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; } internal static List FindAllEntries(string npcSubject, Player player) { List list = new List(); if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null) { return list; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return list; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Trigger != null && string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) && string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) && GuidanceDispatcher.CheckGates(guidance, player)) { list.Add(guidance); } } return list; } } [HarmonyPatch(typeof(Trader), "GetHoverText")] internal static class TraderHoverTextPatch { [HarmonyPostfix] private static void Postfix(Trader __instance, ref string __result) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } GameObject gameObject = ((Component)__instance).gameObject; string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); GuidanceEntry guidanceEntry = NpcConversationTrigger.FindEntry(npcSubject, localPlayer); if (guidanceEntry?.HoverText?.Default != null) { __result = __result + "\n" + guidanceEntry.HoverText.Default; return; } GuidanceEntry guidanceEntry2 = FindFiredEntryWithAfterFireHover(npcSubject, localPlayer); if (guidanceEntry2 != null) { __result = __result + "\n" + guidanceEntry2.HoverText.AfterFire; } else if (guidanceEntry != null) { __result += "\n[Hold E] Quest"; } } private static GuidanceEntry FindFiredEntryWithAfterFireHover(string npcSubject, Player player) { if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null) { return null; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return null; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) || !string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(guidance.HoverText?.AfterFire) || !guidance.Once || !SeenTracker.HasFired(player, guidance.Id, guidance.Scope)) { continue; } return guidance; } return null; } } 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(); } } 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); List list = NpcConversationTrigger.FindAllEntries(npcSubject, localPlayer); if (list.Count == 0) { if ((Object)(object)StoreGui.instance != (Object)null) { StoreGui.instance.Show(pendingTrader2); } } else if (list.Count == 1) { GuidanceEntry guidanceEntry = list[0]; string template = ((!string.IsNullOrEmpty(guidanceEntry.Message)) ? guidanceEntry.Message : guidanceEntry.Display?.Text); string renderedText = GuidanceDispatcher.TemplateText(template, null, localPlayer.GetPlayerName()); GuidanceDisplay.Show(guidanceEntry, renderedText); } else { NpcConversationPanel.Get().OpenSelection(pendingTrader2.m_name, list, npcSubject); } } } 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 { { "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[1-8] $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[1-8] $npc_giveitem") : "\n[1-8] $npc_giveitem"); __result += text; } } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathTrigger { [HarmonyPostfix] private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "player_death", Subject = "" }); } } } [HarmonyPatch(typeof(TeleportWorld), "Teleport")] internal static class PortalUsedTrigger { [HarmonyPostfix] private static void Postfix(TeleportWorld __instance, Player player) { if (!((Object)(object)Player.m_localPlayer == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer)) { string text = __instance.GetText() ?? ""; GuidanceDispatcher.Raise(new TriggerEvent { Type = "portal_used", Subject = text, DisplayName = text }); } } } [HarmonyPatch(typeof(ShipControlls), "Interact")] internal static class ShipSailedTrigger { [HarmonyPostfix] private static void Postfix(ShipControlls __instance, Humanoid character, bool repeat) { if (!repeat) { Player val = (Player)(object)((character is Player) ? character : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && !((Object)(object)((Character)val).GetStandingOnShip() != (Object)(object)__instance.m_ship)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "ship_sailed" }); } } } } [HarmonyPatch(typeof(Sign), "Interact")] internal static class SignReadTrigger { [HarmonyPostfix] private static void Postfix(bool hold, bool __result) { if (!((Object)(object)Player.m_localPlayer == (Object)null) && !hold && __result) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "sign_read" }); } } } [HarmonyPatch(typeof(Skills), "RaiseSkill")] internal static class SkillLevelTrigger { private static int _prevLevel; [HarmonyPrefix] private static void Prefix(Skills __instance, SkillType skillType) { //IL_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(text, ignoreCase: true, out SkillType result)) { int num2 = (int)localPlayer.m_skills.GetSkillLevel(result); if (num2 >= num) { Plugin.Log.LogInfo((object)$"[skill_level] Login scan: {text}:{num} (player {num2}) — raising."); GuidanceDispatcher.Raise(new TriggerEvent { Type = "skill_level", Subject = $"{text}:{num}" }); } } } } private static void CollectThreshold(TriggerSpec t, List<(string, int)> list) { if (t != null && string.Equals(t.Type, "skill_level", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(t.Skill) && t.Level > 0) { list.Add((t.Skill, t.Level)); } } } [HarmonyPatch(typeof(Tameable), "Tame")] internal static class TamedCreatureTrigger { [HarmonyPostfix] private static void Postfix(Tameable __instance) { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { GameObject gameObject = ((Component)__instance).gameObject; string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); if (!string.IsNullOrEmpty(text)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "tamed_creature", Subject = text, DisplayName = __instance.m_character?.m_name }); } } } } internal static class TimedTrigger { private static readonly Dictionary _coroutines = new Dictionary(); 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(); } private static IEnumerator TimerRoutine(string entryId, string triggerId, float interval, bool isGlobal) { yield return (object)new WaitForSeconds(interval); while (true) { Plugin.Log.LogInfo((object)("[timed] '" + entryId + "' firing.")); if (isGlobal && Application.isBatchMode) { GuidanceSync.BroadcastTimedGuidance(entryId); } else { GuidanceDispatcher.Raise(new TriggerEvent { Type = "timed", Subject = triggerId }); } yield return (object)new WaitForSeconds(interval); } } 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 TimeTrigger { private const float PollInterval = 30f; private static Coroutine _coroutine; public static void Start() { if (_coroutine == null) { _coroutine = ((MonoBehaviour)Plugin.Instance).StartCoroutine(PollRoutine()); } } private static IEnumerator PollRoutine() { WaitForSeconds wait = new WaitForSeconds(30f); while (true) { yield return wait; Tick(); } } private static void Tick() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return; } DateTime utcNow = DateTime.UtcNow; EnvMan instance = EnvMan.instance; foreach (GuidanceEntry guidance in currentConfig.Guidances) { TriggerSpec trigger = guidance.Trigger; if (trigger == null) { continue; } bool flag; switch (trigger.Type?.ToLowerInvariant()) { case "time_of_day": if ((Object)(object)instance == (Object)null) { continue; } flag = MatchesTimeOfDay(instance, trigger); break; case "day_number": if ((Object)(object)instance == (Object)null) { continue; } flag = instance.GetDay() == ParseDay(trigger.Day) && instance.GetDayFraction() >= 0.25f; break; case "real_world_time": flag = utcNow.Hour == trigger.UtcHour && utcNow.Minute == trigger.UtcMinute; break; case "day_of_week": flag = string.Equals(utcNow.DayOfWeek.ToString(), trigger.Day, StringComparison.OrdinalIgnoreCase); break; default: continue; } if (flag && GuidanceDispatcher.CheckGates(guidance, localPlayer)) { Plugin.Log.LogInfo((object)("[time] '" + guidance.Id + "' condition matched (" + trigger.Type + ").")); GuidanceDispatcher.FireEntry(guidance, new TriggerEvent { Type = trigger.Type }); } } } private static bool MatchesTimeOfDay(EnvMan envMan, TriggerSpec t) { float num = Mathf.Abs(envMan.GetDayFraction() - t.GameTimeFraction); num = Mathf.Min(num, 1f - num); return num <= t.Window; } private static int ParseDay(string s) { int result; return int.TryParse(s, out result) ? result : (-1); } } [HarmonyPatch(typeof(TombStone), "Interact")] internal static class TombstonePickedTrigger { [HarmonyPostfix] private static void Postfix(bool hold, bool __result) { if (!((Object)(object)Player.m_localPlayer == (Object)null) && !hold && __result) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "tombstone_picked" }); } } } 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; } } [HarmonyPatch(typeof(PrivateArea), "Interact")] internal static class WardActivatedTrigger { [HarmonyPostfix] private static void Postfix(bool hold, bool __result) { if (!((Object)(object)Player.m_localPlayer == (Object)null) && !hold && __result) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "ward_activated" }); } } } } 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 list = new List(); 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 list = new List(); 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 ConversationNodeState { private const string Prefix = "VSG.cn."; public static string GetCurrentNode(Player player, string entryId) { if (player?.m_customData == null || string.IsNullOrEmpty(entryId)) { return null; } string value; return player.m_customData.TryGetValue("VSG.cn." + entryId, out value) ? value : null; } public static void SetCurrentNode(Player player, string entryId, string nodeId) { if (player?.m_customData != null && !string.IsNullOrEmpty(entryId)) { player.m_customData["VSG.cn." + entryId] = nodeId; } } public static void Clear(Player player, string entryId) { player?.m_customData?.Remove("VSG.cn." + entryId); } public static void ResetAll(Player player) { if (player?.m_customData == null) { return; } List list = new List(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.cn.")) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } } public static class DebugFireLog { private const int MaxEntries = 10; private static readonly Dictionary> _log = new Dictionary>(); public static void Record(string playerName, string id) { if (!string.IsNullOrEmpty(playerName) && !string.IsNullOrEmpty(id)) { if (!_log.TryGetValue(playerName, out List<(string, DateTime)> value)) { value = new List<(string, DateTime)>(); _log[playerName] = value; } value.Add((id, DateTime.Now)); if (value.Count > 10) { value.RemoveAt(0); } } } public static IReadOnlyList<(string Id, DateTime When)> Get(string playerName) { if (string.IsNullOrEmpty(playerName) || !_log.TryGetValue(playerName, out List<(string, DateTime)> value)) { return Array.Empty<(string, DateTime)>(); } return value; } } 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 list = new List(); 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 KillCountState { private const string ProgressPrefix = "VSG.kc."; private static string Key(string entryId) { return "VSG.kc." + 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 list = new List(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.kc.")) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } } public static class PrerequisiteChecker { public static bool AllSatisfied(List 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) != true) { Plugin.Log.LogWarning((object)("[prereq] '" + reqId + "' not found in config — treating as unsatisfied.")); } return false; } } public static class QuestStartLogState { private const string LoggedPrefix = "VSG.qs."; private static string Key(string entryId) { return "VSG.qs." + entryId; } public static bool WasLogged(Player player, string entryId) { if (player?.m_customData == null || string.IsNullOrEmpty(entryId)) { return false; } return player.m_customData.ContainsKey(Key(entryId)); } public static void MarkLogged(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 list = new List(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.qs.")) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } } 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 CooldownExpiry = new Dictionary(); 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 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 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 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 GetFiredIds(Player player) { if ((Object)(object)player == (Object)null) { return (IReadOnlyCollection)(object)Array.Empty(); } return GetSet(player); } private static HashSet GetSet(Player player) { if (!player.m_customData.TryGetValue("VSG.fired", out var value) || string.IsNullOrEmpty(value)) { return new HashSet(); } return new HashSet(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 list = new List(); 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); } } } public static class TrackedQuestState { private const string TrackedKey = "VSG.trk"; private const string PosKey = "VSG.tpos"; public static bool IsTracked(Player player, string entryId) { if (player?.m_customData == null || string.IsNullOrEmpty(entryId)) { return false; } return GetSet(player).Contains(entryId); } public static bool SetTracked(Player player, string entryId, bool tracked) { if (player?.m_customData == null || string.IsNullOrEmpty(entryId)) { return false; } HashSet set = GetSet(player); bool flag = (tracked ? set.Add(entryId) : set.Remove(entryId)); if (flag) { Save(player, set); } return flag; } public static IReadOnlyCollection GetAll(Player player) { if ((Object)(object)player == (Object)null) { return (IReadOnlyCollection)(object)Array.Empty(); } return GetSet(player); } public static void Clear(Player player, string entryId) { if (player?.m_customData != null && !string.IsNullOrEmpty(entryId)) { HashSet set = GetSet(player); if (set.Remove(entryId)) { Save(player, set); } } } public static void ResetAll(Player player) { player?.m_customData?.Remove("VSG.trk"); } public static Vector2? GetPosition(Player player) { //IL_00bf: Unknown result type (might be due to invalid IL or missing references) if (player?.m_customData == null) { return null; } if (!player.m_customData.TryGetValue("VSG.tpos", out var value) || string.IsNullOrEmpty(value)) { return null; } string[] array = value.Split(new char[1] { ',' }); if (array.Length != 2) { return null; } if (float.TryParse(array[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && float.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { return new Vector2(result, result2); } return null; } public static void SetPosition(Player player, Vector2 pos) { if (player?.m_customData != null) { player.m_customData["VSG.tpos"] = pos.x.ToString("0.##", CultureInfo.InvariantCulture) + "," + pos.y.ToString("0.##", CultureInfo.InvariantCulture); } } private static HashSet GetSet(Player player) { if (!player.m_customData.TryGetValue("VSG.trk", out var value) || string.IsNullOrEmpty(value)) { return new HashSet(); } return new HashSet(from s in value.Split(new char[1] { ',' }) where !string.IsNullOrEmpty(s) select s); } private static void Save(Player player, HashSet set) { if (set.Count == 0) { player.m_customData.Remove("VSG.trk"); } else { player.m_customData["VSG.trk"] = string.Join(",", set); } } } } namespace ValheimServerGuide.Rewards { public static class RewardDispatcher { public static void Grant(List rewards, Player player, Func expand = null) { 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; case "map_pin": GrantMapPin(reward); break; case "location_pin": GrantLocationPin(reward, player); break; case "unlock_recipe": GrantUnlockRecipe(reward, player); break; case "spawn_creature": GrantSpawnCreature(reward, player); break; case "set_global_key": GrantSetGlobalKey(reward); break; case "remove_global_key": GrantRemoveGlobalKey(reward); break; case "set_player_key": GrantSetPlayerKey(reward, player); break; case "remove_player_key": GrantRemovePlayerKey(reward, player); break; case "weather": GrantWeather(reward); break; case "chat_message": GrantChatMessage(reward, player, expand); break; case "teleport": GrantTeleport(reward, player); break; case "rename_player": GrantRenamePlayer(reward, player); break; case "discord": GrantDiscord(reward, player, expand); break; default: Plugin.Log.LogWarning((object)("[rewards] Unknown reward type '" + reward.Type + "' — skipping.")); break; } } RewardNotification.Show(rewards); } public static void ValidateRewards(List 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 prefab2 = ZNetScene.instance.GetPrefab(reward.Item); if ((Object)(object)prefab2 == (Object)null) { Plugin.Log.LogWarning((object)("[rewards] " + context + ": item prefab '" + reward.Item + "' not found.")); } else if ((Object)(object)prefab2.GetComponent() == (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(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; } case "unlock_recipe": if (!string.IsNullOrEmpty(reward.Recipe) && (Object)(object)ObjectDB.instance != (Object)null && !ObjectDB.instance.m_recipes.Exists((Recipe r) => (Object)(object)r != (Object)null && ((Object)r).name == reward.Recipe)) { Plugin.Log.LogWarning((object)("[rewards] " + context + ": recipe '" + reward.Recipe + "' not found.")); } break; case "spawn_creature": if (!string.IsNullOrEmpty(reward.Prefab) && (Object)(object)ZNetScene.instance != (Object)null) { GameObject prefab = ZNetScene.instance.GetPrefab(reward.Prefab); if ((Object)(object)prefab == (Object)null) { Plugin.Log.LogWarning((object)("[rewards] " + context + ": creature prefab '" + reward.Prefab + "' not found.")); } } 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(); 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(val, val3, Quaternion.identity); ItemDrop component2 = val4.GetComponent(); 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)" : "") + ".")); } private static void GrantMapPin(RewardSpec reward) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Minimap.instance == (Object)null) { Plugin.Log.LogWarning((object)"[rewards] map_pin: Minimap.instance null — skipping."); return; } Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor(reward.X, 0f, reward.Z); string text = (string.IsNullOrEmpty(reward.Name) ? "Marker" : reward.Name); Minimap.instance.AddPin(val, ParseIcon(reward.Icon), text, true, false, 0L, default(PlatformUserID)); Plugin.Log.LogInfo((object)$"[rewards] Added map pin '{text}' at ({reward.X}, {reward.Z})."); } private static void GrantLocationPin(RewardSpec reward, Player player) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(reward.Location)) { Plugin.Log.LogWarning((object)"[rewards] location_pin reward missing 'location' field — skipping."); return; } if ((Object)(object)ZoneSystem.instance == (Object)null || (Object)(object)Minimap.instance == (Object)null) { Plugin.Log.LogWarning((object)"[rewards] location_pin: ZoneSystem/Minimap not ready — skipping."); return; } LocationInstance val = default(LocationInstance); if (!ZoneSystem.instance.FindClosestLocation(reward.Location, ((Component)player).transform.position, ref val)) { Plugin.Log.LogWarning((object)("[rewards] location_pin: no discovered instance of '" + reward.Location + "' found — skipping.")); return; } string text = (string.IsNullOrEmpty(reward.PinName) ? reward.Location : reward.PinName); Minimap.instance.AddPin(val.m_position, ParseIcon(reward.Icon), text, true, false, 0L, default(PlatformUserID)); Plugin.Log.LogInfo((object)("[rewards] Added location pin '" + text + "' for '" + reward.Location + "'.")); } private static PinType ParseIcon(string icon) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(icon) && Enum.TryParse(icon, ignoreCase: true, out PinType result)) { return result; } return (PinType)3; } private static void GrantUnlockRecipe(RewardSpec reward, Player player) { if (string.IsNullOrEmpty(reward.Recipe)) { Plugin.Log.LogWarning((object)"[rewards] unlock_recipe reward missing 'recipe' field — skipping."); return; } Recipe val = ObjectDB.instance?.m_recipes.Find((Recipe r) => (Object)(object)r != (Object)null && ((Object)r).name == reward.Recipe); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("[rewards] unlock_recipe: recipe '" + reward.Recipe + "' not found — skipping.")); return; } player.AddKnownRecipe(val); Plugin.Log.LogInfo((object)("[rewards] Unlocked recipe '" + reward.Recipe + "'.")); } private static void GrantSpawnCreature(RewardSpec reward, Player player) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(reward.Prefab)) { Plugin.Log.LogWarning((object)"[rewards] spawn_creature reward missing 'prefab' field — skipping."); return; } ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab(reward.Prefab) : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("[rewards] spawn_creature: prefab '" + reward.Prefab + "' not found — skipping.")); return; } int num = Mathf.Max(1, reward.Count); for (int i = 0; i < num; i++) { Vector3 val2 = Quaternion.Euler(0f, (float)i * (360f / (float)num), 0f) * Vector3.forward * 2.5f; Vector3 val3 = ((Component)player).transform.position + val2; GameObject val4 = Object.Instantiate(val, val3, Quaternion.identity); Character component = val4.GetComponent(); if ((Object)(object)component != (Object)null && reward.Tamed) { component.SetTamed(true); } } Plugin.Log.LogInfo((object)($"[rewards] Spawned '{reward.Prefab}' x{num}" + (reward.Tamed ? " (tamed)" : "") + ".")); } private static void GrantSetGlobalKey(RewardSpec reward) { if (string.IsNullOrEmpty(reward.Key)) { Plugin.Log.LogWarning((object)"[rewards] set_global_key reward missing 'key' field — skipping."); return; } ZoneSystem instance = ZoneSystem.instance; if (instance != null) { instance.SetGlobalKey(reward.Key); } Plugin.Log.LogInfo((object)("[rewards] Set global key '" + reward.Key + "'.")); } private static void GrantRemoveGlobalKey(RewardSpec reward) { if (string.IsNullOrEmpty(reward.Key)) { Plugin.Log.LogWarning((object)"[rewards] remove_global_key reward missing 'key' field — skipping."); return; } ZoneSystem instance = ZoneSystem.instance; if (instance != null) { instance.RemoveGlobalKey(reward.Key); } Plugin.Log.LogInfo((object)("[rewards] Removed global key '" + reward.Key + "'.")); } private static void GrantSetPlayerKey(RewardSpec reward, Player player) { if (string.IsNullOrEmpty(reward.Key)) { Plugin.Log.LogWarning((object)"[rewards] set_player_key reward missing 'key' field — skipping."); return; } ((Humanoid)player).AddUniqueKey(reward.Key); Plugin.Log.LogInfo((object)("[rewards] Set player key '" + reward.Key + "'.")); } private static void GrantRemovePlayerKey(RewardSpec reward, Player player) { if (string.IsNullOrEmpty(reward.Key)) { Plugin.Log.LogWarning((object)"[rewards] remove_player_key reward missing 'key' field — skipping."); return; } ((Humanoid)player).RemoveUniqueKey(reward.Key); Plugin.Log.LogInfo((object)("[rewards] Removed player key '" + reward.Key + "'.")); } private static void GrantWeather(RewardSpec reward) { if (string.IsNullOrEmpty(reward.Preset)) { Plugin.Log.LogWarning((object)"[rewards] weather reward missing 'preset' field — skipping."); return; } if ((Object)(object)EnvMan.instance == (Object)null) { Plugin.Log.LogWarning((object)"[rewards] weather: EnvMan.instance null — skipping."); return; } if (!EnvMan.instance.m_environments.Exists((EnvSetup e) => e != null && e.m_name == reward.Preset)) { Plugin.Log.LogWarning((object)("[rewards] weather: preset '" + reward.Preset + "' not recognised by EnvMan — forcing anyway.")); } EnvMan.instance.SetForceEnvironment(reward.Preset); float num = ((reward.Duration > 0f) ? reward.Duration : 60f); ((MonoBehaviour)Plugin.Instance).StartCoroutine(ClearForcedWeatherAfter(reward.Preset, num)); Plugin.Log.LogInfo((object)$"[rewards] Forced weather '{reward.Preset}' for {num}s."); } private static IEnumerator ClearForcedWeatherAfter(string preset, float duration) { yield return (object)new WaitForSeconds(duration); if ((Object)(object)EnvMan.instance != (Object)null && EnvMan.instance.m_forceEnv == preset) { EnvMan.instance.SetForceEnvironment(""); } } private static void GrantChatMessage(RewardSpec reward, Player player, Func expand = null) { if (string.IsNullOrEmpty(reward.Message)) { Plugin.Log.LogWarning((object)"[rewards] chat_message reward missing 'message' field — skipping."); return; } if ((Object)(object)Chat.instance == (Object)null) { Plugin.Log.LogWarning((object)"[rewards] chat_message: Chat.instance null — skipping."); return; } string text = ExpandPlayerName((expand != null) ? expand(reward.Message) : reward.Message, player); ((Terminal)Chat.instance).AddString(text); } private static void GrantTeleport(RewardSpec reward, Player player) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor(reward.X, ((Component)player).transform.position.y, reward.Z); ((Character)player).TeleportTo(val, ((Component)player).transform.rotation, true); Plugin.Log.LogInfo((object)$"[rewards] Teleported player to ({reward.X}, {reward.Z})."); } private static void GrantRenamePlayer(RewardSpec reward, Player player) { if (string.IsNullOrEmpty(reward.Suffix)) { Plugin.Log.LogWarning((object)"[rewards] rename_player reward missing 'suffix' field — skipping."); return; } ZNetView component = ((Component)player).GetComponent(); ZDO val = (((Object)(object)component != (Object)null) ? component.GetZDO() : null); if (val == null) { Plugin.Log.LogWarning((object)"[rewards] rename_player: player ZDO not available — skipping."); return; } string playerName = player.GetPlayerName(); string text = playerName + " " + reward.Suffix; val.Set(ZDOVars.s_playerName, text); Plugin.Log.LogInfo((object)("[rewards] Renamed player to '" + text + "'.")); } private static void GrantDiscord(RewardSpec reward, Player player, Func expand = null) { if (string.IsNullOrEmpty(reward.Message)) { Plugin.Log.LogWarning((object)"[rewards] discord reward missing 'message' field — skipping."); return; } string message = ExpandPlayerName((expand != null) ? expand(reward.Message) : reward.Message, player); GuidanceSync.SendRewardDiscord(message); } private static string ExpandPlayerName(string template, Player player) { return template.Replace("{player_name}", ((player != null) ? player.GetPlayerName() : null) ?? ""); } 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(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 rewards) { if (rewards == null || rewards.Count == 0 || (Object)(object)MessageHud.instance == (Object)null) { return; } List list = new List(); 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) { switch (reward.Type?.ToLowerInvariant()) { case "item": return DescribeItem(reward); case "skill_exp": return DescribeSkillExp(reward); case "skill_level": return DescribeSkillLevel(reward); case "buff": return DescribeBuff(reward); case "map_pin": return string.IsNullOrEmpty(reward.Name) ? null : ("Map pin: " + reward.Name); case "location_pin": return string.IsNullOrEmpty(reward.PinName ?? reward.Location) ? null : ("Map pin: " + (reward.PinName ?? reward.Location)); case "unlock_recipe": return string.IsNullOrEmpty(reward.Recipe) ? null : ("Recipe: " + reward.Recipe); case "spawn_creature": return string.IsNullOrEmpty(reward.Prefab) ? null : $"{reward.Prefab} x{Mathf.Max(1, reward.Count)}"; case "set_global_key": case "remove_global_key": case "set_player_key": case "remove_player_key": return null; case "weather": return string.IsNullOrEmpty(reward.Preset) ? null : ("Weather: " + reward.Preset); case "teleport": return "Teleported"; case "rename_player": return string.IsNullOrEmpty(reward.Suffix) ? null : ("Title: " + reward.Suffix); case "chat_message": case "discord": return null; default: return 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() : 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); } } } } private const string RpcName = "VSG_SyncConfig"; private const string RpcTriggerGlobal = "VSG_TriggerGlobal"; private const string RpcPlayGlobal = "VSG_PlayGlobal"; private const string RpcAnnounce = "VSG_AnnounceRequest"; private const string RpcAdminResetGlobal = "VSG_AdminResetGlobal"; private const string RpcTimedGuidance = "VSG_TimedGuidance"; private const string RpcChainStepUpdate = "VSG_ChainStepUpdate"; private const string RpcChainStateRequest = "VSG_ChainStateRequest"; private const string RpcChainStatePush = "VSG_ChainStatePush"; private const string RpcCompleteAnnounce = "VSG_CompleteAnnounce"; private const string RpcRewardDiscord = "VSG_RewardDiscord"; private const string RpcShareKillProgress = "VSG_ShareKillProgress"; private const string RpcQuestStartLog = "VSG_QuestStartLog"; private const string RpcAdminPlayerListReq = "VSG_APListReq"; private const string RpcAdminPlayerListFwd = "VSG_APListFwd"; private const string RpcAdminPlayerListResp = "VSG_APListResp"; private const string RpcAdminPlayerListOut = "VSG_APListOut"; private const string RpcAdminPlayerResetReq = "VSG_APResetReq"; private const string RpcAdminPlayerResetFwd = "VSG_APResetFwd"; private const string RpcAdminPlayerResetAck = "VSG_APResetAck"; private const string RpcAdminPlayerResetOut = "VSG_APResetOut"; private static bool _registered; private static bool _rpcsBound; private static readonly Dictionary> _playerChainData = new Dictionary>(); public static void Register() { if (!_registered) { _registered = true; } } private static void EnsureRegistered() { if (!_rpcsBound && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register("VSG_SyncConfig", (Action)OnReceive); ZRoutedRpc.instance.Register("VSG_TriggerGlobal", (Action)OnTriggerGlobal); ZRoutedRpc.instance.Register("VSG_PlayGlobal", (Action)OnPlayGlobal); ZRoutedRpc.instance.Register("VSG_AnnounceRequest", (Action)OnAnnounceRequest); ZRoutedRpc.instance.Register("VSG_AdminResetGlobal", (Action)OnAdminResetGlobal); ZRoutedRpc.instance.Register("VSG_TimedGuidance", (Action)OnTimedGuidance); ZRoutedRpc.instance.Register("VSG_ChainStepUpdate", (Action)OnChainStepUpdate); ZRoutedRpc.instance.Register("VSG_ChainStateRequest", (Action)OnChainStateRequest); ZRoutedRpc.instance.Register("VSG_ChainStatePush", (Action)OnChainStatePush); ZRoutedRpc.instance.Register("VSG_CompleteAnnounce", (Action)OnCompleteAnnounce); ZRoutedRpc.instance.Register("VSG_RewardDiscord", (Action)OnRewardDiscord); ZRoutedRpc.instance.Register("VSG_ShareKillProgress", (Action)OnShareKillProgress); ZRoutedRpc.instance.Register("VSG_QuestStartLog", (Action)OnQuestStartLog); ZRoutedRpc.instance.Register("VSG_APListReq", (Action)OnAdminPlayerListReq); ZRoutedRpc.instance.Register("VSG_APListFwd", (Action)OnAdminPlayerListFwd); ZRoutedRpc.instance.Register("VSG_APListResp", (Action)OnAdminPlayerListResp); ZRoutedRpc.instance.Register("VSG_APListOut", (Action)OnAdminPlayerListOut); ZRoutedRpc.instance.Register("VSG_APResetReq", (Action)OnAdminPlayerResetReq); ZRoutedRpc.instance.Register("VSG_APResetFwd", (Action)OnAdminPlayerResetFwd); ZRoutedRpc.instance.Register("VSG_APResetAck", (Action)OnAdminPlayerResetAck); ZRoutedRpc.instance.Register("VSG_APResetOut", (Action)OnAdminPlayerResetOut); _rpcsBound = true; Plugin.Log.LogInfo((object)"RPCs registered with ZRoutedRpc."); } } private static void OnReceive(long sender, ZPackage pkg) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return; } string yaml = pkg.ReadString(); try { GuidanceConfig guidanceConfig = (Plugin.CurrentConfig = Deserialize(yaml)); GuidanceDisplay.RegisterTutorials(guidanceConfig); Plugin.Log.LogInfo((object)$"Received guidance config from server: {guidanceConfig.Guidances.Count} entries."); } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to apply synced config: " + ex.Message)); } } public static void BroadcastToClients(GuidanceConfig config) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { ZPackage val = new ZPackage(); val.Write(Serialize(config)); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "VSG_SyncConfig", new object[1] { val }); } } public static void SendToPeer(long peerId, GuidanceConfig config) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { ZPackage val = new ZPackage(); val.Write(Serialize(config)); ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "VSG_SyncConfig", new object[1] { val }); } } public static void SendTriggerGlobal(string entryId, string playerName) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_TriggerGlobal", new object[2] { entryId, playerName ?? "" }); } } private static void OnTriggerGlobal(long sender, string entryId, string playerName) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry == null) { Plugin.Log.LogWarning((object)$"[global] server got trigger for unknown id '{entryId}' from {sender}."); return; } if (!SeenTracker.IsGlobalScope(guidanceEntry.Scope)) { Plugin.Log.LogWarning((object)("[global] server got trigger for non-global id '" + entryId + "'; ignoring.")); return; } if (guidanceEntry.Once && SeenTracker.HasFired(null, guidanceEntry.Id, guidanceEntry.Scope)) { Plugin.Log.LogInfo((object)("[global] '" + entryId + "' already fired world-wide; ignoring duplicate trigger from " + playerName + ".")); return; } SeenTracker.MarkFired(null, guidanceEntry.Id, guidanceEntry.Scope); Plugin.Log.LogInfo((object)("[global] '" + entryId + "' marked & broadcasting (triggered by " + playerName + ").")); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "VSG_PlayGlobal", new object[2] { entryId, playerName ?? "" }); if (guidanceEntry.Announce?.Discord != null) { DiscordAnnouncer.Announce(guidanceEntry, playerName); } } private static void OnPlayGlobal(long sender, string entryId, string playerName) { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { GuidanceDispatcher.PlayGlobalReceived(entryId, playerName); } } public static void SendAnnounceRequest(string entryId, string playerName) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_AnnounceRequest", new object[2] { entryId, playerName ?? "" }); } } private static void OnAnnounceRequest(long sender, string entryId, string playerName) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry?.Announce?.Discord != null) { DiscordAnnouncer.Announce(guidanceEntry, playerName); } } } public static void SendCompleteAnnounce(string entryId, string playerName) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_CompleteAnnounce", new object[2] { entryId ?? "", playerName ?? "" }); } } private static void OnCompleteAnnounce(long sender, string entryId, string playerName) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry != null && guidanceEntry.DiscordOnComplete) { DiscordAnnouncer.AnnounceChainComplete(playerName, guidanceEntry.Title ?? entryId); } } } public static void SendRewardDiscord(string message) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_RewardDiscord", new object[1] { message ?? "" }); } } private static void OnRewardDiscord(long sender, string message) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { DiscordAnnouncer.AnnounceRaw(message); } } public static void SendShareKillProgress(string entryId, string playerName, Vector3 position) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(0L, "VSG_ShareKillProgress", new object[3] { entryId ?? "", playerName ?? "", position }); } } private static void OnShareKillProgress(long sender, string entryId, string playerName, Vector3 position) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && !string.Equals(playerName, localPlayer.GetPlayerName(), StringComparison.Ordinal)) { KillCountTracker.ApplySharedIncrement(entryId, localPlayer, position); } } public static void SendQuestStartLog(string entryId, string playerName, string biome, Vector3 position) { if (ZRoutedRpc.instance != null) { string separator = '\u001f'.ToString(); string text = string.Join(separator, entryId ?? "", playerName ?? "", biome ?? "", position.x.ToString(CultureInfo.InvariantCulture), position.y.ToString(CultureInfo.InvariantCulture), position.z.ToString(CultureInfo.InvariantCulture)); long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_QuestStartLog", new object[1] { text }); } } private static void OnQuestStartLog(long sender, string encoded) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || string.IsNullOrEmpty(encoded)) { return; } string[] array = encoded.Split(new char[1] { '\u001f' }); if (array.Length >= 6) { string entryId = array[0]; string playerName = array[1]; string biome = array[2]; CultureInfo invariantCulture = CultureInfo.InvariantCulture; float.TryParse(array[3], NumberStyles.Float, invariantCulture, out var result); float.TryParse(array[4], NumberStyles.Float, invariantCulture, out var result2); float.TryParse(array[5], NumberStyles.Float, invariantCulture, out var result3); GuidanceEntry entry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); DiscordAnnouncer.AnnounceQuestStart(entry, entryId, playerName, biome, new Vector3(result, result2, result3)); } } public static void BroadcastTimedGuidance(string entryId) { if (ZRoutedRpc.instance != null) { Plugin.Log.LogInfo((object)("[timed] server broadcasting '" + entryId + "' to all clients.")); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "VSG_TimedGuidance", new object[1] { entryId }); } } private static void OnTimedGuidance(long sender, string entryId) { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry?.Trigger != null) { string subject = guidanceEntry.Trigger.Id ?? entryId; GuidanceDispatcher.Raise(new TriggerEvent { Type = "timed", Subject = subject }); } } } public static void SendChainStepUpdate(string playerName, string entryId, string value) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_ChainStepUpdate", new object[3] { playerName ?? "", entryId ?? "", value ?? "" }); } } private static void OnChainStepUpdate(long sender, string playerName, string entryId, string value) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && !string.IsNullOrEmpty(playerName) && !string.IsNullOrEmpty(entryId)) { if (!_playerChainData.TryGetValue(playerName, out var value2)) { value2 = (_playerChainData[playerName] = new Dictionary()); } value2[entryId] = value; Plugin.Log.LogInfo((object)("[chain-sync] stored '" + entryId + "'='" + value + "' for player '" + playerName + "'.")); } } public static void RequestChainState(string playerName) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_ChainStateRequest", new object[1] { playerName ?? "" }); } } private static void OnChainStateRequest(long sender, string playerName) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !_playerChainData.TryGetValue(playerName, out var value) || value.Count == 0) { return; } List list = new List(value.Count); foreach (KeyValuePair item in value) { list.Add(item.Key + "=" + item.Value); } string text = string.Join("|", list); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "VSG_ChainStatePush", new object[2] { playerName, text }); } private static void OnChainStatePush(long sender, string playerName, string encoded) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerName() != playerName || string.IsNullOrEmpty(encoded)) { return; } string[] array = encoded.Split(new char[1] { '|' }); foreach (string text in array) { int num = text.IndexOf('='); if (num < 0) { continue; } string text2 = text.Substring(0, num); string text3 = text.Substring(num + 1); int num2 = text2.IndexOf(':'); if (num2 >= 0) { string text4 = text2.Substring(0, num2); string s = text2.Substring(num2 + 1); if (int.TryParse(s, out var result) && int.TryParse(text3, out var result2)) { int counter = ChainState.GetCounter(localPlayer, text4, result); if (result2 > counter) { ChainState.SetCounter(localPlayer, text4, result, result2); Plugin.Log.LogInfo((object)$"[chain-sync] server pushed counter {result2} for '{text4}' step {result} (was {counter})."); } } continue; } string text5 = text2; int result3; if (text3 == "done") { if (!ChainState.IsComplete(localPlayer, text5)) { ChainState.MarkComplete(localPlayer, text5); Plugin.Log.LogInfo((object)("[chain-sync] server pushed complete for '" + text5 + "'.")); } } else if (int.TryParse(text3, out result3)) { int step = ChainState.GetStep(localPlayer, text5); if (result3 > step) { ChainState.SetStep(localPlayer, text5, result3); Plugin.Log.LogInfo((object)$"[chain-sync] server pushed step {result3} for '{text5}' (was {step})."); } } } GuidanceHudTracker.Instance?.Refresh(); } public static void SendAdminResetGlobal(string entryId) { if (ZRoutedRpc.instance != null) { long serverPeerID = ZRoutedRpc.instance.GetServerPeerID(); ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerID, "VSG_AdminResetGlobal", new object[1] { entryId }); } } private static void OnAdminResetGlobal(long sender, string entryId) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); object obj; if (peer == null) { obj = null; } else { ISocket socket = peer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (string.IsNullOrEmpty(text) || !ZNet.instance.IsAdmin(text)) { Plugin.Log.LogWarning((object)$"[admin-reset] non-admin sender ({sender}, host='{text}') tried to reset '{entryId}' — denied."); return; } GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == entryId); if (guidanceEntry == null) { Plugin.Log.LogWarning((object)("[admin-reset] '" + entryId + "' not in current config; ignoring.")); return; } if (!SeenTracker.IsGlobalScope(guidanceEntry.Scope)) { Plugin.Log.LogWarning((object)("[admin-reset] '" + entryId + "' is not global-scope; ignoring.")); return; } bool flag = SeenTracker.ClearFired(null, entryId, "global"); Plugin.Log.LogInfo((object)(flag ? ("[admin-reset] cleared global '" + entryId + "' for admin " + text + ".") : ("[admin-reset] global '" + entryId + "' was not set; nothing to clear (request by " + text + ")."))); } public static bool ListPlayerForLocalAdmin(string targetName) { ZNetPeer val = FindPeerByPlayerName(targetName); if (val == null) { return false; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(val.m_uid, "VSG_APListFwd", new object[1] { "server" }); } return true; } public static bool ResetPlayerForLocalAdmin(string targetName, string resetArg) { ZNetPeer val = FindPeerByPlayerName(targetName); if (val == null) { return false; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(val.m_uid, "VSG_APResetFwd", new object[2] { "server", resetArg ?? "all" }); } return true; } public static void SendAdminPlayerListReq(string targetName) { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "VSG_APListReq", new object[1] { targetName ?? "" }); } } public static void SendAdminPlayerResetReq(string targetName, string resetArg) { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "VSG_APResetReq", new object[2] { targetName ?? "", resetArg ?? "all" }); } } public static IEnumerable GetOnlinePeerNames() { if ((Object)(object)ZNet.instance == (Object)null) { yield break; } foreach (ZNetPeer p in ZNet.instance.GetPeers()) { if (!string.IsNullOrEmpty(p.m_playerName)) { yield return p.m_playerName; } } } private static void OnAdminPlayerListReq(long sender, string targetName) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); object obj; if (peer == null) { obj = null; } else { ISocket socket = peer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (string.IsNullOrEmpty(text) || !ZNet.instance.IsAdmin(text)) { Plugin.Log.LogWarning((object)$"[admin-plist] non-admin ({sender}) tried to list '{targetName}' — denied."); return; } ZNetPeer val = FindPeerByPlayerName(targetName); if (val == null) { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "VSG_APListOut", new object[1] { "vsg_list_player: '" + targetName + "' is not currently online." }); } else { ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, "VSG_APListFwd", new object[1] { sender.ToString() }); } } private static void OnAdminPlayerListFwd(long sender, string adminMarker) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null)) { string text = CollectPlayerStatePayload(localPlayer); if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "VSG_APListResp", new object[2] { adminMarker, text }); } } } private static void OnAdminPlayerListResp(long sender, string adminMarker, string payload) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } long result; if (adminMarker == "server") { string[] array = payload.Split(new char[1] { '\n' }); foreach (string text in array) { Console instance = Console.instance; if (instance != null) { ((Terminal)instance).AddString(text); } } } else if (long.TryParse(adminMarker, out result)) { ZRoutedRpc.instance.InvokeRoutedRPC(result, "VSG_APListOut", new object[1] { payload }); } } private static void OnAdminPlayerListOut(long sender, string payload) { if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } string[] array = payload.Split(new char[1] { '\n' }); foreach (string text in array) { Console instance = Console.instance; if (instance != null) { ((Terminal)instance).AddString(text); } } } private static void OnAdminPlayerResetReq(long sender, string targetName, string resetArg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); object obj; if (peer == null) { obj = null; } else { ISocket socket = peer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (string.IsNullOrEmpty(text) || !ZNet.instance.IsAdmin(text)) { Plugin.Log.LogWarning((object)$"[admin-preset] non-admin ({sender}) tried to reset '{targetName}' — denied."); return; } ZNetPeer val = FindPeerByPlayerName(targetName); if (val == null) { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "VSG_APResetOut", new object[1] { "vsg_reset_player: '" + targetName + "' is not currently online." }); } else { ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, "VSG_APResetFwd", new object[2] { sender.ToString(), resetArg }); } } private static void OnAdminPlayerResetFwd(long sender, string adminMarker, string resetArg) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } string text; if (string.Equals(resetArg, "all", StringComparison.OrdinalIgnoreCase)) { int num = SeenTracker.ClearAllFired(localPlayer); ChainState.ResetAll(localPlayer); SubmitState.ResetAll(localPlayer); GoalStartedState.ResetAll(localPlayer); KillCountState.ResetAll(localPlayer); ConversationNodeState.ResetAll(localPlayer); TrackedQuestState.ResetAll(localPlayer); QuestStartLogState.ResetAll(localPlayer); GuidanceDisplay.ClearAllVsgTutorialSeen(); GuidanceDisplay.ClearRavenState(); GuidanceHudTracker.Instance?.Refresh(); text = $"vsg_reset_player: cleared {num} fired id(s) + all chain/submit/goal state for {localPlayer.GetPlayerName()}."; } else { GuidanceEntry guidanceEntry = Plugin.CurrentConfig?.Guidances?.Find((GuidanceEntry g) => g.Id == resetArg); bool flag = guidanceEntry != null && guidanceEntry.Steps?.Count > 0; if (flag) { ChainState.Reset(localPlayer, resetArg); } GuidanceDisplay.ClearVsgTutorialSeenForEntry(resetArg); GuidanceDisplay.ClearRavenQueueForId(resetArg); bool flag2 = SeenTracker.ClearFired(localPlayer, resetArg); bool flag3 = SubmitState.Get(localPlayer, resetArg) > 0; if (flag3) { SubmitState.Clear(localPlayer, resetArg); } bool flag4 = GoalStartedState.IsStarted(localPlayer, resetArg); if (flag4) { GoalStartedState.Clear(localPlayer, resetArg); } bool flag5 = KillCountState.Get(localPlayer, resetArg) > 0; if (flag5) { KillCountState.Clear(localPlayer, resetArg); } bool flag6 = ConversationNodeState.GetCurrentNode(localPlayer, resetArg) != null; if (flag6) { ConversationNodeState.Clear(localPlayer, resetArg); } QuestStartLogState.Clear(localPlayer, resetArg); if (flag2 || flag || flag3 || flag4 || flag5 || flag6) { GuidanceHudTracker.Instance?.Refresh(); text = "vsg_reset_player: cleared '" + resetArg + "'" + (flag ? " (chain)" : "") + (flag3 ? " (submit)" : "") + (flag4 ? " (goal)" : "") + (flag5 ? " (kill)" : "") + (flag6 ? " (node)" : "") + " for " + localPlayer.GetPlayerName() + "."; } else { text = "vsg_reset_player: '" + resetArg + "' was not set for " + localPlayer.GetPlayerName() + " (nothing cleared)."; } } Plugin.Log.LogInfo((object)("[admin-preset] " + text)); if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "VSG_APResetAck", new object[2] { adminMarker, text }); } } private static void OnAdminPlayerResetAck(long sender, string adminMarker, string resultMsg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } long result; if (adminMarker == "server") { Console instance = Console.instance; if (instance != null) { ((Terminal)instance).AddString(resultMsg); } } else if (long.TryParse(adminMarker, out result)) { ZRoutedRpc.instance.InvokeRoutedRPC(result, "VSG_APResetOut", new object[1] { resultMsg }); } } private static void OnAdminPlayerResetOut(long sender, string resultMsg) { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { Console instance = Console.instance; if (instance != null) { ((Terminal)instance).AddString(resultMsg); } } } private static string CollectPlayerStatePayload(Player player) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== ValheimServerGuide (" + player.GetPlayerName() + ") ==="); List list = (from s in SeenTracker.GetFiredIds(player) orderby s select s).ToList(); stringBuilder.AppendLine($"Fired ({list.Count}):"); if (list.Count == 0) { stringBuilder.AppendLine(" (none)"); } else { foreach (string item in list) { stringBuilder.AppendLine(" - " + item); } } List list2 = (from k in player.m_customData.Keys where k.StartsWith("VSG.fc.") orderby k select k).ToList(); if (list2.Count > 0) { stringBuilder.AppendLine($"Fire counts ({list2.Count}):"); foreach (string item2 in list2) { stringBuilder.AppendLine(" - " + item2.Substring("VSG.fc.".Length) + " = " + player.m_customData[item2]); } } List list3 = (from k in player.m_customData.Keys where k.StartsWith("VSG.cd.") orderby k select k).ToList(); List list4 = (from k in player.m_customData.Keys where k.StartsWith("VSG.cp.") orderby k select k).ToList(); if (list3.Count > 0 || list4.Count > 0) { stringBuilder.AppendLine("Chain state:"); foreach (string item3 in list3) { stringBuilder.AppendLine(" - " + item3.Substring("VSG.cd.".Length) + ": complete"); } foreach (string item4 in list4) { stringBuilder.AppendLine(" - " + item4.Substring("VSG.cp.".Length) + ": step " + player.m_customData[item4]); } } List list5 = (from k in player.m_customData.Keys where k.StartsWith("VSG.is.") orderby k select k).ToList(); if (list5.Count > 0) { stringBuilder.AppendLine("Submit state:"); foreach (string item5 in list5) { stringBuilder.AppendLine(" - " + item5.Substring("VSG.is.".Length) + ": " + player.m_customData[item5] + " submitted"); } } List list6 = (from k in player.m_customData.Keys where k.StartsWith("VSG.ig.") orderby k select k).ToList(); if (list6.Count > 0) { stringBuilder.AppendLine("Goal state:"); foreach (string item6 in list6) { stringBuilder.AppendLine(" - " + item6.Substring("VSG.ig.".Length) + ": started"); } } return stringBuilder.ToString().TrimEnd('\n', '\r'); } private static ZNetPeer FindPeerByPlayerName(string name) { if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrEmpty(name)) { return null; } foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (string.Equals(peer.m_playerName, name, StringComparison.OrdinalIgnoreCase)) { return peer; } } return null; } private static string Serialize(GuidanceConfig config) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown ISerializer val = ((BuilderSkeleton)new SerializerBuilder()).WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); using StringWriter stringWriter = new StringWriter(); val.Serialize((TextWriter)stringWriter, (object)config); return stringWriter.ToString(); } private static GuidanceConfig Deserialize(string yaml) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown IDeserializer val = ((BuilderSkeleton)new DeserializerBuilder()).WithNamingConvention(UnderscoredNamingConvention.Instance).IgnoreUnmatchedProperties().Build(); return val.Deserialize(yaml) ?? GuidanceConfig.Empty; } } } namespace ValheimServerGuide.Display { public class GuidanceCodex : MonoBehaviour { private GameObject _uiRoot; private GameObject _panel; private Transform _leftContent; private readonly List<(GuidanceEntry entry, GameObject row, TMP_Text label)> _guideRows = new List<(GuidanceEntry, GameObject, TMP_Text)>(); private GuidanceEntry _selected; private TMP_Text _titleText; private TMP_Text _badgeText; private TMP_Text _bodyText; private RectTransform _bodyContentRect; private Transform _upcomingContent; private GameObject _trackToggleGo; private TMP_Text _trackToggleText; private TMP_FontAsset _font; private static readonly Color ColGold = new Color(1f, 0.82f, 0.42f); private static readonly Color ColText = new Color(0.9f, 0.88f, 0.82f); private static readonly Color ColLocked = new Color(0.5f, 0.5f, 0.5f); private static readonly Color ColGreen = new Color(0.6f, 0.95f, 0.6f); private static readonly Color ColBg = new Color(0.06f, 0.05f, 0.04f, 0.96f); private static readonly Color ColPanelBg = new Color(0.1f, 0.08f, 0.06f, 0.92f); private static readonly Color ColHdrBar = new Color(0.12f, 0.1f, 0.08f, 1f); private static readonly Color ColDivider = new Color(0.3f, 0.25f, 0.2f, 1f); private static readonly Color ColRowSel = new Color(0.22f, 0.17f, 0.1f, 0.9f); public static GuidanceCodex Instance { get; internal set; } public bool IsOpen { get; private set; } public void BuildPanel() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_020a: Unknown result type (might be due to invalid IL or missing references) //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) _uiRoot = new GameObject("VSG_CodexRoot"); Canvas val = _uiRoot.AddComponent(); val.renderMode = (RenderMode)0; val.sortingOrder = 1100; object obj; if (!((Object)(object)Hud.instance != (Object)null)) { obj = null; } else { Canvas componentInParent = ((Component)Hud.instance).GetComponentInParent(); obj = ((componentInParent != null) ? ((Component)componentInParent).GetComponent() : null); } CanvasScaler val2 = (CanvasScaler)obj; CanvasScaler val3 = _uiRoot.AddComponent(); if ((Object)(object)val2 != (Object)null) { val3.uiScaleMode = val2.uiScaleMode; val3.referenceResolution = val2.referenceResolution; val3.screenMatchMode = val2.screenMatchMode; val3.matchWidthOrHeight = val2.matchWidthOrHeight; } _uiRoot.AddComponent(); _uiRoot.SetActive(false); GameObject val4 = new GameObject("VSG_CodexBackdrop"); val4.transform.SetParent(_uiRoot.transform, false); RectTransform val5 = val4.AddComponent(); val5.anchorMin = Vector2.zero; val5.anchorMax = Vector2.one; val5.offsetMin = Vector2.zero; val5.offsetMax = Vector2.zero; Image val6 = val4.AddComponent(); ((Graphic)val6).color = new Color(0f, 0f, 0f, 0.6f); Button val7 = val4.AddComponent