Decompiled source of QuickRunReload v1.2.0

BepInEx/plugins/QuickRunReload/QuickRunReload.dll

Decompiled 2 weeks ago
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.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: AssemblyProduct("QuickRunReload")]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyCompany("qup0ix")]
[assembly: AssemblyFileVersion("1.2.0.0")]
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyTitle("QuickRunReload")]
[assembly: AssemblyDescription("Pre-landing checkpoint and host-only wipe reload for Lethal Company.")]
[assembly: AssemblyVersion("1.2.0.0")]
namespace QuickRunReload;

[BepInPlugin("qup0ix.QuickRunReload", "QuickRunReload", "1.2.0")]
public sealed class Plugin : BaseUnityPlugin
{
	public const string PluginGuid = "qup0ix.QuickRunReload";

	public const string PluginName = "QuickRunReload";

	public const string PluginVersion = "1.2.0";

	private Harmony _harmony;

	private ConfigEntry<KeyboardShortcut> _reloadHotkey;

	private ConfigEntry<bool> _requireFullWipe;

	private ChatCommandHandler _chatCommandHandler;

	private AutoRestartMonitor _autoRestartMonitor;

	private CheckpointManager _checkpoints;

	private ReloadController _reloadController;

	internal static Plugin Instance { get; private set; }

	internal static ManualLogSource Log { get; private set; }

	private void Awake()
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0149: Unknown result type (might be due to invalid IL or missing references)
		//IL_0153: Expected O, but got Unknown
		//IL_0187: Unknown result type (might be due to invalid IL or missing references)
		Instance = this;
		Log = ((BaseUnityPlugin)this).Logger;
		_reloadHotkey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "ReloadHotkey", new KeyboardShortcut((KeyCode)289, (KeyCode[])(object)new KeyCode[0]), "Host-only hotkey that restores the pre-landing checkpoint and reloads the same save.");
		_requireFullWipe = ((BaseUnityPlugin)this).Config.Bind<bool>("Safety", "RequireAllPlayersDead", true, "When true, reload requests are accepted only after the whole crew is dead.");
		ConfigEntry<bool> enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "EnableChatCommand", true, "Enables the host-only chat command that requests the same run reload as the hotkey.");
		ConfigEntry<string> command = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "ChatCommand", "/restart", "Exact chat message used to request a run reload. Comparison ignores case and surrounding whitespace.");
		ConfigEntry<bool> hideCommandMessage = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "HideCommandMessage", true, "When true, the matched command is consumed locally instead of being sent as a normal chat message.");
		ConfigEntry<bool> enabled2 = ((BaseUnityPlugin)this).Config.Bind<bool>("AutoRestart", "EnableAutoRestart", false, "Automatically reload the pre-landing checkpoint when all active players are dead.");
		ConfigEntry<float> delay = ((BaseUnityPlugin)this).Config.Bind<float>("AutoRestart", "AutoRestartDelay", 3f, "Seconds to wait after all active players are confirmed dead before automatically reloading the checkpoint.");
		_checkpoints = new CheckpointManager();
		_reloadController = new ReloadController(this, _checkpoints, _requireFullWipe);
		_chatCommandHandler = new ChatCommandHandler(enabled, command, hideCommandMessage);
		_autoRestartMonitor = new AutoRestartMonitor(this, _reloadController, _checkpoints, enabled2, delay);
		_harmony = new Harmony("qup0ix.QuickRunReload");
		_harmony.PatchAll(typeof(Plugin).Assembly);
		Log.LogInfo((object)string.Format("{0} {1} loaded. Reload hotkey: {2}", "QuickRunReload", "1.2.0", _reloadHotkey.Value));
		_chatCommandHandler.ValidateConfiguration();
		LogCompatibilityInformation();
	}

	private void Update()
	{
		//IL_000e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		if (_reloadHotkey != null)
		{
			KeyboardShortcut value = _reloadHotkey.Value;
			if (((KeyboardShortcut)(ref value)).IsDown())
			{
				RequestRestart("hotkey");
			}
		}
		if (_autoRestartMonitor != null)
		{
			_autoRestartMonitor.Tick();
		}
	}

	private void OnDestroy()
	{
		if (_harmony != null)
		{
			_harmony.UnpatchSelf();
		}
	}

	internal void CaptureCheckpoint(StartOfRound round)
	{
		if (_reloadController.IsReloadInProgress)
		{
			return;
		}
		GameNetworkManager instance = GameNetworkManager.Instance;
		if ((Object)(object)instance == (Object)null || (Object)(object)round == (Object)null || !instance.isHostingGame || !round.inShipPhase || round.beganLoadingNewLevel)
		{
			return;
		}
		NetworkManager singleton = NetworkManager.Singleton;
		if ((Object)(object)singleton == (Object)null || !singleton.IsHost)
		{
			return;
		}
		if (string.Equals(instance.currentSaveFileName, "LCChallengeFile", StringComparison.Ordinal))
		{
			Log.LogWarning((object)"Challenge saves are not checkpointed.");
			return;
		}
		try
		{
			SavePreLandingState(instance);
			_checkpoints.Create(instance.currentSaveFileName, instance.saveFileNum);
		}
		catch (Exception ex)
		{
			Log.LogError((object)"Failed to create the pre-landing checkpoint. Reload requests are disabled for this landing.");
			Log.LogError((object)ex);
			_checkpoints.Invalidate();
		}
	}

	internal bool ShouldSuppressSaveGame()
	{
		if (_reloadController != null)
		{
			return _reloadController.SuppressSaveGame;
		}
		return false;
	}

	internal bool HandleChatSubmit(HUDManager hudManager, CallbackContext context)
	{
		//IL_000f: Unknown result type (might be due to invalid IL or missing references)
		if (_chatCommandHandler != null)
		{
			return _chatCommandHandler.RunOriginalAfterSubmit(hudManager, context);
		}
		return true;
	}

	internal bool HandleChatEnable(HUDManager hudManager, CallbackContext context)
	{
		//IL_000f: Unknown result type (might be due to invalid IL or missing references)
		if (_chatCommandHandler != null)
		{
			return _chatCommandHandler.RunOriginalAfterEnable(hudManager, context);
		}
		return true;
	}

	internal bool RequestRestart(string source, bool activePlayersVerifiedDead = false)
	{
		if (_reloadController != null)
		{
			return _reloadController.TryBeginReload(source, activePlayersVerifiedDead);
		}
		return false;
	}

	private static void SavePreLandingState(GameNetworkManager network)
	{
		FieldInfo fieldInfo = null;
		bool flag = true;
		try
		{
			Type type = Type.GetType("LobbyControl.LobbyControl, LobbyControl", throwOnError: false);
			if (type != null)
			{
				fieldInfo = type.GetField("CanSave", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				if (fieldInfo != null && fieldInfo.FieldType == typeof(bool))
				{
					flag = (bool)fieldInfo.GetValue(null);
					if (!flag)
					{
						Log.LogInfo((object)"Temporarily allowing LobbyControl save for the pre-landing checkpoint.");
						fieldInfo.SetValue(null, true);
					}
				}
			}
			network.SaveGame();
		}
		finally
		{
			if (fieldInfo != null && !flag)
			{
				fieldInfo.SetValue(null, false);
			}
		}
	}

	private static void LogCompatibilityInformation()
	{
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			string text = pluginInfo.Value.Metadata.Name ?? string.Empty;
			if (text.IndexOf("LobbyControl", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Save Before Landing", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("SaveBeforeLanding", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("MoreCompany", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("LateCompany", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				Log.LogInfo((object)$"Detected compatibility mod: {text} {pluginInfo.Value.Metadata.Version}");
			}
		}
	}
}
internal sealed class AutoRestartMonitor
{
	private enum IgnoreReason
	{
		None,
		NotHost,
		ReloadInProgress,
		NotInActiveLanding
	}

	private const float PollInterval = 0.25f;

	private readonly Plugin _plugin;

	private readonly ReloadController _reloadController;

	private readonly CheckpointManager _checkpoints;

	private readonly ConfigEntry<bool> _enabled;

	private readonly ConfigEntry<float> _delay;

	private float _nextPollTime;

	private float _restartAt;

	private bool _restartScheduled;

	private IgnoreReason _lastIgnoreReason;

	internal AutoRestartMonitor(Plugin plugin, ReloadController reloadController, CheckpointManager checkpoints, ConfigEntry<bool> enabled, ConfigEntry<float> delay)
	{
		_plugin = plugin;
		_reloadController = reloadController;
		_checkpoints = checkpoints;
		_enabled = enabled;
		_delay = delay;
	}

	internal void Tick()
	{
		if (!_enabled.Value)
		{
			ResetTracking();
			return;
		}
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (realtimeSinceStartup < _nextPollTime)
		{
			return;
		}
		_nextPollTime = realtimeSinceStartup + 0.25f;
		GameNetworkManager instance = GameNetworkManager.Instance;
		NetworkManager singleton = NetworkManager.Singleton;
		StartOfRound instance2 = StartOfRound.Instance;
		if ((Object)(object)instance == (Object)null || (Object)(object)singleton == (Object)null || (Object)(object)instance2 == (Object)null)
		{
			Block(IgnoreReason.NotInActiveLanding);
			return;
		}
		if (!instance.isHostingGame || !singleton.IsHost)
		{
			Block(IgnoreReason.NotHost);
			return;
		}
		if (_reloadController.IsReloadInProgress)
		{
			Block(IgnoreReason.ReloadInProgress);
			return;
		}
		if (!IsActiveLanding(instance, singleton, instance2))
		{
			Block(IgnoreReason.NotInActiveLanding);
			return;
		}
		CountActivePlayers(instance2, singleton, out var activePlayers, out var livingPlayers);
		if (activePlayers == 0)
		{
			Block(IgnoreReason.NotInActiveLanding);
			return;
		}
		_lastIgnoreReason = IgnoreReason.None;
		if (livingPlayers > 0)
		{
			if (_restartScheduled)
			{
				Plugin.Log.LogInfo((object)"Auto restart cancelled because a player is alive");
			}
			_restartScheduled = false;
			return;
		}
		if (!_restartScheduled)
		{
			float num = _delay.Value;
			if (num < 0f || float.IsNaN(num) || float.IsInfinity(num))
			{
				num = 0f;
			}
			_restartScheduled = true;
			_restartAt = realtimeSinceStartup + num;
			Plugin.Log.LogInfo((object)"All active players are dead");
			Plugin.Log.LogInfo((object)string.Format(CultureInfo.InvariantCulture, "Auto restart scheduled in {0:0.###} seconds", num));
		}
		if (!(realtimeSinceStartup < _restartAt))
		{
			_restartScheduled = false;
			Plugin.Log.LogInfo((object)"Auto restart triggered");
			_plugin.RequestRestart("auto restart", activePlayersVerifiedDead: true);
		}
	}

	private bool IsActiveLanding(GameNetworkManager gameNetwork, NetworkManager netcode, StartOfRound round)
	{
		if (netcode.IsListening && ((NetworkBehaviour)round).IsSpawned && gameNetwork.gameHasStarted && !round.inShipPhase && round.shipHasLanded && !round.shipIsLeaving)
		{
			return _checkpoints.Matches(gameNetwork.currentSaveFileName);
		}
		return false;
	}

	private static void CountActivePlayers(StartOfRound round, NetworkManager netcode, out int activePlayers, out int livingPlayers)
	{
		activePlayers = 0;
		livingPlayers = 0;
		PlayerControllerB[] allPlayerScripts = round.allPlayerScripts;
		if (allPlayerScripts == null)
		{
			return;
		}
		foreach (PlayerControllerB val in allPlayerScripts)
		{
			if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).IsSpawned && !val.disconnectedMidGame && (val.isPlayerControlled || val.isPlayerDead) && netcode.ConnectedClients.ContainsKey(val.actualClientId))
			{
				activePlayers++;
				if (!val.isPlayerDead)
				{
					livingPlayers++;
				}
			}
		}
	}

	private void Block(IgnoreReason reason)
	{
		_restartScheduled = false;
		if (_lastIgnoreReason != reason)
		{
			_lastIgnoreReason = reason;
			switch (reason)
			{
			case IgnoreReason.NotHost:
				Plugin.Log.LogInfo((object)"Auto restart ignored: local player is not host");
				break;
			case IgnoreReason.ReloadInProgress:
				Plugin.Log.LogInfo((object)"Auto restart ignored: reload already in progress");
				break;
			case IgnoreReason.NotInActiveLanding:
				Plugin.Log.LogInfo((object)"Auto restart ignored: not in active landing");
				break;
			}
		}
	}

	private void ResetTracking()
	{
		_restartScheduled = false;
		_lastIgnoreReason = IgnoreReason.None;
		_nextPollTime = 0f;
	}
}
internal sealed class ChatCommandHandler
{
	private readonly ConfigEntry<bool> _enabled;

	private readonly ConfigEntry<string> _command;

	private readonly ConfigEntry<bool> _hideCommandMessage;

	private bool _warnedAboutEmptyCommand;

	internal ChatCommandHandler(ConfigEntry<bool> enabled, ConfigEntry<string> command, ConfigEntry<bool> hideCommandMessage)
	{
		_enabled = enabled;
		_command = command;
		_hideCommandMessage = hideCommandMessage;
	}

	internal void ValidateConfiguration()
	{
		GetConfiguredCommand();
	}

	internal bool RunOriginalAfterEnable(HUDManager hudManager, CallbackContext context)
	{
		if (!_enabled.Value || !((CallbackContext)(ref context)).performed || (Object)(object)hudManager == (Object)null || GetConfiguredCommand() == null)
		{
			return true;
		}
		GameNetworkManager instance = GameNetworkManager.Instance;
		StartOfRound instance2 = StartOfRound.Instance;
		if (!IsActiveHost() || (Object)(object)instance == (Object)null || (Object)(object)instance.localPlayerController == (Object)null || (Object)(object)instance2 == (Object)null || instance2.inShipPhase || !instance.localPlayerController.isPlayerDead)
		{
			return true;
		}
		hudManager.localPlayer = instance.localPlayerController;
		if (hudManager.localPlayer.inTerminalMenu || (Object)(object)hudManager.chatTextField == (Object)null)
		{
			return true;
		}
		hudManager.localPlayer.isTypingChat = true;
		((Selectable)hudManager.chatTextField).Select();
		hudManager.chatTextField.ActivateInputField();
		hudManager.PingHUDElement(hudManager.Chat, 0.1f, 1f, 1f);
		if ((Object)(object)hudManager.typingIndicator != (Object)null)
		{
			((Behaviour)hudManager.typingIndicator).enabled = true;
		}
		return false;
	}

	internal bool RunOriginalAfterSubmit(HUDManager hudManager, CallbackContext context)
	{
		if (!_enabled.Value || !((CallbackContext)(ref context)).performed || (Object)(object)hudManager == (Object)null || (Object)(object)hudManager.chatTextField == (Object)null)
		{
			return true;
		}
		string configuredCommand = GetConfiguredCommand();
		if (configuredCommand == null)
		{
			return true;
		}
		string text = hudManager.chatTextField.text;
		if (text == null || !string.Equals(text.Trim(), configuredCommand, StringComparison.OrdinalIgnoreCase))
		{
			if (IsDeadActiveHost())
			{
				CloseChatInput(hudManager);
				return false;
			}
			return true;
		}
		Plugin.Log.LogInfo((object)"Chat command detected");
		if (!IsActiveHost())
		{
			Plugin.Log.LogWarning((object)"Chat command ignored: local player is not host");
			if (_hideCommandMessage.Value)
			{
				CloseChatInput(hudManager);
				return false;
			}
			return true;
		}
		Plugin.Log.LogInfo((object)"Restart requested by chat command");
		Plugin.Instance.RequestRestart("chat command");
		if (!_hideCommandMessage.Value && !IsDeadActiveHost())
		{
			return true;
		}
		CloseChatInput(hudManager);
		return false;
	}

	private string GetConfiguredCommand()
	{
		string value = _command.Value;
		if (string.IsNullOrWhiteSpace(value))
		{
			if (!_warnedAboutEmptyCommand)
			{
				Plugin.Log.LogWarning((object)"ChatCommand is empty or whitespace; the chat command is disabled until a non-empty value is configured.");
				_warnedAboutEmptyCommand = true;
			}
			return null;
		}
		return value.Trim();
	}

	private static bool IsActiveHost()
	{
		NetworkManager singleton = NetworkManager.Singleton;
		GameNetworkManager instance = GameNetworkManager.Instance;
		if ((Object)(object)singleton != (Object)null && singleton.IsHost && (Object)(object)instance != (Object)null)
		{
			return instance.isHostingGame;
		}
		return false;
	}

	private static bool IsDeadActiveHost()
	{
		GameNetworkManager instance = GameNetworkManager.Instance;
		if (IsActiveHost() && (Object)(object)instance != (Object)null && (Object)(object)instance.localPlayerController != (Object)null)
		{
			return instance.localPlayerController.isPlayerDead;
		}
		return false;
	}

	private static void CloseChatInput(HUDManager hudManager)
	{
		GameNetworkManager instance = GameNetworkManager.Instance;
		if ((Object)(object)instance != (Object)null && (Object)(object)instance.localPlayerController != (Object)null)
		{
			instance.localPlayerController.isTypingChat = false;
		}
		if ((Object)(object)hudManager.chatTextField != (Object)null)
		{
			hudManager.chatTextField.text = string.Empty;
			hudManager.chatTextField.DeactivateInputField(false);
		}
		if ((Object)(object)hudManager.typingIndicator != (Object)null)
		{
			((Behaviour)hudManager.typingIndicator).enabled = false;
		}
	}
}
internal sealed class CheckpointManager
{
	private sealed class CheckpointFile
	{
		internal string OriginalPath;

		internal string BackupPath;
	}

	private readonly string _rootDirectory;

	private readonly List<CheckpointFile> _files = new List<CheckpointFile>();

	internal string SaveFileName { get; private set; }

	internal int SaveFileNumber { get; private set; }

	internal DateTime CreatedUtc { get; private set; }

	internal bool IsValid { get; private set; }

	internal CheckpointManager()
	{
		_rootDirectory = Path.Combine(Paths.ConfigPath, "QuickRunReload", "checkpoint");
	}

	internal bool Matches(string saveFileName)
	{
		if (IsValid)
		{
			return string.Equals(SaveFileName, saveFileName, StringComparison.Ordinal);
		}
		return false;
	}

	internal void Invalidate()
	{
		IsValid = false;
		_files.Clear();
		SaveFileName = null;
	}

	internal void Create(string saveFileName, int saveFileNumber)
	{
		if (string.IsNullOrEmpty(saveFileName))
		{
			throw new InvalidOperationException("GameNetworkManager.currentSaveFileName is empty.");
		}
		string persistentDataPath = Application.persistentDataPath;
		string text = Path.Combine(persistentDataPath, saveFileName);
		if (!File.Exists(text))
		{
			throw new FileNotFoundException("The active save file does not exist after SaveGame().", text);
		}
		string text2 = _rootDirectory + ".staging";
		RecreateOwnedDirectory(text2);
		List<string> list = new List<string>();
		list.Add(text);
		string[] files = Directory.GetFiles(persistentDataPath, saveFileName + ".*");
		foreach (string text3 in files)
		{
			if (!list.Contains<string>(text3, StringComparer.OrdinalIgnoreCase) && !text3.EndsWith(".qrr.restore.tmp", StringComparison.OrdinalIgnoreCase))
			{
				list.Add(text3);
			}
		}
		foreach (string item in list)
		{
			string destFileName = Path.Combine(text2, Path.GetFileName(item));
			File.Copy(item, destFileName, overwrite: true);
		}
		string contents = string.Format("version=1{0}saveBase64={1}{0}saveFileNumber={2}{0}createdUtc={3:o}{0}files={4}{0}", Environment.NewLine, Convert.ToBase64String(Encoding.UTF8.GetBytes(saveFileName)), saveFileNumber, DateTime.UtcNow, string.Join("|", list.Select(Path.GetFileName).ToArray()));
		File.WriteAllText(Path.Combine(text2, "checkpoint.meta"), contents, Encoding.UTF8);
		RecreateOwnedDirectory(_rootDirectory);
		string[] files2 = Directory.GetFiles(text2);
		foreach (string text4 in files2)
		{
			File.Move(text4, Path.Combine(_rootDirectory, Path.GetFileName(text4)));
		}
		Directory.Delete(text2, recursive: false);
		_files.Clear();
		foreach (string item2 in list)
		{
			_files.Add(new CheckpointFile
			{
				OriginalPath = item2,
				BackupPath = Path.Combine(_rootDirectory, Path.GetFileName(item2))
			});
		}
		SaveFileName = saveFileName;
		SaveFileNumber = saveFileNumber;
		CreatedUtc = DateTime.UtcNow;
		IsValid = true;
		Plugin.Log.LogInfo((object)$"Created pre-landing checkpoint for '{saveFileName}' ({_files.Count} file(s)) at {_rootDirectory}.");
	}

	internal void Restore()
	{
		if (!IsValid || _files.Count == 0)
		{
			throw new InvalidOperationException("No valid pre-landing checkpoint is available.");
		}
		foreach (CheckpointFile file in _files)
		{
			if (!File.Exists(file.BackupPath))
			{
				throw new FileNotFoundException("Checkpoint file is missing.", file.BackupPath);
			}
		}
		foreach (CheckpointFile file2 in _files)
		{
			AtomicCopy(file2.BackupPath, file2.OriginalPath);
		}
		Plugin.Log.LogInfo((object)$"Restored checkpoint for '{SaveFileName}'.");
	}

	private static void AtomicCopy(string source, string destination)
	{
		string text = destination + ".qrr.restore.tmp";
		File.Copy(source, text, overwrite: true);
		try
		{
			if (File.Exists(destination))
			{
				File.Replace(text, destination, null, ignoreMetadataErrors: true);
			}
			else
			{
				File.Move(text, destination);
			}
		}
		catch (PlatformNotSupportedException)
		{
			File.Copy(text, destination, overwrite: true);
			File.Delete(text);
		}
		catch (IOException)
		{
			File.Copy(text, destination, overwrite: true);
			File.Delete(text);
		}
	}

	private static void RecreateOwnedDirectory(string path)
	{
		if (Directory.Exists(path))
		{
			Directory.Delete(path, recursive: true);
		}
		Directory.CreateDirectory(path);
	}
}
internal sealed class ReloadController
{
	private sealed class LobbySettingsSnapshot
	{
		internal string Name;

		internal bool IsPublic;

		internal string Tag;
	}

	private readonly Plugin _plugin;

	private readonly CheckpointManager _checkpoints;

	private readonly ConfigEntry<bool> _requireFullWipe;

	private float _lastRequestTime = -100f;

	internal bool IsReloadInProgress { get; private set; }

	internal bool SuppressSaveGame { get; private set; }

	internal ReloadController(Plugin plugin, CheckpointManager checkpoints, ConfigEntry<bool> requireFullWipe)
	{
		_plugin = plugin;
		_checkpoints = checkpoints;
		_requireFullWipe = requireFullWipe;
	}

	internal bool TryBeginReload(string source, bool activePlayersVerifiedDead = false)
	{
		if (IsReloadInProgress || Time.realtimeSinceStartup - _lastRequestTime < 1f)
		{
			Plugin.Log.LogWarning((object)$"Reload request ignored ({source}): a reload is already running or another request was just received.");
			return false;
		}
		_lastRequestTime = Time.realtimeSinceStartup;
		GameNetworkManager instance = GameNetworkManager.Instance;
		NetworkManager singleton = NetworkManager.Singleton;
		StartOfRound instance2 = StartOfRound.Instance;
		if ((Object)(object)instance == (Object)null || (Object)(object)singleton == (Object)null || (Object)(object)instance2 == (Object)null || !instance.isHostingGame || !singleton.IsHost)
		{
			Plugin.Log.LogWarning((object)$"Reload request ignored ({source}): only the active host can reload a run.");
			return false;
		}
		if (instance2.inShipPhase)
		{
			Plugin.Log.LogWarning((object)$"Reload request ignored ({source}): the ship is not currently landed.");
			return false;
		}
		if (_requireFullWipe.Value && !activePlayersVerifiedDead && !instance2.allPlayersDead && instance2.livingPlayers > 0)
		{
			Plugin.Log.LogWarning((object)$"Reload request ignored ({source}): at least one player is still alive.");
			return false;
		}
		if (!_checkpoints.Matches(instance.currentSaveFileName))
		{
			Plugin.Log.LogError((object)$"Reload request ignored ({source}): there is no checkpoint for the active save and landing.");
			return false;
		}
		string currentSaveFileName = instance.currentSaveFileName;
		int saveFileNum = instance.saveFileNum;
		LobbySettingsSnapshot lobby = CaptureLobbySettings(instance);
		IsReloadInProgress = true;
		SuppressSaveGame = true;
		Plugin.Log.LogWarning((object)$"Wipe reload requested by {source}. Restoring '{currentSaveFileName}' and restarting the host session.");
		((MonoBehaviour)_plugin).StartCoroutine(ReloadRoutine(instance, currentSaveFileName, saveFileNum, lobby));
		return true;
	}

	private IEnumerator ReloadRoutine(GameNetworkManager oldNetwork, string saveFileName, int saveFileNumber, LobbySettingsSnapshot lobby)
	{
		try
		{
			_checkpoints.Restore();
			oldNetwork.Disconnect();
			_checkpoints.Restore();
		}
		catch (Exception ex)
		{
			Plugin.Log.LogError((object)"Could not restore the checkpoint; the session was not re-hosted.");
			Plugin.Log.LogError((object)ex);
			Finish(success: false);
			yield break;
		}
		float menuDeadline = Time.realtimeSinceStartup + 30f;
		MenuManager menu = null;
		while (Time.realtimeSinceStartup < menuDeadline)
		{
			Scene activeScene = SceneManager.GetActiveScene();
			if (((Scene)(ref activeScene)).name == "MainMenu")
			{
				menu = Object.FindObjectOfType<MenuManager>();
			}
			if ((Object)(object)menu != (Object)null && (Object)(object)GameNetworkManager.Instance != (Object)null)
			{
				break;
			}
			yield return null;
		}
		if ((Object)(object)menu == (Object)null || (Object)(object)GameNetworkManager.Instance == (Object)null)
		{
			Plugin.Log.LogError((object)"Timed out waiting for MainMenu after disconnect. The checkpoint is safe on disk; host the same save manually.");
			Finish(success: false);
			yield break;
		}
		yield return null;
		GameNetworkManager newNetwork = GameNetworkManager.Instance;
		try
		{
			_checkpoints.Restore();
			newNetwork.currentSaveFileName = saveFileName;
			newNetwork.saveFileNum = saveFileNumber;
			newNetwork.lobbyHostSettings = new HostSettings(lobby.Name, lobby.IsPublic, lobby.Tag);
			Plugin.Log.LogInfo((object)$"Re-hosting save '{saveFileName}' through GameNetworkManager.StartHost().");
			newNetwork.StartHost();
		}
		catch (Exception ex2)
		{
			Plugin.Log.LogError((object)"Automatic re-host failed. The checkpoint is restored; host the same save manually.");
			Plugin.Log.LogError((object)ex2);
			Finish(success: false);
			yield break;
		}
		float hostDeadline = Time.realtimeSinceStartup + 60f;
		while (Time.realtimeSinceStartup < hostDeadline)
		{
			NetworkManager manager = NetworkManager.Singleton;
			StartOfRound newRound = StartOfRound.Instance;
			if ((Object)(object)manager != (Object)null && manager.IsHost && (Object)(object)newRound != (Object)null && newRound.inShipPhase)
			{
				Plugin.Log.LogInfo((object)$"Reload complete. Save '{saveFileName}' is back in the pre-landing ship state.");
				Finish(success: true);
				yield break;
			}
			yield return null;
		}
		Plugin.Log.LogError((object)"Timed out waiting for the restored ship scene. The checkpoint remains available on disk.");
		Finish(success: false);
	}

	private static LobbySettingsSnapshot CaptureLobbySettings(GameNetworkManager network)
	{
		HostSettings lobbyHostSettings = network.lobbyHostSettings;
		LobbySettingsSnapshot lobbySettingsSnapshot = new LobbySettingsSnapshot();
		lobbySettingsSnapshot.Name = ((lobbyHostSettings != null && !string.IsNullOrEmpty(lobbyHostSettings.lobbyName)) ? lobbyHostSettings.lobbyName : ((!string.IsNullOrEmpty(network.steamLobbyName)) ? network.steamLobbyName : "QuickRunReload"));
		lobbySettingsSnapshot.IsPublic = lobbyHostSettings?.isLobbyPublic ?? false;
		lobbySettingsSnapshot.Tag = ((lobbyHostSettings != null) ? lobbyHostSettings.serverTag : string.Empty);
		return lobbySettingsSnapshot;
	}

	private void Finish(bool success)
	{
		SuppressSaveGame = false;
		IsReloadInProgress = false;
		if (!success)
		{
			Plugin.Log.LogWarning((object)"Quick reload stopped safely. Save suppression has been released.");
		}
	}
}
[HarmonyPatch(typeof(StartOfRound), "StartGame")]
internal static class StartOfRoundStartGamePatch
{
	[HarmonyPrefix]
	[HarmonyPriority(0)]
	private static void CaptureCheckpointPrefix(StartOfRound __instance)
	{
		if ((Object)(object)Plugin.Instance != (Object)null)
		{
			Plugin.Instance.CaptureCheckpoint(__instance);
		}
	}
}
[HarmonyPatch(typeof(GameNetworkManager), "SaveGame")]
internal static class GameNetworkManagerSaveGamePatch
{
	[HarmonyPriority(800)]
	[HarmonyPrefix]
	private static bool SuppressDeathSaveDuringReloadPrefix()
	{
		bool flag = (Object)(object)Plugin.Instance != (Object)null && Plugin.Instance.ShouldSuppressSaveGame();
		if (flag)
		{
			Plugin.Log.LogWarning((object)"Suppressed GameNetworkManager.SaveGame() while restoring the pre-landing checkpoint.");
		}
		return !flag;
	}
}
[HarmonyPatch(typeof(HUDManager), "SubmitChat_performed")]
internal static class HUDManagerSubmitChatPatch
{
	[HarmonyPrefix]
	[HarmonyPriority(800)]
	private static bool DetectRestartCommandPrefix(HUDManager __instance, CallbackContext context)
	{
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		if (!((Object)(object)Plugin.Instance == (Object)null))
		{
			return Plugin.Instance.HandleChatSubmit(__instance, context);
		}
		return true;
	}
}
[HarmonyPatch(typeof(HUDManager), "EnableChat_performed")]
internal static class HUDManagerEnableChatPatch
{
	[HarmonyPrefix]
	[HarmonyPriority(800)]
	private static bool AllowDeadHostCommandInputPrefix(HUDManager __instance, CallbackContext context)
	{
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		if (!((Object)(object)Plugin.Instance == (Object)null))
		{
			return Plugin.Instance.HandleChatEnable(__instance, context);
		}
		return true;
	}
}