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 RunBackup v1.1.0
RunBackup.dll
Decompiled 3 hours agousing 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<DirectoryInfo> SelectForPruning(string backupRoot, int keep) { List<DirectoryInfo> list = new List<DirectoryInfo>(); if (!Directory.Exists(backupRoot)) { return list; } List<DirectoryInfo> list2 = new List<DirectoryInfo>(); 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<bool> Enabled; internal static ConfigEntry<int> MaxBackups; internal static ConfigEntry<float> WriteDelay; internal static ConfigEntry<string> BackupDirectory; internal static ConfigEntry<bool> 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<bool>("General", "Enabled", true, "Master switch. Turn off to stop taking backups without uninstalling."); MaxBackups = ((BaseUnityPlugin)this).Config.Bind<int>("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<int>(1, 100), Array.Empty<object>())); WriteDelay = ((BaseUnityPlugin)this).Config.Bind<float>("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<float>(0.5f, 60f), Array.Empty<object>())); BackupDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("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<bool>("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<BackupRunner>(); 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"; } }