using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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 TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [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("SaveBackups")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("SaveBackups")] [assembly: AssemblyTitle("SaveBackups")] [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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SaveBackups { public class BackupInfo { public string Path; public string SaveName; public DateTime TakenUtc; public bool Automatic; public string Summary = ""; public long Bytes; } public static class BackupStore { private const string Stamp = "yyyyMMdd-HHmmss"; public static string SaveFolder => Path.Combine(Application.persistentDataPath, "Saves"); public static string Root => Path.Combine(Application.persistentDataPath, "SaveBackups"); public static string SavePath(string saveName) { return Path.Combine(SaveFolder, saveName + ".txt"); } public static string FolderFor(string saveName) { return Path.Combine(Root, Sanitise(saveName)); } private static string Sanitise(string name) { if (string.IsNullOrEmpty(name)) { return "unnamed"; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { name = name.Replace(oldChar, '_'); } return name; } public static List List(string saveName) { List list = new List(); if (string.IsNullOrEmpty(saveName)) { return list; } try { string path = FolderFor(saveName); if (!Directory.Exists(path)) { return list; } string[] files = Directory.GetFiles(path, "*.txt"); for (int i = 0; i < files.Length; i++) { BackupInfo backupInfo = Describe(files[i], saveName); if (backupInfo != null) { list.Add(backupInfo); } } list.Sort((BackupInfo a, BackupInfo b) => b.TakenUtc.CompareTo(a.TakenUtc)); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not list backups: " + ex)); } return list; } private static BackupInfo Describe(string path, string saveName) { try { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); bool flag = !fileNameWithoutExtension.EndsWith("-manual", StringComparison.OrdinalIgnoreCase); DateTime takenUtc = (DateTime.TryParseExact(flag ? fileNameWithoutExtension : fileNameWithoutExtension.Substring(0, fileNameWithoutExtension.Length - "-manual".Length), "yyyyMMdd-HHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out takenUtc) ? DateTime.SpecifyKind(takenUtc, DateTimeKind.Utc) : File.GetLastWriteTimeUtc(path)); return new BackupInfo { Path = path, SaveName = saveName, TakenUtc = takenUtc, Automatic = flag, Bytes = new FileInfo(path).Length, Summary = Summarise(File.ReadAllText(path)) }; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Skipping unreadable backup " + path + ": " + ex.Message)); return null; } } private static string Summarise(string json) { try { ServerSaveObject val = JsonUtility.FromJson(json); if (val == null) { return "unreadable"; } TimeSpan timeSpan = TimeSpan.FromSeconds(val.Playtime); return $"{val.Money} money, island {val.SpawnedIsland + 1}, {timeSpan.Hours}h {timeSpan.Minutes:D2}m played"; } catch (Exception) { return "unreadable"; } } public static bool Capture(string saveName, bool automatic) { if (string.IsNullOrEmpty(saveName)) { return false; } try { string path = SavePath(saveName); if (!File.Exists(path)) { Plugin.Log.LogWarning((object)("No save file on disk for '" + saveName + "', nothing to back up.")); return false; } string text = File.ReadAllText(path); if (string.IsNullOrEmpty(text)) { Plugin.Log.LogWarning((object)("Save file for '" + saveName + "' is empty, refusing to back it up.")); return false; } string text2 = FolderFor(saveName); Directory.CreateDirectory(text2); if (automatic && Plugin.SkipUnchanged.Value && MatchesNewest(text2, text)) { return false; } string path2 = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + (automatic ? "" : "-manual") + ".txt"; string path3 = Path.Combine(text2, path2); if (File.Exists(path3)) { path3 = Path.Combine(text2, DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N").Substring(0, 4) + ".txt"); } File.WriteAllText(path3, text); Prune(saveName); Plugin.Log.LogInfo((object)("Backed up '" + saveName + "' (" + (automatic ? "automatic" : "manual") + ").")); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("Backup failed: " + ex)); return false; } } private static bool MatchesNewest(string folder, string content) { try { string text = null; DateTime dateTime = DateTime.MinValue; string[] files = Directory.GetFiles(folder, "*.txt"); foreach (string text2 in files) { DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(text2); if (lastWriteTimeUtc > dateTime) { dateTime = lastWriteTimeUtc; text = text2; } } return text != null && File.ReadAllText(text) == content; } catch (Exception) { return false; } } private static void Prune(string saveName) { int value = Plugin.MaxAutomaticBackups.Value; if (value <= 0) { return; } List list = new List(); foreach (BackupInfo item in List(saveName)) { if (item.Automatic) { list.Add(item); } } for (int i = value; i < list.Count; i++) { try { File.Delete(list[i].Path); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not prune old backup: " + ex.Message)); } } } public static bool Restore(BackupInfo info) { if (info == null) { return false; } try { if (!File.Exists(info.Path)) { Plugin.Log.LogWarning((object)"That backup no longer exists on disk."); return false; } string text = File.ReadAllText(info.Path); if (string.IsNullOrEmpty(text)) { Plugin.Log.LogError((object)"That backup is empty, refusing to restore it."); return false; } Capture(info.SaveName, automatic: false); Directory.CreateDirectory(SaveFolder); File.WriteAllText(SavePath(info.SaveName), text); Plugin.Log.LogInfo((object)$"Restored '{info.SaveName}' from {info.TakenUtc.ToLocalTime():g}."); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("Restore failed: " + ex)); return false; } } public static bool Delete(BackupInfo info) { try { if (info != null && File.Exists(info.Path)) { File.Delete(info.Path); return true; } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not delete backup: " + ex)); } return false; } } public static class BackupUI { private static bool _failed; private static bool _built; private static ButtonManager _buttons; private static GameObject _infoHolder; private static Transform _screenParent; private static GameObject _buttonTemplate; private static GameObject _labelTemplate; private static GameObject _openButton; private static GameObject _panel; private static Transform _content; private static TextMeshProUGUI _status; private static string _selectedSave; private static bool _refreshing; public static void OnSaveSelected(string saveName) { if (_refreshing) { return; } _selectedSave = saveName; if (_failed) { return; } try { if (EnsureBuilt()) { _openButton.SetActive(!string.IsNullOrEmpty(saveName)); Close(); } } catch (Exception ex) { _failed = true; Plugin.Log.LogError((object)("Backup UI failed, falling back to no UI: " + ex)); } } public static void Close() { if (!_refreshing) { if ((Object)(object)_panel != (Object)null) { _panel.SetActive(false); } if ((Object)(object)_infoHolder != (Object)null && !string.IsNullOrEmpty(_selectedSave)) { _infoHolder.SetActive(true); } } } private static bool EnsureBuilt() { if (_built && (Object)(object)_openButton != (Object)null && (Object)(object)_panel != (Object)null) { return true; } _buttons = Object.FindAnyObjectByType(); if ((Object)(object)_buttons == (Object)null) { return false; } object? obj = AccessTools.Field(typeof(ButtonManager), "_selectedSaveInfoHolder")?.GetValue(_buttons); _infoHolder = (GameObject)((obj is GameObject) ? obj : null); if ((Object)(object)_infoHolder == (Object)null) { Plugin.Log.LogError((object)"Could not find the save info panel in the load game screen."); _failed = true; return false; } _screenParent = _infoHolder.transform.parent; _buttonTemplate = FindButtonTemplate(((Object)(object)_screenParent != (Object)null) ? _screenParent : _infoHolder.transform); if ((Object)(object)_buttonTemplate == (Object)null) { Plugin.Log.LogError((object)"Could not find a menu button to copy the style from."); _failed = true; return false; } TextMeshProUGUI componentInChildren = _buttonTemplate.GetComponentInChildren(true); _labelTemplate = (((Object)(object)componentInChildren != (Object)null) ? ((Component)componentInChildren).gameObject : null); _openButton = CloneButton(_infoHolder.transform, "Backups", Open); PlaceOpenButton(); _panel = BuildPanel(); _built = true; Plugin.Log.LogInfo((object)"Backups button added to the load game screen."); return true; } private static void PlaceOpenButton() { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) LayoutElement val = _openButton.GetComponent(); if ((Object)(object)val == (Object)null) { val = _openButton.AddComponent(); } val.ignoreLayout = true; RectTransform component = _buttonTemplate.GetComponent(); float num; if ((Object)(object)component != (Object)null) { Rect rect = component.rect; if (((Rect)(ref rect)).height > 1f) { rect = component.rect; num = ((Rect)(ref rect)).height; goto IL_0066; } } num = 44f; goto IL_0066; IL_0066: float num2 = num; RectTransform component2 = _openButton.GetComponent(); if (!((Object)(object)component2 == (Object)null)) { component2.anchorMin = new Vector2(0.5f, 0f); component2.anchorMax = new Vector2(0.5f, 0f); component2.pivot = new Vector2(0.5f, 0f); component2.sizeDelta = new Vector2(Plugin.OpenButtonWidth.Value, num2); component2.anchoredPosition = new Vector2(0f, Plugin.OpenButtonBottomMargin.Value); } } private static GameObject FindButtonTemplate(Transform root) { Button[] componentsInChildren = ((Component)root).GetComponentsInChildren