using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("ModSaveBackups")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("raddude")] [assembly: AssemblyProduct("ModSaveBackups")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("144ea274-5a8d-4bce-917f-b42e2d203bc2")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.0.0")] [module: UnverifiableCode] namespace ModSaveBackups; public static class ModSave { private static string _basePath = Application.persistentDataPath; public static void Save(PluginInfo pluginInfo, object data) { if (GameState.playing) { Directory.CreateDirectory(GetSaveDirectory(SaveSlots.currentSlot)); FileStream fileStream = File.Create(GetSaveModFile(SaveSlots.currentSlot, pluginInfo)); BinaryFormatter binaryFormatter = new BinaryFormatter(); try { binaryFormatter.Serialize(fileStream, data); } catch (Exception ex) { MSB_Plugin.LogError("Could not serialize data '" + pluginInfo.Metadata.GUID + "'"); MSB_Plugin.LogError(pluginInfo.Metadata.GUID + ": " + ex.Message); } fileStream.Close(); } } public static bool Load(PluginInfo pluginInfo, out object loadedObject) { loadedObject = null; if (!GameState.playing && !GameState.currentlyLoading) { return false; } if (!File.Exists(GetSaveModFile(SaveSlots.currentSlot, pluginInfo))) { MSB_Plugin.LogError("Could not find mod save file for '" + pluginInfo.Metadata.GUID + "'"); return false; } FileStream fileStream = File.OpenRead(GetSaveModFile(SaveSlots.currentSlot, pluginInfo)); if (fileStream.Length <= 0) { fileStream.Close(); MSB_Plugin.LogError("File stream length is 0 '" + pluginInfo.Metadata.GUID + "'"); return false; } try { BinaryFormatter binaryFormatter = new BinaryFormatter(); object obj = binaryFormatter.Deserialize(fileStream); fileStream.Close(); loadedObject = obj; return true; } catch (Exception ex) { fileStream.Close(); MSB_Plugin.LogError("Could not deserialize mod save for '" + pluginInfo.Metadata.GUID + "'"); MSB_Plugin.LogError(pluginInfo.Metadata.GUID + ": " + ex.Message); return false; } } public static bool Load(PluginInfo pluginInfo, out T loadedObject) { loadedObject = default(T); object loadedObject2; bool flag = Load(pluginInfo, out loadedObject2); if (flag) { loadedObject = (T)loadedObject2; } return flag; } internal static void SetBasePath(string path) { _basePath = path; } internal static string GetSaveModFile(int slot, PluginInfo pluginInfo) { return Path.Combine(GetSaveDirectory(slot), pluginInfo.Metadata.GUID + ".save"); } public static string GetSaveDirectory(int slot) { return Path.Combine(_basePath, $"slot{slot}"); } } internal class ModSaveSlots { public static void PushBackups(PluginInfo pluginInfo) { int num = 5; string backupPath = GetBackupPath(pluginInfo, SaveSlots.currentSlot, num); if (File.Exists(backupPath)) { File.Delete(backupPath); } for (int num2 = num - 1; num2 > 0; num2--) { string backupPath2 = GetBackupPath(pluginInfo, SaveSlots.currentSlot, num2); if (File.Exists(backupPath2)) { File.Move(backupPath2, GetBackupPath(pluginInfo, SaveSlots.currentSlot, num2 + 1)); } } string currentSaveModFile = GetCurrentSaveModFile(pluginInfo); if (File.Exists(currentSaveModFile)) { File.Move(currentSaveModFile, GetBackupPath(pluginInfo, SaveSlots.currentSlot, 1)); } } private static string GetBackupPath(PluginInfo pluginInfo, int slot, int backupIndex) { return Path.Combine(ModSave.GetSaveDirectory(slot), pluginInfo.Metadata.GUID + "_backup" + backupIndex + ".save"); } public static string GetCurrentSaveModFile(PluginInfo pluginInfo) { return Path.Combine(ModSave.GetSaveDirectory(SaveSlots.currentSlot), pluginInfo.Metadata.GUID + ".save"); } public static string GetBackupSaveModFile(PluginInfo pluginInfo, int slot, int backupIndex) { for (int num = backupIndex; num > 0; num--) { string backupPath = GetBackupPath(pluginInfo, slot, num); if (File.Exists(backupPath)) { MSB_Plugin.LogDebug("Loading SaveMod backup file path: " + backupPath); return backupPath; } } MSB_Plugin.LogDebug($"Loading current {pluginInfo} SaveMod file"); return GetCurrentSaveModFile(pluginInfo); } } [BepInPlugin("com.raddude.modsavebackups", "ModSaveBackups", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class MSB_Plugin : BaseUnityPlugin { public const string PLUGIN_GUID = "com.raddude.modsavebackups"; public const string PLUGIN_NAME = "ModSaveBackups"; public const string PLUGIN_VERSION = "1.2.0"; public const string PORTABLE_SAVES_GUID = "com.nandbrew.PortableSaves"; private static ManualLogSource _logger; internal static MSB_Plugin Instance { get; private set; } internal static void LogDebug(string message) { _logger.LogDebug((object)message); } internal static void LogInfo(string message) { _logger.LogInfo((object)message); } internal static void LogWarning(string message) { _logger.LogWarning((object)message); } internal static void LogError(string message) { _logger.LogError((object)message); } private void Awake() { if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this) { Object.Destroy((Object)(object)((Component)this).gameObject); return; } Instance = this; _logger = ((BaseUnityPlugin)this).Logger; foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { BepInPlugin metadata = pluginInfo.Value.Metadata; if (metadata.GUID.Equals("com.nandbrew.PortableSaves")) { LogInfo("PortableSaves mod found"); Type type = ((object)pluginInfo.Value.Instance).GetType(); string value = Traverse.Create(type).Property("PortableSavePath", (object[])null).GetValue(); ModSave.SetBasePath(value); break; } } Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "com.raddude.modsavebackups"); } } internal class SaveLoadPatches { [HarmonyPatch(typeof(ModSave))] private class ModSavePatches { [HarmonyPrefix] [HarmonyPatch("Save")] public static void SavePatch(PluginInfo pluginInfo) { ModSaveSlots.PushBackups(pluginInfo); backupIndex = 0; } [HarmonyPrefix] [HarmonyPatch("GetSaveModFile")] public static bool GetSaveModFilePatch(ref string __result, int slot, PluginInfo pluginInfo) { if (backupIndex == 0) { return true; } __result = ModSaveSlots.GetBackupSaveModFile(pluginInfo, slot, backupIndex); return false; } } [HarmonyPatch(typeof(SaveLoadManager))] private class SaveLoadManagerPatches { [HarmonyPostfix] [HarmonyPatch("Awake")] public static void AwakePatch() { backupIndex = 0; } [HarmonyPrefix] [HarmonyPatch("LoadGame")] public static void LoadGamePatch(int backupIndex) { SaveLoadPatches.backupIndex = backupIndex; } } private static int backupIndex; }