Decompiled source of RunBackup v1.1.0

RunBackup.dll

Decompiled 3 hours ago
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<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";
	}
}