Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of PreventingSaveDeletion v1.0.0
BepInEx/plugins/PreventingSaveDeletion/PreventingSaveDeletion.dll
Decompiled 12 hours agousing 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; } } } }