Decompiled source of ClientGhostWatchdog v1.1.0

ClientGhostWatchdog.dll

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

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ClientGhostWatchdog")]
[assembly: AssemblyDescription("Client-side watchdog mod for players connecting to Valheim servers that detects dropped connections and disconnects cleanly, preventing ghost desyncs. For more information, visit https://github.com/dreamwraith")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("dreamwraith (https://github.com/dreamwraith)")]
[assembly: AssemblyProduct("ClientGhostWatchdog")]
[assembly: AssemblyCopyright("Copyright © 2026 dreamwraith")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("e6d8b2f0-1a74-4b52-9b2f-45b08ce74e99")]
[assembly: AssemblyFileVersion("1.1.0")]
[assembly: AssemblyInformationalVersion("1.1.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.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.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.1.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGUID = "dreamwraith.ClientGhostWatchdog";

		public const string ModName = "ClientGhostWatchdog";

		public const string ModVersion = "1.1.0";

		internal static ManualLogSource Log;

		private Harmony? _harmony;

		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;

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

		private void Awake()
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Expected O, but got Unknown
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Expected O, but got Unknown
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Expected O, but got Unknown
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Expected O, but got Unknown
			//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0205: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			TimeoutSeconds = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).Config.Bind<string>("2 - On-Screen Notices", "WarningNoticeMessage", "Server connection lost, attempting to reconnect... ({0}s)", "Large on-screen notice displayed halfway through the check period. {0} is replaced with remaining seconds until disconnect.");
			RecoveredNoticeMessage = ((BaseUnityPlugin)this).Config.Bind<string>("2 - On-Screen Notices", "RecoveredNoticeMessage", "Server connection restored.", "Large on-screen notice displayed if server connection recovers before timing out.");
			DisconnectNoticeMessage = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).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 = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Debug", "EnableDebugLogs", false, "Whether to print detailed debug logs with every watchdog check, timer tick, and network/lifecycle event.");
			DebugSimulateSeverKey = ((BaseUnityPlugin)this).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.");
			_harmony = new Harmony("dreamwraith.ClientGhostWatchdog");
			_harmony.PatchAll();
			Log.LogInfo((object)"ClientGhostWatchdog v1.1.0 loaded successfully.");
		}

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

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

		internal static void SimulateSeveredPeer()
		{
			if (EnableDebugLogs == null || !EnableDebugLogs.Value)
			{
				Log.LogWarning((object)"[DEBUG TEST] Cannot simulate peer disconnect: EnableDebugLogs must be true in config.");
				return;
			}
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				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)
			{
				Log.LogWarning((object)"[DEBUG TEST] Server peer is already null.");
				return;
			}
			Log.LogWarning((object)"[DEBUG TEST] Intentionally calling ZNet.instance.Disconnect(serverPeer) to simulate badly behaved third-party mod behavior...");
			ZNet.instance.Disconnect(serverPeer);
		}

		internal static void LogDebug(string message)
		{
			if (EnableDebugLogs != null && EnableDebugLogs.Value)
			{
				Log.LogInfo((object)("[DEBUG] " + message));
			}
		}
	}
	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 (Plugin.TimeoutSeconds.Value <= 0f)
			{
				Plugin.LogDebug("WatchdogManager.Start: Bypassed. Watchdog disabled (TimeoutSeconds <= 0).");
				return;
			}
			Stop("restarting from " + caller);
			if ((Object)(object)Plugin.Instance != (Object)null)
			{
				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={Plugin.CheckIntervalSeconds.Value}s, Timeout={Plugin.TimeoutSeconds.Value}s.");
			}
		}

		public static void Stop(string reason)
		{
			if (s_watchdogCoroutine != null)
			{
				Plugin.LogDebug("WatchdogManager.Stop: Halting active coroutine. Reason: " + reason + ".");
				if ((Object)(object)Plugin.Instance != (Object)null)
				{
					((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)(object)MessageHud.instance != (Object)null && !string.IsNullOrEmpty(Plugin.DisconnectNoticeMessage.Value))
				{
					MessageHud.instance.ShowMessage((MessageType)2, Plugin.DisconnectNoticeMessage.Value, 0, (Sprite)null, false, true);
				}
				if (Plugin.ShowDisconnectReason.Value)
				{
					DisconnectReasonManager.SetReason(Plugin.DisconnectMessage.Value);
				}
				float num = Mathf.Clamp(Plugin.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)(object)Game.instance != (Object)null && !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, Plugin.CheckIntervalSeconds.Value));
				if (Plugin.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 = Plugin.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 = Plugin.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(Plugin.WarningNoticeMessage.Value))
						{
							string text = FormatNotice(Plugin.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(Plugin.RecoveredNoticeMessage.Value))
					{
						MessageHud.instance.ShowMessage((MessageType)2, Plugin.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 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)(object)startup.m_connectionFailedPanel != (Object)null)
			{
				startup.m_connectionFailedPanel.SetActive(true);
			}
			if ((Object)(object)startup.m_connectionFailedError != (Object)null)
			{
				Component component = ((Component)startup.m_connectionFailedError).GetComponent("Localize");
				Behaviour val = (Behaviour)(object)((component is Behaviour) ? component : null);
				if ((Object)(object)val != (Object)null)
				{
					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.");
			}
		}
	}
	[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(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(ZNet), "Shutdown")]
	public static class ZNet_Shutdown_Patch
	{
		[HarmonyPostfix]
		public static void Postfix()
		{
			Plugin.LogDebug("ZNet.Shutdown postfix triggered.");
			WatchdogManager.Stop("ZNet.Shutdown");
		}
	}
	[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(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)
			{
				Plugin.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
				{
					Plugin.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);
		}
	}
	internal static class VersionInfo
	{
		public const string Version = "1.1.0";
	}
}