Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimServerGuide v0.9.0
ValheimServerGuide.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.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<bool> RavenEnabled { get; private set; } public static ConfigEntry<string> IntroMusicName { get; private set; } public static ConfigEntry<float> IntroMusicDuration { get; private set; } public static ConfigEntry<float> IntroFadeInDuration { get; private set; } public static ConfigEntry<float> IntroPreDelay { get; private set; } public static ConfigEntry<float> IntroDisplaySeconds { get; private set; } public static ConfigEntry<float> IntroFadeOutDuration { get; private set; } public static ConfigEntry<string> ChatColor { get; private set; } public static ConfigEntry<bool> CodexEnabled { get; private set; } public static ConfigEntry<string> CodexKey { get; private set; } public static ConfigEntry<bool> TrackerEnabled { get; private set; } public static ConfigEntry<string> TrackerPosition { get; private set; } public static ConfigEntry<int> TrackerMaxVisible { get; private set; } public static ConfigEntry<string> TrackerHotkey { get; private set; } public static ConfigEntry<bool> TrackerBadgeEnabled { get; private set; } public static ConfigEntry<string> DiscordWebhookUrl { get; private set; } public static ConfigEntry<string> DiscordDefaultTemplate { get; private set; } public static ConfigEntry<string> DiscordBotUsername { get; private set; } public static ConfigEntry<bool> DiscordGuideEnabled { get; private set; } public static ConfigEntry<string> DiscordGuideFormat { get; private set; } public static ConfigEntry<bool> DiscordQuestStartEnabled { get; private set; } public static ConfigEntry<string> 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<bool>("Display", "RavenEnabled", true, "Enable raven (Hugin) popup mode. Independent of Valheim's 'Tutorials' setting — this mod's raven popups will fire even when vanilla raven hints are turned off in game options."); IntroMusicName = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "IntroMusicName", "intro", "Music track to play while a guidance is shown in 'intro' display mode. 'intro' is the vanilla Valkyrie-intro track."); IntroMusicDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroMusicDuration", 60f, "Seconds the intro music stays pinned once it starts. The music plays for at least this long even if the player dismisses the on-screen text early. After the duration elapses, vanilla MusicMan resumes normal environment-based selection."); IntroFadeInDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroFadeInDuration", 3f, "Seconds to fade the screen to black before the intro text + music start. Uses vanilla Hud.m_loadingScreen so no custom assets are needed. Set 0 to disable."); IntroPreDelay = ((BaseUnityPlugin)this).Config.Bind<float>("Display", "IntroPreDelay", 1f, "Seconds to hold on a black screen after the fade-in, before the intro text appears. Adds dramatic weight to the transition."); IntroDisplaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("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<float>("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<string>("Display", "ChatColor", "#E0C078", "Hex color (with or without leading '#') applied to chat-mode guidance messages, so they read distinct from regular say (white) and shout (yellow). Set to empty string to disable coloring."); TrackerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("HudTracker", "TrackerEnabled", true, "Show the objective tracker widget on the HUD. Set false to hide it entirely (the widget GameObject remains in the scene — just inactive)."); TrackerPosition = ((BaseUnityPlugin)this).Config.Bind<string>("HudTracker", "TrackerPosition", "TopRight", "Corner the tracker widget anchors to: TopRight | TopLeft | BottomRight | BottomLeft. Takes effect on next session start (Hud.Awake)."); TrackerMaxVisible = ((BaseUnityPlugin)this).Config.Bind<int>("HudTracker", "TrackerMaxVisible", 3, "Maximum number of active guide chains shown simultaneously. Chains beyond this limit are collapsed into a '+N more' label."); TrackerHotkey = ((BaseUnityPlugin)this).Config.Bind<string>("HudTracker", "TrackerHotkey", "F10", "KeyCode name for the tracker toggle hotkey (e.g. F9, F10, H). See UnityEngine.KeyCode enum for valid values. YAML tracker.hotkey wins when set."); TrackerBadgeEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("HudTracker", "TrackerBadgeEnabled", true, "Show the persistent corner hint badge (e.g. '[F9] Quests (2)') even when the main tracker panel is hidden. YAML tracker.badge_enabled wins when set."); CodexEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Codex", "CodexEnabled", true, "Enable the in-game Guide Codex panel. Set false to disable the keybind and skip instantiating the panel entirely."); CodexKey = ((BaseUnityPlugin)this).Config.Bind<string>("Codex", "CodexKey", "F3", "KeyCode name for the Codex toggle hotkey (e.g. F2, F3). See UnityEngine.KeyCode enum for valid values."); DiscordWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL. Set on the server only — never share this with clients. Leave empty to disable all discord announcements."); DiscordDefaultTemplate = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "DefaultTemplate", "**{playerName}** triggered **{topic}**", "Default message template when a guidance entry has `announce: { discord: \"\" }` (empty string = use default). Tokens: {playerName}, {id}, {topic}, {text}."); DiscordBotUsername = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "BotUsername", "ValheimServerGuide", "Username shown for webhook messages in Discord."); DiscordGuideEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "DiscordGuideEnabled", true, "Enable guide-completion webhook POSTs (discord_on_complete). Set false to suppress these without affecting kill/event POSTs."); DiscordGuideFormat = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "DiscordGuideFormat", "plain", "Format for guide-completion messages: 'plain' (content string) or 'embed' (rich embed)."); DiscordQuestStartEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("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<string>("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<Vector2i, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances) { LocationInstance value = locationInstance.Value; if (!value.m_placed) { continue; } string text = value.m_location?.m_prefabName; if (!string.IsNullOrEmpty(text)) { string id = "dist_" + text; if (!SeenTracker.HasFired(__instance, id) && AnyEntryInRange(currentConfig, text, position, value.m_position)) { SeenTracker.MarkFired(__instance, id); Plugin.Log.LogInfo((object)("[distance] entered range of '" + text + "'.")); GuidanceDispatcher.Raise(new TriggerEvent { Type = "distance", Subject = text }); } } } } private static bool AnyEntryInRange(GuidanceConfig config, string prefabName, Vector3 playerPos, Vector3 locPos) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) foreach (GuidanceEntry guidance in config.Guidances) { if (CheckTrigger(guidance.Trigger, prefabName, playerPos, locPos)) { return true; } if (guidance.Steps == null) { continue; } foreach (GuidanceStep step in guidance.Steps) { if (CheckTrigger(step?.Trigger, prefabName, playerPos, locPos)) { return true; } } } return false; } private static bool CheckTrigger(TriggerSpec t, string prefabName, Vector3 playerPos, Vector3 locPos) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if (t == null) { return false; } if (!string.Equals(t.Type, "distance", StringComparison.OrdinalIgnoreCase)) { return false; } if (!LocationMatches(t.Location, prefabName)) { return false; } float num = ((t.Radius > 0f) ? t.Radius : 50f); return Vector3.Distance(playerPos, locPos) <= num; } private static bool LocationMatches(string pattern, string value) { if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value)) { return false; } if (pattern.EndsWith("*")) { return value.StartsWith(pattern.Substring(0, pattern.Length - 1), StringComparison.OrdinalIgnoreCase); } return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase); } } [HarmonyPatch(typeof(Humanoid), "EquipItem")] internal static class EquipTrigger { [HarmonyPostfix] private static void Postfix(Humanoid __instance, ItemData item, bool __result) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && __result && item != null) { string text = ResolveItemName(item); if (!string.IsNullOrEmpty(text)) { Plugin.Log.LogInfo((object)("[equip] subject='" + text + "' (token='" + item.m_shared?.m_name + "').")); GuidanceDispatcher.Raise(new TriggerEvent { Type = "equip", Subject = text, DisplayName = item.m_shared?.m_name }); } } } private static string ResolveItemName(ItemData item) { GameObject dropPrefab = item.m_dropPrefab; string text = ((dropPrefab != null) ? ((Object)dropPrefab).name : null); if (!string.IsNullOrEmpty(text)) { return TriggerUtils.NormalizePrefabName(text); } return TriggerUtils.NormalizePrefabName(item.m_shared?.m_name ?? ""); } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class FirstLoginTrigger { private const string GuardKey = "first_login_fired"; [HarmonyPostfix] private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !SeenTracker.HasFired(__instance, "first_login_fired")) { SeenTracker.MarkFired(__instance, "first_login_fired"); GuidanceDispatcher.Raise(new TriggerEvent { Type = "first_login", Subject = "" }); } } } public static class GuidanceDispatcher { public static void Raise(TriggerEvent evt) { Player 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<string> list = new List<string>(); 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<string> completedIds) { if (ChainState.IsComplete(player, entry.Id)) { return false; } if (!PrerequisiteChecker.AllSatisfied(entry.Requires, player, Plugin.CurrentConfig)) { Plugin.Log.LogDebug((object)("[chain] '" + entry.Id + "' prerequisites not met.")); return false; } int step = ChainState.GetStep(player, entry.Id); if (step >= entry.Steps.Count) { ChainState.MarkComplete(player, entry.Id); return false; } GuidanceStep guidanceStep = entry.Steps[step]; if (guidanceStep?.Trigger == null) { return false; } if (guidanceStep.ProgressGoal > 0) { return HandleCounterStep(entry, guidanceStep, step, evt, player, completedIds); } return HandleNormalStep(entry, guidanceStep, step, evt, player, completedIds); } private static bool HandleNormalStep(GuidanceEntry entry, GuidanceStep step, int stepIndex, TriggerEvent evt, Player player, List<string> completedIds) { if (!MatchesTrigger(step.Trigger, evt)) { return false; } 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<string> completedIds) { if (step.ProgressTrigger == null) { Plugin.Log.LogWarning((object)($"[chain] '{entry.Id}' step {stepIndex} has progress_goal " + "but no progress_trigger — treating as normal step.")); return HandleNormalStep(entry, step, stepIndex, evt, player, completedIds); } int counter = ChainState.GetCounter(player, entry.Id, stepIndex); if (counter < 0) { if (!MatchesTrigger(step.Trigger, evt)) { return false; } 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<string> completedIds) { int num = stepIndex + 1; if (num >= entry.Steps.Count) { ChainState.MarkComplete(player, entry.Id); ChainState.SetCompletedVersion(player, entry.Id, entry.Version); Plugin.Log.LogInfo((object)$"[chain] '{entry.Id}' complete (all {entry.Steps.Count} steps done)."); GuidanceSync.SendChainStepUpdate(player.GetPlayerName(), entry.Id, "done"); GuidanceHudTracker.Instance?.FlashCompletion(entry.Id); 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<string, object> Extra; } [HarmonyPatch(typeof(Humanoid), "Pickup")] internal static class ItemAcquiredTrigger { [HarmonyPostfix] private static void Postfix(Humanoid __instance, GameObject go) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)go == (Object)null)) { string text = TriggerUtils.NormalizePrefabName(((Object)go).name); if (!string.IsNullOrEmpty(text)) { string displayName = go.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_name; GuidanceDispatcher.Raise(new TriggerEvent { Type = "item_acquired", Subject = text, DisplayName = displayName }); CheckCountGoals(text, displayName); } } } internal static List<ItemGoalSpec> GetEffectiveGoals(TriggerSpec trigger) { if (trigger == null) { return null; } if (trigger.Goals != null && trigger.Goals.Count > 0) { return trigger.Goals; } if (!string.IsNullOrEmpty(trigger.Item) && trigger.Count > 1) { return new List<ItemGoalSpec> { new ItemGoalSpec { Item = trigger.Item, Count = trigger.Count } }; } return null; } private static bool IsCountGoalEntry(GuidanceEntry entry) { if (entry.Trigger == null) { return false; } if (!string.Equals(entry.Trigger.Type, "item_acquired", StringComparison.OrdinalIgnoreCase)) { return false; } return GetEffectiveGoals(entry.Trigger) != null; } internal static void CheckAllCountGoals() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (!IsCountGoalEntry(guidance) || !GuidanceDispatcher.CheckGates(guidance, localPlayer)) { continue; } List<ItemGoalSpec> effectiveGoals = GetEffectiveGoals(guidance.Trigger); bool flag = true; bool flag2 = false; foreach (ItemGoalSpec item in effectiveGoals) { int num = CountInInventory(localPlayer, item.Item); Plugin.Log.LogInfo((object)$"[item_acquired] '{guidance.Id}' seed {item.Item}: {num}/{item.Count}."); if (num < item.Count) { flag = false; } if (num > 0) { flag2 = true; } } if (flag) { Plugin.Log.LogInfo((object)("[item_acquired] '" + guidance.Id + "' all goals already met — firing.")); GoalStartedState.Clear(localPlayer, guidance.Id); GuidanceDispatcher.FireEntry(guidance, new TriggerEvent { Type = "item_acquired", Subject = effectiveGoals[0].Item }); GuidanceHudTracker.Instance?.FlashCompletion(guidance.Id); } else if (flag2 || GoalStartedState.IsStarted(localPlayer, guidance.Id)) { if (flag2) { GoalStartedState.MarkStarted(localPlayer, guidance.Id); } GuidanceHudTracker.Instance?.Refresh(fromProgress: true); } } } internal static void CheckCountGoals(string prefabName, string displayName) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (!IsCountGoalEntry(guidance)) { continue; } List<ItemGoalSpec> effectiveGoals = GetEffectiveGoals(guidance.Trigger); bool flag = false; foreach (ItemGoalSpec item in effectiveGoals) { if (ItemWildcardMatch(item.Item, prefabName)) { flag = true; break; } } if (!flag || !GuidanceDispatcher.CheckGates(guidance, localPlayer)) { continue; } bool flag2 = true; bool flag3 = false; foreach (ItemGoalSpec item2 in effectiveGoals) { int num = CountInInventory(localPlayer, item2.Item); Plugin.Log.LogInfo((object)$"[item_acquired] '{guidance.Id}' {item2.Item}: {num}/{item2.Count}."); if (num < item2.Count) { flag2 = false; } if (num > 0) { flag3 = true; } } if (flag2) { Plugin.Log.LogInfo((object)("[item_acquired] '" + guidance.Id + "' all goals reached — firing.")); GoalStartedState.Clear(localPlayer, guidance.Id); GuidanceDispatcher.FireEntry(guidance, new TriggerEvent { Type = "item_acquired", Subject = prefabName, DisplayName = displayName }); GuidanceHudTracker.Instance?.FlashCompletion(guidance.Id); } else { if (flag3) { GoalStartedState.MarkStarted(localPlayer, guidance.Id); } GuidanceHudTracker.Instance?.Refresh(fromProgress: true); } } } internal static int CountInInventory(Player player, string prefabPattern) { if (string.IsNullOrEmpty(prefabPattern)) { return 0; } Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { return 0; } int num = 0; foreach (ItemData allItem in inventory.GetAllItems()) { if (!((Object)(object)allItem?.m_dropPrefab == (Object)null)) { string value = TriggerUtils.NormalizePrefabName(((Object)allItem.m_dropPrefab).name); if (ItemWildcardMatch(prefabPattern, value)) { num += allItem.m_stack; } } } return num; } internal static string BuildGoalProgressText(Player player, List<ItemGoalSpec> goals) { if ((Object)(object)player == (Object)null || goals == null || goals.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (ItemGoalSpec goal in goals) { int value = Math.Min(CountInInventory(player, goal.Item), goal.Count); if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(goal.Item).Append(": ").Append(value) .Append('/') .Append(goal.Count); } return stringBuilder.ToString(); } internal static bool ItemWildcardMatch(string pattern, string value) { if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value)) { return false; } if (pattern.EndsWith("*")) { string value2 = pattern.Substring(0, pattern.Length - 1); return value.StartsWith(value2, StringComparison.OrdinalIgnoreCase); } return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase); } } [HarmonyPatch(typeof(InventoryGui), "DoCrafting")] internal static class ItemAcquiredCraftPatch { [HarmonyPostfix] private static void Postfix(InventoryGui __instance, Player player) { if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } Recipe craftRecipe = __instance.m_craftRecipe; if (!((Object)(object)craftRecipe?.m_item == (Object)null)) { string text = TriggerUtils.NormalizePrefabName(((Object)((Component)craftRecipe.m_item).gameObject).name); if (!string.IsNullOrEmpty(text)) { string displayName = craftRecipe.m_item.m_itemData?.m_shared?.m_name; ItemAcquiredTrigger.CheckCountGoals(text, displayName); } } } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class KillTrigger { [HarmonyPostfix] private static void Postfix(Character __instance) { if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } object obj; if (__instance == null) { obj = null; } else { HitData lastHit = __instance.m_lastHit; obj = ((lastHit != null) ? lastHit.GetAttacker() : null); } Character val = (Character)obj; if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer)) { GameObject gameObject = ((Component)__instance).gameObject; string text = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); if (!string.IsNullOrEmpty(text)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "kill", Subject = text, DisplayName = __instance.m_name }); 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<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (Location s_allLocation in Location.s_allLocations) { if ((Object)(object)s_allLocation == (Object)null || Vector3.Distance(position, ((Component)s_allLocation).transform.position) > 40f) { continue; } string text = ((Object)((Component)s_allLocation).gameObject).name; if (text.EndsWith("(Clone)")) { text = text.Substring(0, text.Length - 7).TrimEnd(Array.Empty<char>()); } if (!string.IsNullOrEmpty(text)) { Plugin.Log.LogDebug((object)("[location_entered] Scene scan in range: '" + text + "'")); string id = "loc_" + text; if (!SeenTracker.HasFired(__instance, id)) { SeenTracker.MarkFired(__instance, id); hashSet.Add(text); GuidanceDispatcher.Raise(new TriggerEvent { Type = "location_entered", Subject = text }); } } } foreach (KeyValuePair<Vector2i, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances) { LocationInstance value = locationInstance.Value; string text2 = value.m_location?.m_prefabName; if (string.IsNullOrEmpty(text2)) { text2 = value.m_location?.m_name; } if (string.IsNullOrEmpty(text2)) { continue; } float num = Vector3.Distance(position, value.m_position); if (num > 40f) { continue; } if (!value.m_placed) { Plugin.Log.LogDebug((object)$"[location_entered] ZoneSystem in range but unplaced: '{text2}' dist={num:F0}"); continue; } Plugin.Log.LogDebug((object)$"[location_entered] ZoneSystem in range (placed): '{text2}' dist={num:F0}"); if (!hashSet.Contains(text2)) { string id2 = "loc_" + text2; if (!SeenTracker.HasFired(__instance, id2)) { SeenTracker.MarkFired(__instance, id2); GuidanceDispatcher.Raise(new TriggerEvent { Type = "location_entered", Subject = text2 }); } } } } } internal static class NpcConvHoldState { internal const float HoldThreshold = 0.5f; internal static float HoldStart = -1f; internal static Trader PendingTrader = null; } [HarmonyPatch(typeof(Trader), "Interact")] internal static class NpcConversationTrigger { [HarmonyPrefix] private static bool Prefix(Trader __instance, Humanoid character, bool hold, ref bool __result) { NpcConversationHoldDetector.EnsureCreated(); Player val = (Player)(object)((character is Player) ? character : null); if ((Object)(object)val == (Object)null || (Object)(object)val != (Object)(object)Player.m_localPlayer) { return true; } GameObject gameObject = ((Component)__instance).gameObject; string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); GuidanceEntry guidanceEntry = FindEntry(npcSubject, val); if (guidanceEntry == null) { return true; } if (!hold) { NpcConvHoldState.HoldStart = Time.time; NpcConvHoldState.PendingTrader = __instance; __result = true; return false; } __result = false; return false; } internal static GuidanceEntry FindEntry(string npcSubject, Player player) { if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null) { return null; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return null; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "npc_conversation", StringComparison.OrdinalIgnoreCase) || !string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) || !GuidanceDispatcher.CheckGates(guidance, player)) { continue; } return guidance; } return null; } internal static List<GuidanceEntry> FindAllEntries(string npcSubject, Player player) { List<GuidanceEntry> list = new List<GuidanceEntry>(); 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<NpcConversationHoldDetector>(); } } private void Update() { if ((Object)(object)NpcConvHoldState.PendingTrader == (Object)null) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { Reset(); } else if (!ZInput.GetButton("Use")) { Trader pendingTrader = NpcConvHoldState.PendingTrader; Reset(); if ((Object)(object)StoreGui.instance != (Object)null) { StoreGui.instance.Show(pendingTrader); } } else { if (!(Time.time - NpcConvHoldState.HoldStart >= 0.5f)) { return; } Trader pendingTrader2 = NpcConvHoldState.PendingTrader; Reset(); GameObject gameObject = ((Component)pendingTrader2).gameObject; string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); List<GuidanceEntry> 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<string, object> { { "item", itemPrefabName } } }; if (num <= 1) { if (consume) { ConsumeItems(player, item, 1); } Plugin.Log.LogInfo((object)("[item_submit] firing entry '" + entry.Id + "' (single) for '" + itemPrefabName + "' -> '" + npcSubject + "'.")); GuidanceDispatcher.FireEntry(entry, evt); return; } int num2 = SubmitState.Get(player, entry.Id); int num3 = num - num2; if (num3 <= 0) { num3 = num; num2 = 0; } int val = Math.Max(1, item.m_stack); int num4 = Math.Min(num3, val); if (consume) { ConsumeItems(player, item, num4); } int num5 = num2 + num4; Plugin.Log.LogInfo((object)($"[item_submit] '{entry.Id}' progress {num5}/{num} " + $"(+{num4} {itemPrefabName}, consume={consume}).")); if (num5 >= num) { SubmitState.Clear(player, entry.Id); Plugin.Log.LogInfo((object)$"[item_submit] '{entry.Id}' complete ({num}/{num}) — firing."); GuidanceDispatcher.FireEntry(entry, evt); GuidanceHudTracker.Instance?.FlashCompletion(entry.Id); return; } SubmitState.Set(player, entry.Id, num5); string text2 = ((!string.IsNullOrEmpty(entry.Title)) ? entry.Title : text); ((Character)player).Message((MessageType)2, $"{text2}: {num5}/{num} {text}", 0, (Sprite)null); GuidanceHudTracker.Instance?.Refresh(fromProgress: true); } private static void ConsumeItems(Player player, ItemData item, int amount) { if (amount > 0) { Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory != null) { inventory.RemoveItem(item, amount); ((Character)player).ShowRemovedMessage(item, amount); } } } private static string Localized(ItemData item) { string text = item.m_shared?.m_name ?? ""; return (Localization.instance != null) ? Localization.instance.Localize(text) : text; } private static string ResolveItemName(ItemData item) { GameObject dropPrefab = item.m_dropPrefab; string text = ((dropPrefab != null) ? ((Object)dropPrefab).name : null); if (!string.IsNullOrEmpty(text)) { return TriggerUtils.NormalizePrefabName(text); } return TriggerUtils.NormalizePrefabName(item.m_shared?.m_name ?? ""); } private static bool IsVanillaUseItem(Trader trader, ItemData item) { if (trader.m_useItems == null || trader.m_useItems.Count == 0) { return false; } string text = item.m_shared?.m_name; if (string.IsNullOrEmpty(text)) { return false; } foreach (TraderUseItem useItem in trader.m_useItems) { string text2 = useItem?.m_prefab?.m_itemData?.m_shared?.m_name; if (!string.IsNullOrEmpty(text2) && string.Equals(text, text2, StringComparison.Ordinal)) { return true; } } return false; } internal static GuidanceEntry FindEntry(string npcSubject, string itemPrefabName, Player player) { if (string.IsNullOrEmpty(npcSubject) || (Object)(object)player == (Object)null) { return null; } GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return null; } GuidanceEntry guidanceEntry = null; foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "npc_item_submit", StringComparison.OrdinalIgnoreCase) || !string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase) || !GuidanceDispatcher.CheckGates(guidance, player)) { continue; } if (string.IsNullOrEmpty(guidance.Trigger.Item)) { if (guidanceEntry == null) { guidanceEntry = guidance; } } else if (string.Equals(guidance.Trigger.Item, itemPrefabName, StringComparison.OrdinalIgnoreCase)) { return guidance; } } return guidanceEntry; } internal static bool NpcHasConfiguredEntries(string npcSubject) { GuidanceConfig currentConfig = Plugin.CurrentConfig; if (currentConfig?.Guidances == null) { return false; } foreach (GuidanceEntry guidance in currentConfig.Guidances) { if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "npc_item_submit", StringComparison.OrdinalIgnoreCase) || !string.Equals(guidance.Trigger.Npc, npcSubject, StringComparison.OrdinalIgnoreCase)) { continue; } return true; } return false; } } [HarmonyPatch(typeof(Trader), "GetHoverText")] internal static class NpcItemSubmitHoverPatch { private const string GiveItemLine = "\n[<color=yellow><b>1-8</b></color>] $npc_giveitem"; [HarmonyPostfix] private static void Postfix(Trader __instance, ref string __result) { if (!((Object)(object)Player.m_localPlayer == (Object)null) && (__instance.m_useItems == null || __instance.m_useItems.Count <= 0)) { GameObject gameObject = ((Component)__instance).gameObject; string npcSubject = TriggerUtils.NormalizePrefabName((gameObject != null) ? ((Object)gameObject).name : null); if (NpcItemSubmitTrigger.NpcHasConfiguredEntries(npcSubject)) { string text = ((Localization.instance != null) ? Localization.instance.Localize("\n[<color=yellow><b>1-8</b></color>] $npc_giveitem") : "\n[<color=yellow><b>1-8</b></color>] $npc_giveitem"); __result += text; } } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathTrigger { [HarmonyPostfix] private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { GuidanceDispatcher.Raise(new TriggerEvent { Type = "player_death", Subject = "" }); } } } [HarmonyPatch(typeof(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<SkillType>(text, ignoreCase: true, out SkillType result)) { int num2 = (int)localPlayer.m_skills.GetSkillLevel(result); if (num2 >= num) { Plugin.Log.LogInfo((object)$"[skill_level] Login scan: {text}:{num} (player {num2}) — raising."); GuidanceDispatcher.Raise(new TriggerEvent { Type = "skill_level", Subject = $"{text}:{num}" }); } } } } private static void CollectThreshold(TriggerSpec t, List<(string, int)> list) { if (t != null && string.Equals(t.Type, "skill_level", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(t.Skill) && t.Level > 0) { list.Add((t.Skill, t.Level)); } } } [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<string, Coroutine> _coroutines = new Dictionary<string, Coroutine>(); public static void OnConfigChanged(GuidanceConfig config) { StopAll(); if (config?.Guidances == null) { return; } bool flag = Application.isBatchMode && IsServerOrHost(); bool flag2 = !IsServerOrHost(); foreach (GuidanceEntry guidance in config.Guidances) { if (guidance.Trigger == null || !string.Equals(guidance.Trigger.Type, "timed", StringComparison.OrdinalIgnoreCase)) { continue; } float num = ParseInterval(guidance.Trigger.Interval); if (num <= 0f) { Plugin.Log.LogWarning((object)("[timed] '" + guidance.Id + "' has invalid interval '" + guidance.Trigger.Interval + "'; skipping.")); continue; } bool flag3 = SeenTracker.IsGlobalScope(guidance.Scope); if ((!flag || flag3) && !(flag2 && flag3)) { string id = guidance.Id; string triggerId = guidance.Trigger.Id ?? guidance.Id; Coroutine value = ((MonoBehaviour)Plugin.Instance).StartCoroutine(TimerRoutine(id, triggerId, num, flag3)); _coroutines[id] = value; Plugin.Log.LogInfo((object)string.Format("[timed] scheduled '{0}' every {1}s ({2}).", id, num, flag3 ? "global" : "player")); } } } private static void StopAll() { if ((Object)(object)Plugin.Instance == (Object)null) { return; } foreach (Coroutine value in _coroutines.Values) { if (value != null) { ((MonoBehaviour)Plugin.Instance).StopCoroutine(value); } } _coroutines.Clear(); } 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<string> list = new List<string>(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith(value)) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } public static void ResetAll(Player player) { if (player?.m_customData == null) { return; } List<string> list = new List<string>(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.cp.") || key.StartsWith("VSG.cd.") || key.StartsWith("VSG.cc.") || key.StartsWith("VSG.cv.")) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } public static int GetCompletedVersion(Player player, string chainId) { if (player?.m_customData == null || string.IsNullOrEmpty(chainId)) { return 0; } if (!player.m_customData.TryGetValue("VSG.cv." + chainId, out var value)) { return 0; } int result; return int.TryParse(value, out result) ? result : 0; } public static void SetCompletedVersion(Player player, string chainId, int version) { if (player?.m_customData != null && !string.IsNullOrEmpty(chainId)) { player.m_customData["VSG.cv." + chainId] = version.ToString(); } } private static string CounterKey(string chainId, int stepIndex) { return "VSG.cc." + chainId + ":" + stepIndex; } public static int GetCounter(Player player, string chainId, int stepIndex) { if (player?.m_customData == null || string.IsNullOrEmpty(chainId)) { return -1; } if (!player.m_customData.TryGetValue(CounterKey(chainId, stepIndex), out var value)) { return -1; } int result; return int.TryParse(value, out result) ? result : (-1); } public static void SetCounter(Player player, string chainId, int stepIndex, int value) { if (player?.m_customData != null && !string.IsNullOrEmpty(chainId)) { player.m_customData[CounterKey(chainId, stepIndex)] = value.ToString(); } } public static void ClearCounter(Player player, string chainId, int stepIndex) { player?.m_customData?.Remove(CounterKey(chainId, stepIndex)); } } public static class 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<string> list = new List<string>(); 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<string, List<(string Id, DateTime When)>> _log = new Dictionary<string, List<(string, DateTime)>>(); 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<string> list = new List<string>(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.ig.")) { list.Add(key); } } foreach (string item in list) { player.m_customData.Remove(item); } } } public static class 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<string> list = new List<string>(); foreach (string key in player.m_customData.Keys) { if (key.StartsWith("VSG.kc.")) { list.Add(key); } } foreach (string item in list) { pl