using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; 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: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunBackup { [HarmonyPatch(typeof(RunManager))] internal static class LevelChangePatch { [HarmonyPostfix] [HarmonyPatch("ChangeLevel")] private static void AfterChangeLevel() { if (Plugin.Enabled.Value && SemiFunc.IsMasterClientOrSingleplayer()) { BackupRunner.Instance?.Queue(); } } } internal class BackupRunner : MonoBehaviour { internal static BackupRunner Instance; private Coroutine _pending; private void Awake() { Instance = this; } internal void Queue() { if (_pending != null) { ((MonoBehaviour)this).StopCoroutine(_pending); } _pending = ((MonoBehaviour)this).StartCoroutine(BackupAfterDelay()); } private IEnumerator BackupAfterDelay() { yield return (object)new WaitForSecondsRealtime(Plugin.WriteDelay.Value); if (SaveFiles.TakeBackup(DescribeRun()) != null) { SaveFiles.Prune(); } _pending = null; } private string DescribeRun() { RunManager instance = RunManager.instance; if ((Object)(object)instance == (Object)null) { return ""; } int levelsCompleted = instance.levelsCompleted; string text = (((Object)(object)instance.levelCurrent != (Object)null) ? ((Object)instance.levelCurrent).name : "unknown"); int num = text.LastIndexOf('-'); if (num >= 0 && num < text.Length - 1) { text = text.Substring(num + 1).Trim(); } return $"L{levelsCompleted}-{text}"; } } internal static class FileOps { internal static int CopyDirectory(string source, string destination) { int num = 0; Directory.CreateDirectory(destination); string[] files = Directory.GetFiles(source); foreach (string path in files) { string path2 = Path.Combine(destination, Path.GetFileName(path)); using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using FileStream destination2 = new FileStream(path2, FileMode.Create, FileAccess.Write, FileShare.None); fileStream.CopyTo(destination2); } num++; } files = Directory.GetDirectories(source); foreach (string text in files) { num += CopyDirectory(text, Path.Combine(destination, Path.GetFileName(text))); } return num; } internal static List SelectForPruning(string backupRoot, int keep) { List list = new List(); if (!Directory.Exists(backupRoot)) { return list; } List list2 = new List(); string[] directories = Directory.GetDirectories(backupRoot); foreach (string path in directories) { list2.Add(new DirectoryInfo(path)); } list2.Sort((DirectoryInfo a, DirectoryInfo b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc)); int num = list2.Count - keep; for (int num2 = 0; num2 < num; num2++) { list.Add(list2[num2]); } return list; } internal static string Sanitise(string raw) { if (string.IsNullOrEmpty(raw)) { return raw; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { raw = raw.Replace(oldChar, '-'); } return raw.Replace(' ', '-'); } } [BepInPlugin("benjamin.runbackup", "RunBackup", "1.1.0")] [BepInProcess("REPO.exe")] public class Plugin : BaseUnityPlugin { public const string Guid = "benjamin.runbackup"; public const string Name = "RunBackup"; public const string Version = "1.1.0"; internal static Plugin Instance; internal static ManualLogSource Log; private Harmony _harmony; internal static ConfigEntry Enabled; internal static ConfigEntry MaxBackups; internal static ConfigEntry WriteDelay; internal static ConfigEntry BackupDirectory; internal static ConfigEntry Verbose; private void Awake() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Expected O, but got Unknown //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Master switch. Turn off to stop taking backups without uninstalling."); MaxBackups = ((BaseUnityPlugin)this).Config.Bind("Backups", "MaxBackups", 10, new ConfigDescription("How many backups to keep. Oldest are deleted first. Each one is a copy of the whole saves folder, so a few hundred KB apiece.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 100), Array.Empty())); WriteDelay = ((BaseUnityPlugin)this).Config.Bind("Backups", "WriteDelay", 5f, new ConfigDescription("Seconds to wait after a level change before copying. The game writes its save around this moment and copying mid-write would capture a truncated file. Raise it if backups ever look incomplete.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 60f), Array.Empty())); BackupDirectory = ((BaseUnityPlugin)this).Config.Bind("Backups", "BackupDirectory", "", "Absolute path to store backups in. Leave empty for the default: a 'REPO RunBackups' folder in your Documents."); Verbose = ((BaseUnityPlugin)this).Config.Bind("Debug", "Verbose", false, "Log every backup taken and every old one pruned."); _harmony = new Harmony("benjamin.runbackup"); _harmony.PatchAll(typeof(LevelChangePatch)); ((Component)this).gameObject.AddComponent(); Log.LogInfo((object)("RunBackup v1.1.0 loaded. Backups -> " + SaveFiles.BackupRoot)); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static void Trace(string msg) { if (Verbose != null && Verbose.Value) { Log.LogInfo((object)msg); } } } internal static class SaveFiles { internal static string SaveRoot => Path.Combine(Application.persistentDataPath, "saves"); internal static string BackupRoot { get { string text = ((Plugin.BackupDirectory != null) ? Plugin.BackupDirectory.Value : null); if (!string.IsNullOrEmpty(text)) { return text; } string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); if (!string.IsNullOrEmpty(folderPath)) { return Path.Combine(folderPath, "REPO RunBackups"); } return Path.Combine(Application.persistentDataPath, "RunBackups"); } } internal static bool SaveFolderExists() { try { return Directory.Exists(SaveRoot); } catch { return false; } } internal static string TakeBackup(string label) { if (!SaveFolderExists()) { Plugin.Trace("[RunBackup] No saves folder at " + SaveRoot + " - nothing to back up yet."); return null; } string text = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string path = (string.IsNullOrEmpty(label) ? text : (text + "_" + FileOps.Sanitise(label))); string text2 = Path.Combine(BackupRoot, path); try { Directory.CreateDirectory(text2); int num = FileOps.CopyDirectory(SaveRoot, text2); if (num == 0) { Directory.Delete(text2, recursive: true); Plugin.Trace("[RunBackup] Saves folder was empty, skipped."); return null; } Plugin.Trace($"[RunBackup] Backed up {num} file(s) to {text2}"); return text2; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RunBackup] Backup failed: " + ex.Message)); return null; } } internal static void Prune() { try { foreach (DirectoryInfo item in FileOps.SelectForPruning(BackupRoot, Plugin.MaxBackups.Value)) { item.Delete(recursive: true); Plugin.Trace("[RunBackup] Pruned old backup " + item.Name); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RunBackup] Prune failed: " + ex.Message)); } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "RunBackup"; public const string PLUGIN_NAME = "RunBackup"; public const string PLUGIN_VERSION = "1.1.0"; } }