Decompiled source of ClientGhostWatchdog v1.2.1

ClientGhostWatchdog.dll

Decompiled 4 days ago
using System;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ClientGhostWatchdog.Configuration;
using ClientGhostWatchdog.Watchdog;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("DreamWraith (https://github.com/dreamwraith)")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 DreamWraith")]
[assembly: AssemblyDescription("Client-side watchdog mod for players connecting to Valheim servers that detects dropped connections and disconnects cleanly to prevent ghost desyncs.")]
[assembly: AssemblyFileVersion("1.2.1.0")]
[assembly: AssemblyInformationalVersion("1.2.1+2697604e950fef13fcc89f7987386f6665a620f2")]
[assembly: AssemblyProduct("ClientGhostWatchdog")]
[assembly: AssemblyTitle("ClientGhostWatchdog")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/dreamwraith/Valheim-ClientGhostWatchdog")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.2.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 ClientGhostWatchdog
{
	[BepInPlugin("dreamwraith.ClientGhostWatchdog", "ClientGhostWatchdog", "1.2.1")]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGUID = "dreamwraith.ClientGhostWatchdog";

		public const string ModName = "ClientGhostWatchdog";

		public const string ModVersion = "1.2.1";

		public const string NetworkPerformanceSystemGUID = "MidnightsFX.NetworkPerformanceSystem";

		internal static ManualLogSource Log;

		private Harmony? _harmony;

		internal static Plugin? Instance { get; private set; }

		private void Awake()
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Invalid comparison between Unknown and I4
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ModConfig.Initialize(((BaseUnityPlugin)this).Config);
			if ((int)SystemInfo.graphicsDeviceType == 4)
			{
				((Behaviour)this).enabled = false;
				return;
			}
			_harmony = new Harmony("dreamwraith.ClientGhostWatchdog");
			_harmony.PatchAll();
			Log.LogInfo((object)"ClientGhostWatchdog v1.2.1 loaded successfully.");
		}

		private void Start()
		{
			if (Chainloader.PluginInfos.ContainsKey("MidnightsFX.NetworkPerformanceSystem"))
			{
				Log.LogInfo((object)"Detected MidnightsFX.NetworkPerformanceSystem; NPS client watchdog stood down in favor of ClientGhostWatchdog.");
			}
		}

		private void OnDestroy()
		{
			WatchdogManager.Stop("Plugin.OnDestroy");
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}

		private void Update()
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			if (ModConfig.EnableDebugLogs.Value && (int)ModConfig.DebugSimulateSeverKey.Value != 0 && Input.GetKeyDown(ModConfig.DebugSimulateSeverKey.Value))
			{
				WatchdogManager.SimulateSeveredPeer();
			}
		}

		internal static void LogDebug(string message)
		{
			if (ModConfig.EnableDebugLogs.Value)
			{
				Log.LogInfo((object)("[DEBUG] " + message));
			}
		}
	}
	internal static class VersionInfo
	{
		public const string Version = "1.2.1";
	}
}
namespace ClientGhostWatchdog.Watchdog
{
	internal static class DisconnectReasonManager
	{
		private static string? s_pendingReason;

		private static float s_reasonTimestamp;

		private const float ReasonTtlSeconds = 300f;

		public static string? PendingReason
		{
			get
			{
				if (string.IsNullOrEmpty(s_pendingReason))
				{
					return null;
				}
				float num = Time.unscaledTime - s_reasonTimestamp;
				if (num > 300f)
				{
					Plugin.LogDebug($"DisconnectReasonManager: Pending reason expired (elapsed {num:F1}s > TTL {300f}s).");
					s_pendingReason = null;
					s_reasonTimestamp = 0f;
					return null;
				}
				return s_pendingReason;
			}
		}

		public static void SetReason(string reason)
		{
			s_pendingReason = reason;
			s_reasonTimestamp = Time.unscaledTime;
			Plugin.LogDebug($"DisconnectReasonManager: Registered pending reason (TTL={300f}s): \"{reason}\"");
		}

		public static void Clear()
		{
			s_pendingReason = null;
			s_reasonTimestamp = 0f;
			Plugin.LogDebug("DisconnectReasonManager: Cleared pending reason.");
		}

		public static void ApplyToMenu(FejdStartup startup)
		{
			string pendingReason = PendingReason;
			if (!string.IsNullOrEmpty(pendingReason))
			{
				ApplyTextAndShowPanel(startup, pendingReason);
				((MonoBehaviour)startup).StartCoroutine(ReinforceNoticeRoutine(startup, pendingReason));
			}
		}

		private static void ApplyTextAndShowPanel(FejdStartup startup, string reason)
		{
			if (Object.op_Implicit((Object)(object)startup.m_connectionFailedPanel))
			{
				startup.m_connectionFailedPanel.SetActive(true);
			}
			if (Object.op_Implicit((Object)(object)startup.m_connectionFailedError))
			{
				Component component = ((Component)startup.m_connectionFailedError).GetComponent("Localize");
				Behaviour val = (Behaviour)(object)((component is Behaviour) ? component : null);
				if (Object.op_Implicit((Object)(object)val))
				{
					val.enabled = false;
				}
				startup.m_connectionFailedError.text = reason;
				Plugin.LogDebug("DisconnectReasonManager: Applied disconnect reason to FejdStartup: \"" + reason + "\"");
			}
		}

		private static IEnumerator ReinforceNoticeRoutine(FejdStartup startup, string reason)
		{
			yield return null;
			if ((Object)(object)startup != (Object)null && (Object)(object)startup.m_connectionFailedError != (Object)null)
			{
				ApplyTextAndShowPanel(startup, reason);
				Plugin.LogDebug("DisconnectReasonManager: Reinforced disconnect reason on next frame.");
			}
		}
	}
	public static class WatchdogManager
	{
		private static Coroutine? s_watchdogCoroutine;

		private static bool s_isWarningActive = false;

		private static int s_lastLoggedRemainingSec = -1;

		private static float s_lastNoticeDisplayTime = 0f;

		private static float s_teleportStartTime = -1f;

		public static bool IsRunning => s_watchdogCoroutine != null;

		public static bool IsWarningActive => s_isWarningActive;

		public static void Start(string caller)
		{
			Plugin.LogDebug("WatchdogManager.Start invoked by '" + caller + "'. Checking eligibility...");
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				object arg = (Object)(object)ZNet.instance == (Object)null;
				ZNet instance = ZNet.instance;
				Plugin.LogDebug($"WatchdogManager.Start: Bypassed. Local world or server host detected (instance null: {arg}, IsServer: {((instance != null) ? new bool?(instance.IsServer()) : ((bool?)null))}).");
				return;
			}
			if (ModConfig.TimeoutSeconds.Value <= 0f)
			{
				Plugin.LogDebug("WatchdogManager.Start: Bypassed. Watchdog disabled (TimeoutSeconds <= 0).");
				return;
			}
			Stop("restarting from " + caller);
			if (Object.op_Implicit((Object)(object)Plugin.Instance))
			{
				s_watchdogCoroutine = ((MonoBehaviour)Plugin.Instance).StartCoroutine(WatchdogRoutine());
				Plugin.Log.LogInfo((object)"[ClientGhostWatchdog] Watchdog coroutine started for server connection.");
				Plugin.LogDebug($"WatchdogManager.Start: Coroutine initiated successfully. Interval={ModConfig.CheckIntervalSeconds.Value}s, Timeout={ModConfig.TimeoutSeconds.Value}s.");
			}
		}

		public static void Stop(string reason)
		{
			if (s_watchdogCoroutine != null)
			{
				Plugin.LogDebug("WatchdogManager.Stop: Halting active coroutine. Reason: " + reason + ".");
				if (Object.op_Implicit((Object)(object)Plugin.Instance))
				{
					((MonoBehaviour)Plugin.Instance).StopCoroutine(s_watchdogCoroutine);
				}
				s_watchdogCoroutine = null;
			}
			else
			{
				Plugin.LogDebug("WatchdogManager.Stop: No active coroutine to stop. Reason: " + reason + ".");
			}
			s_isWarningActive = false;
			s_lastLoggedRemainingSec = -1;
			s_lastNoticeDisplayTime = 0f;
			s_teleportStartTime = -1f;
		}

		private static IEnumerator DisconnectRoutine(string reasonDetail)
		{
			Plugin.Log.LogWarning((object)("[ClientGhostWatchdog] " + reasonDetail + ". Forcing clean logout to preserve player progress."));
			s_isWarningActive = false;
			if ((int)ZNet.GetConnectionStatus() == 2)
			{
				if (Object.op_Implicit((Object)(object)MessageHud.instance) && !string.IsNullOrEmpty(ModConfig.DisconnectNoticeMessage.Value))
				{
					MessageHud.instance.ShowMessage((MessageType)2, ModConfig.DisconnectNoticeMessage.Value, 0, (Sprite)null, false, true);
				}
				if (ModConfig.ShowDisconnectReason.Value)
				{
					DisconnectReasonManager.SetReason(ModConfig.DisconnectMessage.Value);
				}
				float num = Mathf.Clamp(ModConfig.DisconnectDelaySeconds?.Value ?? 5f, 0f, 30f);
				if (num > 0f)
				{
					yield return (object)new WaitForSecondsRealtime(num);
				}
				ZNet.SetExternalError((ConnectionStatus)4);
			}
			else
			{
				Plugin.LogDebug($"DisconnectRoutine: Preserving existing native connection status ({ZNet.GetConnectionStatus()}).");
			}
			s_watchdogCoroutine = null;
			if (Object.op_Implicit((Object)(object)Game.instance) && !Game.instance.IsShuttingDown())
			{
				Game.instance.Logout(true, true);
			}
		}

		private static IEnumerator WatchdogRoutine()
		{
			Plugin.LogDebug("WatchdogManager: Coroutine execution loop entered.");
			float timeSinceLastPing;
			float value2;
			while (true)
			{
				yield return (object)new WaitForSecondsRealtime(Mathf.Max(1f, ModConfig.CheckIntervalSeconds.Value));
				if (ModConfig.TimeoutSeconds.Value <= 0f)
				{
					Plugin.LogDebug("Watchdog tick: TimeoutSeconds <= 0 (watchdog disabled via config).");
					s_isWarningActive = false;
					continue;
				}
				if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
				{
					object arg = (Object)(object)ZNet.instance == (Object)null;
					ZNet instance = ZNet.instance;
					Plugin.LogDebug($"Watchdog tick: Terminating. Local world or server host detected (instance null: {arg}, IsServer: {((instance != null) ? new bool?(instance.IsServer()) : ((bool?)null))}).");
					s_watchdogCoroutine = null;
					yield break;
				}
				if ((Object)(object)Game.instance == (Object)null || Game.instance.IsShuttingDown())
				{
					Plugin.LogDebug("Watchdog tick: Terminating. Game instance is null or shutting down.");
					s_watchdogCoroutine = null;
					yield break;
				}
				if ((int)ZNet.GetConnectionStatus() != 2)
				{
					if ((Object)(object)Player.m_localPlayer != (Object)null)
					{
						yield return DisconnectRoutine($"Connection status lost ({ZNet.GetConnectionStatus()})");
						yield break;
					}
					Plugin.LogDebug($"Watchdog tick: Initial connection status is {ZNet.GetConnectionStatus()} before player spawn. Terminating.");
					s_watchdogCoroutine = null;
					yield break;
				}
				if ((Object)(object)Player.m_localPlayer == (Object)null)
				{
					Plugin.LogDebug("Watchdog tick: Waiting for local player to spawn into world...");
					continue;
				}
				if (((Character)Player.m_localPlayer).IsTeleporting())
				{
					float value = ModConfig.TeleportTimeoutSeconds.Value;
					if (value <= 0f)
					{
						Plugin.LogDebug("Watchdog tick: Skipping check during player teleportation (ceiling disabled via config).");
						continue;
					}
					if (s_teleportStartTime < 0f)
					{
						s_teleportStartTime = Time.unscaledTime;
					}
					float num = Time.unscaledTime - s_teleportStartTime;
					if (num < value)
					{
						Plugin.LogDebug($"Watchdog tick: Skipping check during player teleportation (elapsed: {num:F1}s / max: {value:F1}s).");
						continue;
					}
					Plugin.Log.LogWarning((object)$"[ClientGhostWatchdog] Player has been in teleporting state for {num:F1}s (ceiling: {value:F1}s). Resuming connection health checks.");
				}
				else
				{
					s_teleportStartTime = -1f;
				}
				ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
				if (serverPeer == null || serverPeer.m_rpc == null)
				{
					yield return DisconnectRoutine("Server peer connection lost (disposed by network layer)");
					yield break;
				}
				bool flag = !serverPeer.m_rpc.IsConnected();
				if (flag)
				{
					yield return DisconnectRoutine("Server socket connection closed");
					yield break;
				}
				timeSinceLastPing = serverPeer.m_rpc.GetTimeSinceLastPing();
				value2 = ModConfig.TimeoutSeconds.Value;
				float num2 = value2 / 2f;
				Plugin.LogDebug($"Watchdog check: pingSilence={timeSinceLastPing:F2}s, socketClosed={flag}, warningThreshold={num2:F1}s, timeout={value2:F1}s, warningActive={s_isWarningActive}");
				if (timeSinceLastPing >= value2)
				{
					break;
				}
				if (timeSinceLastPing >= num2)
				{
					int num3 = Mathf.Max(1, Mathf.CeilToInt(value2 - timeSinceLastPing));
					if (!s_isWarningActive)
					{
						s_isWarningActive = true;
						Plugin.Log.LogWarning((object)$"[ClientGhostWatchdog] Server silence detected ({timeSinceLastPing:F1}s >= halfway threshold {num2:F1}s). Displaying reconnect warning.");
						Plugin.LogDebug($"Watchdog trigger: Stage 1 warning activated. remainingSec={num3}s.");
					}
					float unscaledTime = Time.unscaledTime;
					if (num3 != s_lastLoggedRemainingSec || unscaledTime - s_lastNoticeDisplayTime >= 1.5f)
					{
						s_lastLoggedRemainingSec = num3;
						s_lastNoticeDisplayTime = unscaledTime;
						if ((Object)(object)MessageHud.instance != (Object)null && !string.IsNullOrEmpty(ModConfig.WarningNoticeMessage.Value))
						{
							string text = FormatNotice(ModConfig.WarningNoticeMessage.Value, num3);
							MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
							Plugin.LogDebug("Watchdog notice refreshed: \"" + text + "\"");
						}
					}
				}
				else if (s_isWarningActive)
				{
					s_isWarningActive = false;
					s_lastLoggedRemainingSec = -1;
					Plugin.Log.LogInfo((object)$"[ClientGhostWatchdog] Server connection recovered after {timeSinceLastPing:F1}s silence.");
					Plugin.LogDebug($"Watchdog trigger: Recovery detected (silence dropped to {timeSinceLastPing:F2}s). Warning cleared.");
					if ((Object)(object)MessageHud.instance != (Object)null && !string.IsNullOrEmpty(ModConfig.RecoveredNoticeMessage.Value))
					{
						MessageHud.instance.ShowMessage((MessageType)2, ModConfig.RecoveredNoticeMessage.Value, 0, (Sprite)null, false, true);
					}
				}
			}
			yield return DisconnectRoutine($"Server stopped responding to pings for {timeSinceLastPing:F1}s (threshold: {value2:F1}s)");
		}

		private static string FormatNotice(string template, int remainingSec)
		{
			if (string.IsNullOrEmpty(template))
			{
				return string.Empty;
			}
			try
			{
				return template.Contains("{0}") ? string.Format(template, remainingSec) : template;
			}
			catch (FormatException)
			{
				return template;
			}
		}

		internal static void SimulateSeveredPeer()
		{
			if (!ModConfig.EnableDebugLogs.Value)
			{
				Plugin.Log.LogWarning((object)"[DEBUG TEST] Cannot simulate peer disconnect: EnableDebugLogs must be true in config.");
				return;
			}
			if (!Object.op_Implicit((Object)(object)ZNet.instance) || ZNet.instance.IsServer() || !Object.op_Implicit((Object)(object)Player.m_localPlayer))
			{
				Plugin.Log.LogWarning((object)"[DEBUG TEST] Cannot simulate peer disconnect: Must be in an active multiplayer session as a client.");
				return;
			}
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (serverPeer == null)
			{
				Plugin.Log.LogWarning((object)"[DEBUG TEST] Server peer is already null.");
				return;
			}
			Plugin.Log.LogWarning((object)"[DEBUG TEST] Intentionally calling ZNet.instance.Disconnect(serverPeer) to simulate badly behaved third-party mod behavior...");
			ZNet.instance.Disconnect(serverPeer);
		}
	}
}
namespace ClientGhostWatchdog.Patches
{
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	public static class FejdStartup_ShowConnectError_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(FejdStartup __instance)
		{
			Plugin.LogDebug("FejdStartup.ShowConnectError postfix triggered.");
			WatchdogManager.Stop("FejdStartup.ShowConnectError");
			DisconnectReasonManager.ApplyToMenu(__instance);
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "Start")]
	public static class FejdStartup_Start_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(FejdStartup __instance)
		{
			Plugin.LogDebug("FejdStartup.Start postfix triggered.");
			WatchdogManager.Stop("FejdStartup.Start");
			DisconnectReasonManager.ApplyToMenu(__instance);
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "OnConnectionFailedOk")]
	public static class FejdStartup_OnConnectionFailedOk_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(FejdStartup __instance)
		{
			Plugin.LogDebug("FejdStartup.OnConnectionFailedOk postfix triggered.");
			if ((Object)(object)__instance.m_connectionFailedError != (Object)null)
			{
				Component component = ((Component)__instance.m_connectionFailedError).GetComponent("Localize");
				Behaviour val = (Behaviour)(object)((component is Behaviour) ? component : null);
				if ((Object)(object)val != (Object)null)
				{
					val.enabled = true;
				}
			}
			DisconnectReasonManager.Clear();
		}
	}
	[HarmonyPatch(typeof(Game), "Start")]
	public static class Game_Start_Patch
	{
		[HarmonyPostfix]
		public static void Postfix()
		{
			Plugin.LogDebug("Game.Start postfix triggered.");
			DisconnectReasonManager.Clear();
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				Plugin.LogDebug("Game.Start: Local world or server host detected; ignoring watchdog start.");
			}
			else
			{
				WatchdogManager.Start("Game.Start");
			}
		}
	}
	[HarmonyPatch(typeof(Game), "OnDestroy")]
	public static class Game_OnDestroy_Patch
	{
		[HarmonyPostfix]
		public static void Postfix()
		{
			Plugin.LogDebug("Game.OnDestroy postfix triggered.");
			WatchdogManager.Stop("Game.OnDestroy");
		}
	}
	[HarmonyPatch(typeof(Terminal), "InitTerminal")]
	public static class Terminal_InitTerminal_Patch
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__0_0;

			internal void <Postfix>b__0_0(ConsoleEventArgs args)
			{
				WatchdogManager.SimulateSeveredPeer();
			}
		}

		[HarmonyPostfix]
		public static void Postfix()
		{
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Expected O, but got Unknown
			if (Terminal.commands.ContainsKey("cgw_simulatesever"))
			{
				return;
			}
			object obj = <>c.<>9__0_0;
			if (obj == null)
			{
				ConsoleEvent val = delegate
				{
					WatchdogManager.SimulateSeveredPeer();
				};
				<>c.<>9__0_0 = val;
				obj = (object)val;
			}
			new ConsoleCommand("cgw_simulatesever", "Simulates an abrupt external peer severance (ServerCharacters behavior) for watchdog testing.", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	public static class ZNet_RPC_PeerInfo_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(ZNet __instance)
		{
			Plugin.LogDebug("ZNet.RPC_PeerInfo postfix triggered.");
			if (__instance.IsServer())
			{
				Plugin.LogDebug("ZNet.RPC_PeerInfo: Host is server; ignoring watchdog start.");
			}
			else
			{
				WatchdogManager.Start("ZNet.RPC_PeerInfo");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Shutdown")]
	public static class ZNet_Shutdown_Patch
	{
		[HarmonyPostfix]
		public static void Postfix()
		{
			Plugin.LogDebug("ZNet.Shutdown postfix triggered.");
			WatchdogManager.Stop("ZNet.Shutdown");
		}
	}
}
namespace ClientGhostWatchdog.Configuration
{
	internal static class ModConfig
	{
		public static ConfigEntry<float> TimeoutSeconds;

		public static ConfigEntry<float> CheckIntervalSeconds;

		public static ConfigEntry<float> TeleportTimeoutSeconds;

		public static ConfigEntry<string> WarningNoticeMessage;

		public static ConfigEntry<string> RecoveredNoticeMessage;

		public static ConfigEntry<string> DisconnectNoticeMessage;

		public static ConfigEntry<float> DisconnectDelaySeconds;

		public static ConfigEntry<bool> ShowDisconnectReason;

		public static ConfigEntry<string> DisconnectMessage;

		public static ConfigEntry<bool> EnableDebugLogs;

		public static ConfigEntry<KeyCode> DebugSimulateSeverKey;

		public static void Initialize(ConfigFile config)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Expected O, but got Unknown
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Expected O, but got Unknown
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Expected O, but got Unknown
			//IL_012e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Expected O, but got Unknown
			TimeoutSeconds = config.Bind<float>("1 - General", "TimeoutSeconds", 30f, new ConfigDescription("Total seconds of server silence before disconnecting to prevent zombie/ghost client desyncs. Halfway through this time (e.g. 15s at default 30s), a warning is displayed. Set to 0 to disable.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 300f), Array.Empty<object>()));
			CheckIntervalSeconds = config.Bind<float>("1 - General", "CheckIntervalSeconds", 1f, new ConfigDescription("Interval in seconds between watchdog checks in the background coroutine timer.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f), Array.Empty<object>()));
			TeleportTimeoutSeconds = config.Bind<float>("1 - General", "TeleportTimeoutSeconds", 30f, new ConfigDescription("Maximum seconds a player can remain in a teleporting/portal state before connection health checks resume. Prevents infinite limbo if a portal handshake hangs. Set to 0 to disable ceiling.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 180f), Array.Empty<object>()));
			WarningNoticeMessage = config.Bind<string>("2 - On-Screen Notices", "WarningNoticeMessage", "Server connection lost, attempting to reconnect... ({0}s)", "Large on-screen notice displayed halfway through TimeoutSeconds. {0} is replaced with remaining seconds until disconnect.");
			RecoveredNoticeMessage = config.Bind<string>("2 - On-Screen Notices", "RecoveredNoticeMessage", "Server connection restored.", "Large on-screen notice displayed if server connection recovers before timing out.");
			DisconnectNoticeMessage = config.Bind<string>("2 - On-Screen Notices", "DisconnectNoticeMessage", "Server connection lost. Disconnecting...", "Large on-screen notice displayed when the timeout is reached and the player is being disconnected.");
			DisconnectDelaySeconds = config.Bind<float>("2 - On-Screen Notices", "DisconnectDelaySeconds", 5f, new ConfigDescription("Seconds to wait after displaying the on-screen disconnect notice before executing logout, giving players time to read the notice. Set to 0 for instantaneous logout.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>()));
			ShowDisconnectReason = config.Bind<bool>("3 - Main Menu Dialog", "ShowDisconnectReason", true, "Whether to display an explanatory message on the main menu dialog after being disconnected by this watchdog.");
			DisconnectMessage = config.Bind<string>("3 - Main Menu Dialog", "DisconnectMessage", "Server connection lost (Ghost connection prevented). Progress was saved.", "Custom message displayed on the main menu dialog when disconnected by this watchdog.");
			EnableDebugLogs = config.Bind<bool>("4 - Debug", "EnableDebugLogs", false, "Whether to print detailed debug logs with every watchdog check, timer tick, and network/lifecycle event.");
			DebugSimulateSeverKey = config.Bind<KeyCode>("4 - Debug", "DebugSimulateSeverKey", (KeyCode)0, "DEBUG ONLY: Keybind to simulate an abrupt external peer disconnection (ServerCharacters behavior). Requires EnableDebugLogs to be true. Keep as None for normal play.");
		}
	}
}