Decompiled source of PreventingSaveDeletion v1.0.0

BepInEx/plugins/PreventingSaveDeletion/PreventingSaveDeletion.dll

Decompiled 12 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
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: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("PreventingSaveDeletion contributors")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Preserves R.E.P.O. saves after a team wipe.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("PreventingSaveDeletion")]
[assembly: AssemblyTitle("PreventingSaveDeletion")]
[assembly: AssemblyVersion("1.0.0.0")]
[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 PreventingSaveDeletion
{
	[BepInPlugin("local.preventingsavedeletion", "Preventing Save Deletion", "1.0.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "local.preventingsavedeletion";

		public const string PluginName = "Preventing Save Deletion";

		public const string PluginVersion = "1.0.0";

		internal static readonly ProtectionState State = new ProtectionState();

		internal static ConfigEntry<bool> ProtectionEnabled = null;

		internal static ConfigEntry<bool> AllowManualDelete = null;

		internal static ManualLogSource Log = null;

		internal static string? PendingInitialSave;

		private Harmony? harmony;

		internal static bool ShouldProtect
		{
			get
			{
				if (ProtectionEnabled.Value && (Object)(object)GameManager.instance != (Object)null)
				{
					return SemiFunc.IsMasterClientOrSingleplayer();
				}
				return false;
			}
		}

		private void Awake()
		{
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			ProtectionEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Protect host/singleplayer saves from automatic deletion and defeat overwrites. Restart the game after changing this setting.");
			AllowManualDelete = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "AllowManualDelete", true, "Allow deleting the selected save using the game's Delete Save button.");
			try
			{
				GameFields.Validate();
				harmony = new Harmony("local.preventingsavedeletion");
				harmony.PatchAll(typeof(Plugin).Assembly);
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Preventing Save Deletion 1.0.0 loaded. After defeat, return to the main menu and load the preserved save.");
			}
			catch (Exception arg)
			{
				Harmony? obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
				((BaseUnityPlugin)this).Logger.LogError((object)$"Save protection could not be installed. Protection is NOT active. {arg}");
				((Behaviour)this).enabled = false;
			}
		}

		private void OnDestroy()
		{
			Harmony? obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
		}

		internal static bool IsPlayableScene(RunManager run)
		{
			if ((Object)(object)run != (Object)null && (Object)(object)run.levelCurrent != (Object)null && !SemiFunc.MenuLevel() && (Object)(object)run.levelCurrent != (Object)(object)run.levelTutorial)
			{
				return (Object)(object)run.levelCurrent != (Object)(object)run.levelRecording;
			}
			return false;
		}

		internal static void FreezeCurrentSave(string reason)
		{
			StatsManager instance = StatsManager.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				string text = GameFields.CurrentSave.GetValue(instance) as string;
				if (State.Freeze(text))
				{
					Log.LogInfo((object)("Preserved '" + text + "' (" + reason + "). Further writes are blocked until it is loaded from the main menu."));
				}
			}
		}

		internal static string SavePath(string saveName)
		{
			if (string.IsNullOrWhiteSpace(saveName) || saveName == "." || saveName == ".." || saveName.IndexOfAny(new char[3] { '/', '\\', ':' }) >= 0)
			{
				return string.Empty;
			}
			return Path.Combine(Application.persistentDataPath, "saves", saveName, saveName + ".es3");
		}
	}
	internal static class GameFields
	{
		internal static readonly FieldInfo CurrentSave;

		internal static readonly FieldInfo SaveReady;

		internal static readonly FieldInfo AllPlayersDead;

		static GameFields()
		{
			CurrentSave = Require(typeof(StatsManager), "saveFileCurrent", typeof(string));
			SaveReady = Require(typeof(StatsManager), "saveFileReady", typeof(bool));
			AllPlayersDead = Require(typeof(RunManager), "allPlayersDead", typeof(bool));
		}

		internal static void Validate()
		{
			_ = CurrentSave;
			_ = SaveReady;
			_ = AllPlayersDead;
		}

		private static FieldInfo Require(Type type, string name, Type fieldType)
		{
			FieldInfo fieldInfo = AccessTools.Field(type, name);
			if (fieldInfo == null || fieldInfo.FieldType != fieldType)
			{
				throw new MissingFieldException(type.FullName, name);
			}
			return fieldInfo;
		}
	}
	[HarmonyPatch(typeof(StatsManager), "SaveFileDelete")]
	internal static class DeleteSavePatch
	{
		[HarmonyPrefix]
		private static bool Prefix(string saveFileName)
		{
			if (!Plugin.ShouldProtect)
			{
				return true;
			}
			if (Plugin.AllowManualDelete.Value && ManualDeleteScope.IsAuthorized(saveFileName))
			{
				return true;
			}
			Plugin.Log.LogInfo((object)("Blocked automatic deletion of '" + saveFileName + "'."));
			return false;
		}
	}
	[HarmonyPatch(typeof(MenuPageSaves), "OnDeleteGame")]
	internal static class ManualDeletePatch
	{
		[HarmonyPrefix]
		private static bool Prefix(string ___currentSaveFileName, out ManualDeleteScope? __state)
		{
			__state = null;
			if (!Plugin.ShouldProtect)
			{
				return true;
			}
			if (!Plugin.AllowManualDelete.Value)
			{
				Plugin.Log.LogInfo((object)"Manual save deletion is disabled in the configuration.");
				return false;
			}
			__state = new ManualDeleteScope(___currentSaveFileName);
			return true;
		}

		[HarmonyFinalizer]
		private static Exception? Finalizer(Exception? __exception, ManualDeleteScope? __state)
		{
			__state?.Dispose();
			return __exception;
		}
	}
	[HarmonyPatch(typeof(RunManager), "ChangeLevel")]
	internal static class FailedLevelPatch
	{
		[HarmonyPrefix]
		private static void Prefix(RunManager __instance, bool _levelFailed, bool ___restarting)
		{
			if (_levelFailed && !___restarting && Plugin.ShouldProtect && Plugin.IsPlayableScene(__instance))
			{
				Plugin.FreezeCurrentSave("failed level / team wipe");
			}
		}
	}
	[HarmonyPatch(typeof(StatsManager), "SaveGame")]
	internal static class WriteSavePatch
	{
		[HarmonyPrefix]
		private static bool Prefix(string fileName)
		{
			if (!Plugin.ShouldProtect)
			{
				return true;
			}
			RunManager instance = RunManager.instance;
			if (Plugin.IsPlayableScene(instance) && ((bool)GameFields.AllPlayersDead.GetValue(instance) || SemiFunc.RunIsArena()))
			{
				Plugin.FreezeCurrentSave("death / arena write");
			}
			return !Plugin.State.IsFrozen(fileName);
		}
	}
	[HarmonyPatch(typeof(StatsManager), "LoadGame")]
	internal static class LoadSavePatch
	{
		[HarmonyPrefix]
		private static void Prefix(string fileName, out bool __state)
		{
			__state = (Object)(object)GameManager.instance != (Object)null && SemiFunc.IsMasterClientOrSingleplayer() && File.Exists(Plugin.SavePath(fileName));
		}

		[HarmonyPostfix]
		private static void Postfix(string fileName, bool __state, bool __runOriginal, string ___saveFileCurrent, bool ___saveFileReady)
		{
			if (__runOriginal && __state && ___saveFileReady && string.Equals(fileName, ___saveFileCurrent, StringComparison.Ordinal))
			{
				Plugin.State.Resume(fileName);
				Plugin.PendingInitialSave = null;
			}
		}
	}
	[HarmonyPatch(typeof(StatsManager), "SaveFileCreate")]
	internal static class CreateSavePatch
	{
		[HarmonyPostfix]
		private static void Postfix(bool __runOriginal, string ___saveFileCurrent, bool ___saveFileReady)
		{
			if (__runOriginal && ___saveFileReady)
			{
				Plugin.State.Resume(___saveFileCurrent);
				Plugin.PendingInitialSave = ___saveFileCurrent;
			}
		}
	}
	[HarmonyPatch(typeof(RunManager), "GameDirectorStart")]
	internal static class InitialCheckpointPatch
	{
		[HarmonyPostfix]
		private static void Postfix(RunManager __instance)
		{
			if (!Plugin.ShouldProtect || !Plugin.IsPlayableScene(__instance) || SemiFunc.RunIsArena())
			{
				return;
			}
			StatsManager instance = StatsManager.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			string text = GameFields.CurrentSave.GetValue(instance) as string;
			if (string.IsNullOrWhiteSpace(text) || !string.Equals(text, Plugin.PendingInitialSave, StringComparison.Ordinal) || !(bool)GameFields.SaveReady.GetValue(instance) || Plugin.State.IsFrozen(text))
			{
				return;
			}
			if (File.Exists(Plugin.SavePath(text)))
			{
				Plugin.PendingInitialSave = null;
				return;
			}
			try
			{
				instance.SaveFileSave();
				if (File.Exists(Plugin.SavePath(text)))
				{
					Plugin.PendingInitialSave = null;
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Could not create the initial save checkpoint: {arg}");
			}
		}
	}
	internal sealed class ProtectionState
	{
		private readonly HashSet<string> frozenSaves = new HashSet<string>(StringComparer.Ordinal);

		public bool Freeze(string? saveName)
		{
			if (!string.IsNullOrWhiteSpace(saveName))
			{
				return frozenSaves.Add(saveName);
			}
			return false;
		}

		public bool IsFrozen(string? saveName)
		{
			if (saveName != null)
			{
				return frozenSaves.Contains(saveName);
			}
			return false;
		}

		public void Resume(string? saveName)
		{
			if (saveName != null)
			{
				frozenSaves.Remove(saveName);
			}
		}
	}
	internal sealed class ManualDeleteScope : IDisposable
	{
		[ThreadStatic]
		private static ManualDeleteScope? current;

		private readonly ManualDeleteScope? previous;

		private readonly string? saveName;

		private bool disposed;

		public ManualDeleteScope(string? saveName)
		{
			this.saveName = saveName;
			previous = current;
			current = this;
		}

		public static bool IsAuthorized(string? saveName)
		{
			if (!string.IsNullOrWhiteSpace(saveName) && current != null)
			{
				return string.Equals(current.saveName, saveName, StringComparison.Ordinal);
			}
			return false;
		}

		public void Dispose()
		{
			if (!disposed)
			{
				current = previous;
				disposed = true;
			}
		}
	}
}