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 Keepsake v0.5.1
plugins/Keepsake.dll
Decompiled 3 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Managers; using Keepsake.UI; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.Audio; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("isimp")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 isimp")] [assembly: AssemblyDescription("Keep your own value for any mod setting, safe from profile syncs.")] [assembly: AssemblyFileVersion("0.5.1.0")] [assembly: AssemblyInformationalVersion("0.5.1+8850ea0a5cea437e3ed82133e6cd4478d956b296")] [assembly: AssemblyProduct("Keepsake")] [assembly: AssemblyTitle("Keepsake")] [assembly: InternalsVisibleTo("Keepsake.SelfTest")] [assembly: AssemblyVersion("0.5.1.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 Keepsake { internal sealed class BindruneKey { public string Id; public string Yours; public string Profile; public bool Active; } internal static class BindruneLink { public const string Guid = "isimp.Bindrune"; private const string DllName = "Bindrune.dll"; public const string StateVersion = "# bindrune state v3"; private static bool _warnedVersion; public static string KeysFile => Path.Combine(Paths.BepInExRootPath, "bindrune.keys"); public static bool InstalledOnDisk() { try { string pluginPath = Paths.PluginPath; if (!Directory.Exists(pluginPath)) { return false; } if (File.Exists(Path.Combine(pluginPath, "Bindrune.dll"))) { return true; } if (Directory.GetDirectories(pluginPath).Any((string dir) => File.Exists(Path.Combine(dir, "Bindrune.dll")))) { return true; } return Directory.GetFiles(pluginPath, "Bindrune.dll", SearchOption.AllDirectories).Any((string f) => string.Equals(Path.GetFileName(f), "Bindrune.dll", StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look for Bindrune: " + ex.Message)); } return false; } } public static bool IsKeybindType(string typeName) { if (!(typeName == "KeyCode")) { return typeName == "KeyboardShortcut"; } return true; } public static List<BindruneKey> ReadKeys() { string[] lines; try { if (!File.Exists(KeysFile)) { return new List<BindruneKey>(); } lines = File.ReadAllLines(KeysFile); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(KeysFile) + ": " + ex.Message)); } return new List<BindruneKey>(); } List<BindruneKey> list = ParseKeys(lines); if (list != null) { return list; } if (!_warnedVersion) { _warnedVersion = true; ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: " + Path.GetFileName(KeysFile) + " is not in a version this Keepsake knows (# bindrune state v3), so its keys are not offered. Updating Keepsake fixes this.")); } } return new List<BindruneKey>(); } public static List<BindruneKey> ParseKeys(IEnumerable<string> lines) { List<BindruneKey> list = new List<BindruneKey>(); bool flag = false; bool flag2 = false; foreach (string line in lines) { if (!flag) { if (line.Trim().Length != 0) { if (line.Trim() != "# bindrune state v3") { return null; } flag = true; } continue; } string text = line.Trim(); if (text.StartsWith("[", StringComparison.Ordinal) && text.EndsWith("]", StringComparison.Ordinal)) { flag2 = text == "[keys]"; } else if (flag2 && text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal)) { string[] array = line.Split(new char[1] { '\t' }); if (array.Length >= 2) { list.Add(new BindruneKey { Id = array[0], Yours = array[1].Trim(), Profile = ((array.Length > 2) ? array[2].Trim() : "none"), Active = (array.Length < 4 || array[3].Trim() == "1") }); } } } return list; } } internal sealed class CfgText { private static readonly char[] EqualsSign = new char[1] { '=' }; private readonly string[] _lines; private readonly string _newline; private readonly bool _byteOrderMark; private bool _changed; public string Text => string.Join(_newline, _lines); public bool Changed => _changed; private CfgText(string text, bool byteOrderMark = false) { _newline = (text.Contains("\r\n") ? "\r\n" : "\n"); _lines = text.Replace("\r\n", "\n").Split(new char[1] { '\n' }); _byteOrderMark = byteOrderMark; } public static CfgText Load(string path) { byte[] array = File.ReadAllBytes(path); bool flag = array.Length >= 3 && array[0] == 239 && array[1] == 187 && array[2] == 191; return new CfgText(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetString(array, flag ? 3 : 0, array.Length - (flag ? 3 : 0)), flag); } public static CfgText Parse(string text) { return new CfgText(text); } public bool TryGet(string section, string key, out string value) { return Find(section, key, out value) >= 0; } public string TypeOf(string section, string key) { string value; for (int num = Find(section, key, out value) - 1; num >= 0; num--) { string text = _lines[num].Trim(); if (!text.StartsWith("#", StringComparison.Ordinal)) { break; } if (text.StartsWith("# Setting type:", StringComparison.Ordinal)) { return text.Substring("# Setting type:".Length).Trim(); } } return null; } public bool Set(string section, string key, string value) { string value2; int num = Find(section, key, out value2); if (num < 0) { return false; } if (value2 == value) { return true; } _lines[num] = key + " = " + value; _changed = true; return true; } public void Save(string path) { PinFile.ReplaceText(path, Text, _byteOrderMark); } private int Find(string section, string key, out string value) { value = null; int result = -1; string text = string.Empty; for (int i = 0; i < _lines.Length; i++) { string text2 = _lines[i].Trim(); if (text2.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2.Substring(1, text2.Length - 2); } else if (!(text != section)) { string[] array = text2.Split(EqualsSign, 2); if (array.Length == 2 && !(array[0].Trim() != key)) { result = i; value = array[1].Trim(); } } } return result; } } internal sealed class KeptPath { public string Path; public bool IsFolder; public bool Covers(string path) { if (!string.Equals(Path, path, StringComparison.OrdinalIgnoreCase)) { if (IsFolder) { return path.StartsWith(Path + "/", StringComparison.OrdinalIgnoreCase); } return false; } return true; } } internal sealed class SettleResult { public bool Clean; public int PutBack; public int Updated; public int Waiting; } internal static class KeptFiles { public const string Version = "# keepsake files v1"; private const string VersionPrefix = "# keepsake files v"; public const string CopySuffix = ".kept"; private static readonly char[] Slash = new char[1] { '/' }; private static readonly string[] Header = new string[4] { "# keepsake files v1", "# Files and folders in BepInEx/config that Keepsake keeps through profile syncs, one", "# per line, relative to BepInEx/config. A folder ends with a slash. Copies are kept in", "# BepInEx/keepsake-files." }; public static readonly TimeSpan LogGrace = TimeSpan.FromMinutes(1.0); public static string ListPath => Path.Combine(Paths.BepInExRootPath, "keepsake.files"); public static string StoreRoot => Path.Combine(Paths.BepInExRootPath, "keepsake-files"); public static string Live(string path) { return Path.Combine(Paths.ConfigPath, path.Replace('/', Path.DirectorySeparatorChar)); } public static string Copy(string path) { return Path.Combine(StoreRoot, path.Replace('/', Path.DirectorySeparatorChar)) + ".kept"; } public static string Normalise(string path) { if (string.IsNullOrWhiteSpace(path)) { return null; } string[] array = path.Trim().Replace('\\', '/').Split(Slash, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0 || array.Any((string p) => p == "." || p == ".." || p.Contains(":"))) { return null; } if (path.Trim().StartsWith("/", StringComparison.Ordinal) || path.Trim().StartsWith("\\", StringComparison.Ordinal)) { return null; } return string.Join("/", array); } public static List<KeptPath> Read() { string[] lines; try { if (!File.Exists(ListPath)) { return new List<KeptPath>(); } lines = File.ReadAllLines(ListPath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(ListPath) + ": " + ex.Message)); } return null; } List<KeptPath> list = Parse(lines); if (list == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return list; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(ListPath) + " was written by a newer Keepsake, so it is left as it is.")); } return list; } public static List<KeptPath> Parse(IEnumerable<string> lines) { List<KeptPath> list = new List<KeptPath>(); foreach (string line in lines) { if (line.StartsWith("# keepsake files v", StringComparison.Ordinal) && line.Trim() != "# keepsake files v1") { return null; } if (line.Trim().Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text = line.Trim(); string path = Normalise(text); if (path == null) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: skipped " + text + " in keepsake.files, which is not a path inside BepInEx/config.")); } } else if (!list.Any((KeptPath k) => string.Equals(k.Path, path, StringComparison.OrdinalIgnoreCase))) { list.Add(new KeptPath { Path = path, IsFolder = (text.EndsWith("/", StringComparison.Ordinal) || text.EndsWith("\\", StringComparison.Ordinal)) }); } } return list; } public static string[] Format(IEnumerable<KeptPath> kept) { List<string> list = new List<string>(Header); list.AddRange(from k in kept.OrderBy<KeptPath, string>((KeptPath k) => k.Path, StringComparer.OrdinalIgnoreCase) select (!k.IsFolder) ? k.Path : (k.Path + "/")); return list.ToArray(); } public static bool Write(IEnumerable<KeptPath> kept) { List<KeptPath> list = kept.ToList(); try { if (list.Count == 0) { if (File.Exists(ListPath)) { File.Delete(ListPath); } return true; } PinFile.ReplaceText(ListPath, string.Join(Environment.NewLine, Format(list)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(ListPath) + ": " + ex.Message)); } return false; } } public static List<string> LiveFiles(KeptPath kept) { string live = Live(kept.Path); if (!kept.IsFolder) { if (!File.Exists(live)) { return new List<string>(); } return new List<string> { kept.Path }; } if (!Directory.Exists(live)) { return new List<string>(); } return (from f in Directory.GetFiles(live, "*", SearchOption.AllDirectories) where !f.EndsWith(".keepsake.tmp", StringComparison.OrdinalIgnoreCase) select kept.Path + "/" + f.Substring(live.Length).TrimStart('\\', '/').Replace('\\', '/')).ToList(); } public static List<string> CopiedFiles(KeptPath kept) { if (!kept.IsFolder) { if (!File.Exists(Copy(kept.Path))) { return new List<string>(); } return new List<string> { kept.Path }; } string folder = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(folder)) { return new List<string>(); } return (from f in Directory.GetFiles(folder, "*.kept", SearchOption.AllDirectories) where f.EndsWith(".kept", StringComparison.OrdinalIgnoreCase) select f.Substring(0, f.Length - ".kept".Length) into f select kept.Path + "/" + f.Substring(folder.Length).TrimStart('\\', '/').Replace('\\', '/')).ToList(); } public static bool Differ(string a, string b) { FileInfo fileInfo = new FileInfo(a); FileInfo fileInfo2 = new FileInfo(b); if (!fileInfo.Exists || !fileInfo2.Exists) { return fileInfo.Exists != fileInfo2.Exists; } if (fileInfo.Length == fileInfo2.Length) { return fileInfo.LastWriteTimeUtc != fileInfo2.LastWriteTimeUtc; } return true; } public static void CopyOver(string from, string to) { string directoryName = Path.GetDirectoryName(to); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string text = to + ".keepsake.tmp"; File.Copy(from, text, overwrite: true); if (File.Exists(to)) { File.Replace(text, to, null); } else { File.Move(text, to); } File.SetLastWriteTimeUtc(to, File.GetLastWriteTimeUtc(from)); } public static int Save(IEnumerable<KeptPath> kept, Func<string, bool> waiting = null) { int num = 0; foreach (KeptPath item in kept) { List<string> list; try { list = LiveFiles(item); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through " + item.Path + ": " + ex.Message)); } continue; } foreach (string item2 in list) { try { if ((waiting == null || !waiting(item2)) && Differ(Live(item2), Copy(item2)) && Trash.Put(item2, Copy(item2), TrashReason.Copy)) { CopyOver(Live(item2), Copy(item2)); num++; } } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not save a copy of " + item2 + ": " + ex2.Message)); } } } } return num; } public static bool ClosedCleanly(SessionState state, DateTime? logEnd) { if (state.Closed.HasValue && (!state.Started.HasValue || state.Closed >= state.Started)) { if (logEnd.HasValue) { return logEnd <= state.Closed + LogGrace; } return true; } return false; } public static SettleResult Settle(IEnumerable<KeptPath> kept, SessionState state, DateTime? logEnd) { SettleResult settleResult = new SettleResult { Clean = ClosedCleanly(state, logEnd) }; DateTime? dateTime = (settleResult.Clean ? state.Closed : ((DateTime?)null)); DateTime? end = (settleResult.Clean ? Later(state.Closed, logEnd) : logEnd); HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeptPath item in kept) { List<string> list; try { list = CopiedFiles(item); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the copies of " + item.Path + ": " + ex.Message)); } continue; } foreach (string item2 in list) { seen.Add(item2); try { SettleOne(item2, state, dateTime, end, settleResult); } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not settle " + item2 + ": " + ex2.Message)); } } } } state.Waiting.RemoveAll((WaitingFile w) => !seen.Contains(w.Path)); return settleResult; } private static void SettleOne(string path, SessionState state, DateTime? from, DateTime? end, SettleResult result) { string text = Live(path); string text2 = Copy(path); WaitingFile waitingFile = state.WaitingFor(path); if (waitingFile != null) { if (waitingFile.PutBack) { if (!Trash.Put(path, text, TrashReason.Replaced)) { result.Waiting++; return; } CopyOver(text2, text); state.Waiting.Remove(waitingFile); result.PutBack++; ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)("Keepsake: put back your copy of " + path + ", as you chose.")); } } else if (!Differ(text2, text)) { state.Waiting.Remove(waitingFile); } else { result.Waiting++; } return; } if (!File.Exists(text)) { CopyOver(text2, text); result.PutBack++; ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogInfo((object)("Keepsake: put back your copy of " + path + ", which was missing.")); } return; } if (!Differ(text2, text)) { return; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(text); if (end.HasValue) { DateTime value = lastWriteTimeUtc; DateTime? dateTime = end; if (value <= dateTime) { if (from.HasValue) { value = lastWriteTimeUtc; dateTime = from; if (!(value > dateTime)) { goto IL_0191; } } if (Trash.Put(path, text2, TrashReason.Copy)) { CopyOver(text, text2); result.Updated++; ManualLogSource log3 = PinFile.Log; if (log3 != null) { log3.LogInfo((object)("Keepsake: " + path + " was written while the game ran, so your copy now matches it.")); } } return; } } goto IL_0191; IL_0191: if (result.Clean) { if (Trash.Put(path, text, TrashReason.Replaced)) { CopyOver(text2, text); result.PutBack++; ManualLogSource log4 = PinFile.Log; if (log4 != null) { DateTime value = lastWriteTimeUtc; DateTime? dateTime = from; log4.LogInfo((object)("Keepsake: put back your copy of " + path + ", which changed after the game closed" + ((value <= dateTime) ? ", though it carries an older time, as mod managers give files they extract." : "."))); } } } else { state.Waiting.Add(new WaitingFile { Path = path }); result.Waiting++; ManualLogSource log5 = PinFile.Log; if (log5 != null) { log5.LogInfo((object)("Keepsake: left " + path + " as it is. It changed after a game Keepsake did not see close, so it waits for you to choose between it and your copy in the panel.")); } } } private static DateTime? Later(DateTime? a, DateTime? b) { if (a.HasValue) { if (b.HasValue) { if (!(a > b)) { return b; } return a; } return a; } return b; } public static bool SetAside(KeptPath kept) { try { return CopiedFiles(kept).All((string path) => Trash.Put(path, Copy(path), TrashReason.Released)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the copies of " + kept.Path + ": " + ex.Message)); } return false; } } public static void Forget(KeptPath kept) { try { foreach (string item in CopiedFiles(kept)) { File.Delete(Copy(item)); } if (!kept.IsFolder) { return; } string text = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(text)) { return; } foreach (string item2 in (from d in Directory.GetDirectories(text, "*", SearchOption.AllDirectories) orderby d.Length descending select d).Concat(new string[1] { text })) { if (Directory.GetFileSystemEntries(item2).Length == 0) { Directory.Delete(item2); } } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not remove the copies of " + kept.Path + ": " + ex.Message)); } } } } internal sealed class Pin { public string File; public string Section; public string Key; public string Value; public string Profile; public string Id => PinFile.IdOf(File, Section, Key); } internal static class PinFile { public const string Version = "# keepsake pins v1"; private const string VersionPrefix = "# keepsake pins v"; private static readonly string[] Header = new string[4] { "# keepsake pins v1", "# Settings Keepsake keeps at your own value. Tab separated: the cfg file in", "# BepInEx/config, the section, the setting, your value, then the profile's value.", "# The profile's value may be left off; it is filled in on the next launch." }; public static ManualLogSource Log; private static readonly char[] Unstorable = new char[3] { '\t', '\r', '\n' }; public const string TempSuffix = ".keepsake.tmp"; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.pins"); public static string IdOf(string file, string section, string key) { return file.ToLowerInvariant() + "\t" + section + "\t" + key; } public static bool Storable(string value) { if (value != null && value.IndexOfAny(Unstorable) < 0) { return value == value.Trim(); } return false; } public static List<Pin> Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new List<Pin>(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } List<Pin> list = Parse(lines); if (list == null) { ManualLogSource log2 = Log; if (log2 == null) { return list; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return list; } public static List<Pin> Parse(IEnumerable<string> lines) { List<Pin> list = new List<Pin>(); HashSet<string> hashSet = new HashSet<string>(); foreach (string line in lines) { if (line.StartsWith("# keepsake pins v", StringComparison.Ordinal) && line.Trim() != "# keepsake pins v1") { return null; } if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string[] array = line.Split(new char[1] { '\t' }); if (array.Length < 4) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Keepsake: skipped a line in keepsake.pins that has fewer than four parts: " + line)); } continue; } string text = KeptFiles.Normalise(array[0]); if (text == null) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: skipped a line in keepsake.pins whose file is not inside BepInEx/config: " + line)); } continue; } Pin pin = new Pin { File = text, Section = array[1].Trim(), Key = array[2].Trim(), Value = array[3].Trim(), Profile = ((array.Length > 4) ? array[4].Trim() : null) }; if (!hashSet.Add(pin.Id)) { list.RemoveAll((Pin p) => p.Id == pin.Id); } list.Add(pin); } return list; } public static bool Write(IEnumerable<Pin> pins) { try { ReplaceText(FilePath, string.Join(Environment.NewLine, Format(pins)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } public static string[] Format(IEnumerable<Pin> pins) { List<string> list = new List<string>(Header); list.AddRange(from p in pins.OrderBy<Pin, string>((Pin p) => p.File, StringComparer.OrdinalIgnoreCase).ThenBy<Pin, string>((Pin p) => p.Section, StringComparer.Ordinal).ThenBy<Pin, string>((Pin p) => p.Key, StringComparer.Ordinal) select string.Join("\t", (p.Profile != null) ? new string[5] { p.File, p.Section, p.Key, p.Value, p.Profile } : new string[4] { p.File, p.Section, p.Key, p.Value })); return list.ToArray(); } public static void ReplaceText(string path, string text, bool byteOrderMark = false) { string text2 = path + ".keepsake.tmp"; File.WriteAllText(text2, text, new UTF8Encoding(byteOrderMark)); if (File.Exists(path)) { File.Replace(text2, path, null); } else { File.Move(text2, path); } } public static string Stamp() { try { FileInfo fileInfo = new FileInfo(FilePath); return fileInfo.Exists ? (fileInfo.LastWriteTimeUtc.Ticks + ":" + fileInfo.Length) : "missing"; } catch (Exception) { return null; } } public static string Relative(string cfgPath) { try { string text = Path.GetFullPath(Paths.ConfigPath).TrimEnd('\\', '/'); char directorySeparatorChar = Path.DirectorySeparatorChar; string text2 = text + directorySeparatorChar; string fullPath = Path.GetFullPath(cfgPath); if (!fullPath.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { return null; } return NormaliseFile(fullPath.Substring(text2.Length)); } catch (Exception) { return null; } } public static string Absolute(string file) { return Path.Combine(Paths.ConfigPath, file.Replace('/', Path.DirectorySeparatorChar)); } private static string NormaliseFile(string file) { return file.Trim().Replace('\\', '/'); } } internal sealed class ProfileChange { public string File; public string Section; public string Key; public string From; public string To; public string Id => PinFile.IdOf(File, Section, Key); public static ProfileChange Of(Pin pin, string from, string to) { return new ProfileChange { File = pin.File, Section = pin.Section, Key = pin.Key, From = from, To = to }; } } internal sealed class QuietSetting { public string File; public string Section; public string Key; public string Id => PinFile.IdOf(File, Section, Key); public static QuietSetting Of(Pin pin) { return new QuietSetting { File = pin.File, Section = pin.Section, Key = pin.Key }; } } internal sealed class ChangeState { public readonly List<ProfileChange> Waiting = new List<ProfileChange>(); public readonly List<QuietSetting> Quiet = new List<QuietSetting>(); public bool IsQuiet(string id) { return Quiet.Any((QuietSetting q) => q.Id == id); } } internal static class ProfileChanges { public const string Version = "# keepsake changes v1"; private const string VersionPrefix = "# keepsake changes v"; private static readonly string[] Header = new string[5] { "# keepsake changes v1", "# Profile changes to kept settings. [changes] waits for an answer in the panel: the cfg", "# file in BepInEx/config, the section, the setting, the profile's value before, then its", "# value now. [quiet] lists settings whose profile changes are recorded without asking:", "# the cfg file, the section and the setting. Tab separated." }; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.changes"); public static ChangeState Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new ChangeState(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } ChangeState changeState = Parse(lines); if (changeState == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return changeState; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return changeState; } public static ChangeState Parse(IEnumerable<string> lines) { ChangeState changeState = new ChangeState(); string text = null; foreach (string line in lines) { if (line.StartsWith("# keepsake changes v", StringComparison.Ordinal) && line.Trim() != "# keepsake changes v1") { return null; } if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text2 = line.Trim(); if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2; continue; } string[] array = (from p in line.Split(new char[1] { '\t' }) select p.Trim()).ToArray(); string file = array[0].Replace('\\', '/'); if (text == "[changes]" && array.Length >= 5) { ProfileChange change = new ProfileChange { File = file, Section = array[1], Key = array[2], From = array[3], To = array[4] }; changeState.Waiting.RemoveAll((ProfileChange c) => c.Id == change.Id); changeState.Waiting.Add(change); } else if (text == "[quiet]" && array.Length >= 3) { QuietSetting quietSetting = new QuietSetting { File = file, Section = array[1], Key = array[2] }; if (!changeState.IsQuiet(quietSetting.Id)) { changeState.Quiet.Add(quietSetting); } } } return changeState; } public static string[] Format(ChangeState state) { List<string> list = new List<string>(Header); list.Add(""); list.Add("[changes]"); list.AddRange(from c in state.Waiting.OrderBy<ProfileChange, string>((ProfileChange c) => c.File, StringComparer.OrdinalIgnoreCase).ThenBy<ProfileChange, string>((ProfileChange c) => c.Section, StringComparer.Ordinal).ThenBy<ProfileChange, string>((ProfileChange c) => c.Key, StringComparer.Ordinal) select string.Join("\t", c.File, c.Section, c.Key, c.From, c.To)); list.Add(""); list.Add("[quiet]"); list.AddRange(from q in state.Quiet.OrderBy<QuietSetting, string>((QuietSetting q) => q.File, StringComparer.OrdinalIgnoreCase).ThenBy<QuietSetting, string>((QuietSetting q) => q.Section, StringComparer.Ordinal).ThenBy<QuietSetting, string>((QuietSetting q) => q.Key, StringComparer.Ordinal) select string.Join("\t", q.File, q.Section, q.Key)); return list.ToArray(); } public static bool Write(ChangeState state) { try { if (state.Waiting.Count == 0 && state.Quiet.Count == 0) { if (File.Exists(FilePath)) { File.Delete(FilePath); } return true; } PinFile.ReplaceText(FilePath, string.Join(Environment.NewLine, Format(state)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } public static bool Merge(ChangeState state, IEnumerable<ProfileChange> found) { bool result = false; foreach (ProfileChange change in found) { if (state.IsQuiet(change.Id)) { continue; } ProfileChange profileChange = state.Waiting.FirstOrDefault((ProfileChange c) => c.Id == change.Id); if (profileChange == null) { state.Waiting.Add(change); result = true; continue; } if (change.To == profileChange.From) { state.Waiting.Remove(profileChange); } else { profileChange.To = change.To; } result = true; } return result; } public static bool KeepOnly(ChangeState state, ICollection<string> keptIds) { return state.Waiting.RemoveAll((ProfileChange c) => !keptIds.Contains(c.Id)) + state.Quiet.RemoveAll((QuietSetting q) => !keptIds.Contains(q.Id)) > 0; } } internal sealed class WaitingFile { public string Path; public bool PutBack; } internal sealed class SessionState { public DateTime? Started; public DateTime? Closed; public string ClosedBy; public readonly List<WaitingFile> Waiting = new List<WaitingFile>(); public WaitingFile WaitingFor(string path) { return Waiting.FirstOrDefault((WaitingFile w) => string.Equals(w.Path, path, StringComparison.OrdinalIgnoreCase)); } } internal static class SessionFile { public const string Version = "# keepsake session v1"; private const string VersionPrefix = "# keepsake session v"; private static readonly string[] Header = new string[4] { "# keepsake session v1", "# When the game last started and closed with Keepsake, in UTC, and under [files] the kept", "# files in BepInEx/config waiting for an answer in the panel: the path, then ask, or put back", "# when your copy goes back in at the next launch. Tab separated." }; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.session"); public static SessionState Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new SessionState(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } SessionState sessionState = Parse(lines); if (sessionState == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return sessionState; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return sessionState; } public static SessionState Parse(IEnumerable<string> lines) { SessionState sessionState = new SessionState(); string text = null; foreach (string line in lines) { if (line.StartsWith("# keepsake session v", StringComparison.Ordinal) && line.Trim() != "# keepsake session v1") { return null; } if (line.Trim().Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text2 = line.Trim(); if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2; continue; } string[] array = (from p in line.Split(new char[1] { '\t' }) select p.Trim()).ToArray(); if (text == null) { if (array.Length >= 2) { if (array[0] == "started") { sessionState.Started = TimeOf(array[1]); } else if (array[0] == "closed") { sessionState.Closed = TimeOf(array[1]); sessionState.ClosedBy = ((array.Length > 2) ? array[2] : null); } } } else if (text == "[files]") { string text3 = KeptFiles.Normalise(array[0]); if (text3 != null && sessionState.WaitingFor(text3) == null) { sessionState.Waiting.Add(new WaitingFile { Path = text3, PutBack = (array.Length > 1 && array[1] == "put back") }); } } } return sessionState; } public static string[] Format(SessionState state) { List<string> list = new List<string>(Header); if (state.Started.HasValue) { list.Add("started\t" + TextOf(state.Started.Value)); } if (state.Closed.HasValue) { list.Add("closed\t" + TextOf(state.Closed.Value) + ((state.ClosedBy != null) ? ("\t" + state.ClosedBy) : "")); } list.Add(""); list.Add("[files]"); list.AddRange(from w in state.Waiting.OrderBy<WaitingFile, string>((WaitingFile w) => w.Path, StringComparer.OrdinalIgnoreCase) select w.Path + "\t" + (w.PutBack ? "put back" : "ask")); return list.ToArray(); } public static bool Write(SessionState state) { try { PinFile.ReplaceText(FilePath, string.Join(Environment.NewLine, Format(state)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } private static string TextOf(DateTime utc) { return utc.ToString("o", CultureInfo.InvariantCulture); } private static DateTime? TimeOf(string text) { if (!DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return null; } return result; } public static DateTime? LogEnd() { DateTime? dateTime = null; try { string[] files = Directory.GetFiles(Paths.BepInExRootPath, "LogOutput.log*"); foreach (string path in files) { string fileName = Path.GetFileName(path); if (fileName != "LogOutput.log" && !fileName.StartsWith("LogOutput.log.", StringComparison.Ordinal)) { continue; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path); if (dateTime.HasValue) { DateTime value = lastWriteTimeUtc; DateTime? dateTime2 = dateTime; if (!(value > dateTime2)) { continue; } } dateTime = lastWriteTimeUtc; } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read when the last game ended: " + ex.Message)); } } return dateTime; } } internal static class SpareCopy { private const string RestoredKey = "Keepsake.SpareCopyRestored"; private static readonly string[] Lists = new string[3] { "keepsake.pins", "keepsake.changes", "keepsake.files" }; private static readonly string[] KeptLists = new string[2] { "keepsake.pins", "keepsake.files" }; private static readonly string[] Folders = new string[2] { "keepsake-files", "keepsake-trash" }; private static readonly string[] Files = new string[5] { "keepsake.spare", "keepsake.session", "keepsake.changes", "keepsake.files", "keepsake.pins" }; public static bool RestoredThisLaunch { get { object data = AppDomain.CurrentDomain.GetData("Keepsake.SpareCopyRestored"); bool flag = default(bool); int num; if (data is bool) { flag = (bool)data; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } set { AppDomain.CurrentDomain.SetData("Keepsake.SpareCopyRestored", value); } } public static string AnswerPath => Path.Combine(Paths.BepInExRootPath, "keepsake.spare"); public static bool? Wanted { get { try { if (!File.Exists(AnswerPath)) { return null; } return SpareFolder.WantedIn(File.ReadAllLines(AnswerPath)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(AnswerPath) + ": " + ex.Message)); } return null; } } } public static bool Offered => Folder != null; public static bool Asks { get { if (Offered && KeepsSomething()) { return !Wanted.HasValue; } return false; } } public static string Folder => SpareFolder.For(Paths.BepInExRootPath); private static string Marker => Path.Combine(Paths.BepInExRootPath, "keepsake.restoring"); private static bool KeepsSomething() { return KeptLists.Any(delegate(string name) { try { string path = Path.Combine(Paths.BepInExRootPath, name); return File.Exists(path) && File.ReadLines(path).Any((string l) => l.Trim().Length > 0 && !l.StartsWith("#", StringComparison.Ordinal)); } catch (Exception) { return false; } }); } public static string Answer(bool keep) { try { PinFile.ReplaceText(AnswerPath, string.Join(Environment.NewLine, "# keepsake spare v1", "# Whether Keepsake keeps a spare copy of what it keeps in this profile outside the", "# profile, for Update existing profile in Thunderstore Mod Manager and r2modman.", "copy\t" + (keep ? "yes" : "no")) + Environment.NewLine); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(AnswerPath) + ": " + ex.Message)); } return "your answer could not be saved, see the log"; } string folder = Folder; if (folder == null) { return null; } try { if (keep) { Update(out var failed); return (failed == 0) ? null : "the spare copy could not be made whole yet, see the log; the next save tries again"; } SpareFolder.Remove(folder); return null; } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not " + (keep ? "make" : "remove") + " the spare copy in " + folder + ": " + ex2.Message)); } return keep ? "the spare copy could not be made, see the log" : "the spare copy could not be removed, see the log"; } } private static bool HasLists(string root) { return Lists.Any((string n) => File.Exists(Path.Combine(root, n))); } public static int Restore() { string folder = Folder; if (folder == null || !Directory.Exists(folder) || !HasLists(folder)) { return 0; } string bepInExRootPath = Paths.BepInExRootPath; bool flag = File.Exists(Marker); if (HasLists(bepInExRootPath) && !flag) { return 0; } ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)(flag ? ("Keepsake: bringing back the spare copy in " + folder + " was cut short at the last launch, so it is done again.") : ("Keepsake: this profile holds nothing Keepsake kept, and a spare copy of it is in " + folder + ". The profile folder was replaced, as Update existing profile does, so the spare copy comes back."))); } File.WriteAllText(Marker, folder); int failed; int result = Mirror(folder, bepInExRootPath, "bringing back", out failed); if (failed == 0) { File.Delete(Marker); } return result; } public static int Update() { int failed; return Update(out failed); } private static int Update(out int failed, bool listsOnly = false) { failed = 0; string folder = Folder; if (folder == null || Wanted != true) { return 0; } string bepInExRootPath = Paths.BepInExRootPath; if (!HasLists(bepInExRootPath) || File.Exists(Marker)) { return 0; } return Mirror(bepInExRootPath, folder, "saving", out failed, listsOnly); } public static void Follow(bool listsOnly = false) { try { Update(out var _, listsOnly); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: saving the spare copy failed: " + ex.Message)); } } } private static int Mirror(string from, string to, string doing, out int failed, bool listsOnly = false) { int num = 0; failed = 0; List<string> list = new List<string>(); string[] array = (listsOnly ? Array.Empty<string>() : Folders); string[] array2 = array; foreach (string folder in array2) { list.AddRange(FilesIn(from, folder)); list.AddRange(FilesIn(to, folder)); } list.AddRange(Files); foreach (string item in list.Distinct<string>(StringComparer.OrdinalIgnoreCase)) { string text = Path.Combine(from, item); string text2 = Path.Combine(to, item); try { if (File.Exists(text)) { if (KeptFiles.Differ(text, text2)) { KeptFiles.CopyOver(text, text2); num++; } } else if (File.Exists(text2)) { File.Delete(text2); num++; } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: " + doing + " the spare copy of " + item + " failed: " + ex.Message)); } failed++; } } array2 = array; foreach (string path in array2) { RemoveEmptyFolders(Path.Combine(to, path)); } return num; } private static IEnumerable<string> FilesIn(string root, string folder) { string path = Path.Combine(root, folder); if (!Directory.Exists(path)) { return Enumerable.Empty<string>(); } return (from f in Directory.GetFiles(path, "*", SearchOption.AllDirectories) where !f.EndsWith(".keepsake.tmp", StringComparison.OrdinalIgnoreCase) select f.Substring(root.Length).TrimStart('\\', '/')).ToList(); } private static void RemoveEmptyFolders(string folder) { try { if (!Directory.Exists(folder)) { return; } foreach (string item in from d in Directory.GetDirectories(folder, "*", SearchOption.AllDirectories) orderby d.Length descending select d) { if (Directory.GetFileSystemEntries(item).Length == 0) { Directory.Delete(item); } } } catch (Exception) { } } } internal static class SpareFolder { public const string AnswerVersion = "# keepsake spare v1"; public static string For(string bepInExRoot) { try { DirectoryInfo parent = Directory.GetParent(bepInExRoot); if (parent == null || !File.Exists(Path.Combine(parent.FullName, "mods.yml"))) { return null; } DirectoryInfo parent2 = parent.Parent; if (parent2 == null || !string.Equals(parent2.Name, "profiles", StringComparison.OrdinalIgnoreCase)) { return null; } DirectoryInfo parent3 = parent2.Parent; return (parent3 == null) ? null : Path.Combine(Path.Combine(parent3.FullName, "Keepsake"), parent.Name); } catch (Exception) { return null; } } public static bool? WantedIn(IEnumerable<string> lines) { List<string> list = lines.ToList(); if (list.Count == 0 || list[0].Trim() != "# keepsake spare v1") { return null; } foreach (string[] item in from l in list.Skip(1) where !l.StartsWith("#", StringComparison.Ordinal) select l.Split(new char[1] { '\t' })) { if (item.Length >= 2 && item[0].Trim() == "copy") { return (item[1].Trim() == "yes") ? new bool?(true) : ((item[1].Trim() == "no") ? new bool?(false) : ((bool?)null)); } } return null; } public static void Remove(string spare) { if (Directory.Exists(spare)) { Directory.Delete(spare, recursive: true); } string directoryName = Path.GetDirectoryName(spare); if (directoryName != null && Directory.Exists(directoryName) && Directory.GetFileSystemEntries(directoryName).Length == 0) { Directory.Delete(directoryName); } } } internal enum TrashReason { Copy, Replaced, Released } internal sealed class TrashEntry { public string Path; public string File; public DateTime At; public int Count; public TrashReason Reason; } internal static class Trash { public const int Versions = 5; internal static long MaxFileBytes = 8388608L; internal static long MaxTotalBytes = 104857600L; private const string StampFormat = "yyyyMMdd-HHmmss-fff"; private static readonly Regex VersionName = new Regex("^(?<name>.+)\\.(?<at>\\d{8}-\\d{6}-\\d{3})(?:-(?<count>\\d+))?\\.(?<reason>copy|replaced|released|launch)\\.kept$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static string Root => Path.Combine(Paths.BepInExRootPath, "keepsake-trash"); private static string FilesRoot => Path.Combine(Root, "files"); private static string ListsRoot => Path.Combine(Root, "lists"); private static string ReasonText(TrashReason reason) { return reason.ToString().ToLowerInvariant(); } public static bool Put(string path, string file, TrashReason reason) { try { if (!File.Exists(file)) { return true; } string folder = Path.Combine(FilesRoot, Path.GetDirectoryName(path.Replace('/', Path.DirectorySeparatorChar)) ?? ""); string fileName = Path.GetFileName(path); TrashEntry trashEntry = In(folder, fileName).FirstOrDefault(); if (trashEntry != null && SameBytes(trashEntry.File, file)) { return true; } long length = new FileInfo(file).Length; if (length > MaxFileBytes) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)($"Keepsake: {path} is {length / 1048576} MB, larger than the {MaxFileBytes / 1048576} MB " + "an earlier version may take, so the version it replaces is not set aside.")); } return true; } KeepUnderTotal(SetAside(file, folder, fileName, ReasonText(reason))); return true; } catch (Exception ex) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not set aside the version of " + path + " it was about to replace, so it is left as it is: " + ex.Message)); } return false; } } public static void KeepLists(IEnumerable<string> lists) { foreach (string list in lists) { try { if (File.Exists(list)) { string fileName = Path.GetFileName(list); TrashEntry trashEntry = In(ListsRoot, fileName).FirstOrDefault(); if (trashEntry == null || !SameBytes(trashEntry.File, list)) { SetAside(list, ListsRoot, fileName, "launch"); } } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not keep a copy of " + Path.GetFileName(list) + ": " + ex.Message)); } } } } public static List<TrashEntry> Of(string path) { try { List<TrashEntry> list = In(Path.Combine(FilesRoot, Path.GetDirectoryName(path.Replace('/', Path.DirectorySeparatorChar)) ?? ""), Path.GetFileName(path)); foreach (TrashEntry item in list) { item.Path = path; } return list; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the earlier versions of " + path + ": " + ex.Message)); } return new List<TrashEntry>(); } } private static void KeepUnderTotal(string added) { if (!Directory.Exists(FilesRoot)) { return; } string name; List<(TrashEntry Entry, long Length)> source = (from file in Directory.GetFiles(FilesRoot, "*.kept", SearchOption.AllDirectories) select (Entry: EntryOf(file, out name), Length: new FileInfo(file).Length) into v where v.Entry != null select v).ToList(); long num = source.Sum(((TrashEntry Entry, long Length) v) => v.Length); foreach (var (trashEntry, num2) in from v in source orderby v.Entry.At, v.Entry.Count select v) { if (num <= MaxTotalBytes) { break; } if (!string.Equals(trashEntry.File, added, StringComparison.OrdinalIgnoreCase)) { File.Delete(trashEntry.File); num -= num2; } } } private static string SetAside(string file, string folder, string name, string reason) { Directory.CreateDirectory(folder); string text = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture); string text2 = Path.Combine(folder, name + "." + text + "." + reason + ".kept"); int num = 1; while (File.Exists(text2)) { text2 = Path.Combine(folder, $"{name}.{text}-{num}.{reason}.kept"); num++; } string text3 = text2 + ".keepsake.tmp"; File.Copy(file, text3, overwrite: true); File.SetLastWriteTimeUtc(text3, File.GetLastWriteTimeUtc(file)); File.Move(text3, text2); foreach (TrashEntry item in In(folder, name).Skip(5)) { File.Delete(item.File); } string[] files = Directory.GetFiles(folder, name + ".*.keepsake.tmp"); foreach (string text4 in files) { if (!string.Equals(text4, text3, StringComparison.OrdinalIgnoreCase)) { File.Delete(text4); } } return text2; } private static List<TrashEntry> In(string folder, string name) { if (!Directory.Exists(folder)) { return new List<TrashEntry>(); } List<TrashEntry> list = new List<TrashEntry>(); string[] files = Directory.GetFiles(folder, name + ".*.kept"); for (int i = 0; i < files.Length; i++) { string name2; TrashEntry trashEntry = EntryOf(files[i], out name2); if (trashEntry != null && string.Equals(name2, name, StringComparison.OrdinalIgnoreCase)) { list.Add(trashEntry); } } return (from e in list orderby e.At descending, e.Count descending select e).ToList(); } private static TrashEntry EntryOf(string file, out string name) { name = null; Match match = VersionName.Match(Path.GetFileName(file)); if (!match.Success) { return null; } if (!DateTime.TryParseExact(match.Groups["at"].Value, "yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return null; } name = match.Groups["name"].Value; return new TrashEntry { File = file, At = result, Count = (match.Groups["count"].Success ? int.Parse(match.Groups["count"].Value, CultureInfo.InvariantCulture) : 0), Reason = ReasonOf(match.Groups["reason"].Value) }; } private static TrashReason ReasonOf(string text) { if (!string.Equals(text, "replaced", StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(text, "released", StringComparison.OrdinalIgnoreCase)) { return TrashReason.Copy; } return TrashReason.Released; } return TrashReason.Replaced; } private static bool SameBytes(string a, string b) { FileInfo fileInfo = new FileInfo(a); FileInfo fileInfo2 = new FileInfo(b); if (fileInfo.Length == fileInfo2.Length) { return File.ReadAllBytes(a).SequenceEqual(File.ReadAllBytes(b)); } return false; } } internal sealed class KeybindSlot<T> { public string BindruneId; public bool IsKeyCode; public T Setting; } internal sealed class KeybindMatch<T> { public T Setting; public string Yours; public string Profile; } internal static class BindruneKeys { public static string IdOf(string modGuid, string section, string key) { return "cfg:" + modGuid + ":" + section + ":" + key; } public static List<KeybindMatch<T>> Match<T>(IEnumerable<BindruneKey> keys, IEnumerable<KeybindSlot<T>> slots) { Dictionary<string, KeybindSlot<T>> dictionary = new Dictionary<string, KeybindSlot<T>>(); foreach (KeybindSlot<T> slot in slots) { dictionary[slot.BindruneId] = slot; } List<KeybindMatch<T>> list = new List<KeybindMatch<T>>(); foreach (BindruneKey key in keys) { if (key.Active && dictionary.TryGetValue(key.Id, out var value)) { string text = AsSetting(key.Yours, value.IsKeyCode); if (text != null) { list.Add(new KeybindMatch<T> { Setting = value.Setting, Yours = text, Profile = AsSetting(key.Profile, value.IsKeyCode) }); } } } return list; } public static string AsSetting(string stored, bool isKeyCode) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) try { int num; KeyboardShortcut val; if (!string.IsNullOrEmpty(stored)) { num = ((stored == "none") ? 1 : 0); if (num == 0) { val = KeyboardShortcut.Deserialize(stored); goto IL_0026; } } else { num = 1; } val = KeyboardShortcut.Empty; goto IL_0026; IL_0026: KeyboardShortcut val2 = val; if (num == 0 && (int)((KeyboardShortcut)(ref val2)).MainKey == 0 && !string.Equals(stored, "None", StringComparison.OrdinalIgnoreCase)) { return null; } return isKeyCode ? TomlTypeConverter.ConvertToString((object)((KeyboardShortcut)(ref val2)).MainKey, typeof(KeyCode)) : TomlTypeConverter.ConvertToString((object)val2, typeof(KeyboardShortcut)); } catch (Exception) { return null; } } } internal sealed class ConfigItem { public string Path; public bool IsFolder; public long Size; public int Files; public int Images; public DateTime Changed; public string Id => FileKeeper.IdOf(Path, IsFolder); public string Name => Path.Substring(Path.LastIndexOf('/') + 1) + (IsFolder ? "/" : ""); public string Parent { get { if (!Path.Contains("/")) { return ""; } return Path.Substring(0, Path.LastIndexOf('/')); } } } internal static class FileKeeper { private static List<KeptPath> _kept; private static bool _unreadable; private static SessionState _session; private static bool _sessionUnreadable; private static readonly HashSet<string> ImageEndings = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".tga", ".dds", ".bmp", ".gif", ".webp", ".psd", ".exr", ".hdr" }; public static IReadOnlyList<KeptPath> Kept { get { Load(); return _kept; } } private static SessionState Session { get { if (_session != null || _sessionUnreadable) { return _session; } _session = SessionFile.Read(); _sessionUnreadable = _session == null; return _session; } } public static int WaitingCount => Session?.Waiting.Count((WaitingFile w) => !w.PutBack && KeptBy(w.Path) != null) ?? 0; public static string IdOf(string path, bool isFolder) { return "file:" + path + (isFolder ? "/" : ""); } private static void Load() { if (_kept == null) { List<KeptPath> list = KeptFiles.Read(); _unreadable = list == null; if (list == null) { list = new List<KeptPath>(); } _kept = list; } } public static KeptPath KeptBy(string path) { return Kept.FirstOrDefault((KeptPath k) => k.Covers(path)); } public static string Keep(string path, bool isFolder) { path = KeptFiles.Normalise(path); if (path == null) { return "that is not a path inside BepInEx/config"; } Load(); if (_unreadable) { return "keepsake.files could not be read, so nothing is added to it until it can"; } if (KeptBy(path) != null) { return null; } KeptPath entry = new KeptPath { Path = path, IsFolder = isFolder }; _kept.RemoveAll((KeptPath k) => entry.Covers(k.Path)); _kept.Add(entry); if (!KeptFiles.Write(_kept)) { return "keepsake.files could not be saved, see the log"; } KeptFiles.Save(new KeptPath[1] { entry }); SpareCopy.Follow(); return null; } public static string Release(string path) { Load(); if (_unreadable) { return "keepsake.files could not be read, so nothing is taken out of it until it can"; } KeptPath entry = Kept.FirstOrDefault((KeptPath k) => string.Equals(k.Path, path, StringComparison.OrdinalIgnoreCase)); if (entry == null) { return null; } if (!KeptFiles.SetAside(entry)) { return "the copies could not be set aside in the trash, so it stays kept; see the log"; } _kept.Remove(entry); if (!KeptFiles.Write(_kept)) { _kept.Add(entry); return "keepsake.files could not be saved, see the log"; } KeptFiles.Forget(entry); SessionState session = Session; if (session != null && session.Waiting.RemoveAll((WaitingFile w) => entry.Covers(w.Path)) > 0) { SessionFile.Write(session); } SpareCopy.Follow(); return null; } public static WaitingFile WaitingFor(string path) { return Session?.WaitingFor(path); } public static string PutBack(string path) { WaitingFile waitingFile = WaitingFor(path); if (waitingFile == null) { return "that file is not waiting for an answer"; } waitingFile.PutBack = true; return Saved(SessionFile.Write(Session)); } private static string Saved(bool written) { if (!written) { return "keepsake.session could not be saved, see the log"; } SpareCopy.Follow(); return null; } public static string KeepCurrent(string path) { WaitingFile waitingFile = WaitingFor(path); if (waitingFile == null) { return "that file is not waiting for an answer"; } if (!File.Exists(KeptFiles.Live(path))) { return "the file is not there to keep"; } if (!Trash.Put(path, KeptFiles.Copy(path), TrashReason.Copy)) { return "your copy could not be set aside, see the log"; } KeptFiles.CopyOver(KeptFiles.Live(path), KeptFiles.Copy(path)); Session.Waiting.Remove(waitingFile); return Saved(SessionFile.Write(Session)); } public static string PutBackVersion(string path, TrashEntry version) { if (!File.Exists(version.File)) { return "that version is no longer in the trash"; } Load(); if (_unreadable) { return "keepsake.files could not be read, so nothing is added to it until it can"; } if (Session == null) { return "keepsake.session could not be read, see the log"; } string text = KeptFiles.Copy(path); bool flag = KeptBy(path) != null; try { byte[] bytes = File.ReadAllBytes(version.File); DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(version.File); if (!Trash.Put(path, text, TrashReason.Copy)) { return "your copy could not be set aside, see the log"; } Directory.CreateDirectory(Path.GetDirectoryName(text)); string text2 = text + ".keepsake.tmp"; File.WriteAllBytes(text2, bytes); if (File.Exists(text)) { File.Replace(text2, text, null); } else { File.Move(text2, text); } File.SetLastWriteTimeUtc(text, lastWriteTimeUtc); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not put back the earlier version of " + path + ": " + ex.Message)); } return "the earlier version could not be put back, see the log"; } if (!flag) { _kept.Add(new KeptPath { Path = path }); if (!KeptFiles.Write(_kept)) { _kept.RemoveAt(_kept.Count - 1); KeptFiles.Forget(new KeptPath { Path = path }); return "keepsake.files could not be saved, see the log"; } } WaitingFile waitingFile = WaitingFor(path); if (waitingFile == null) { List<WaitingFile> waiting = Session.Waiting; WaitingFile obj = new WaitingFile { Path = path }; waitingFile = obj; waiting.Add(obj); } waitingFile.PutBack = true; return Saved(SessionFile.Write(Session)); } public static void Close(string by, DateTime? at = null) { Load(); if (_unreadable) { return; } SessionState session = Session; if (Kept.Count == 0) { if (session != null && File.Exists(SessionFile.FilePath)) { File.Delete(SessionFile.FilePath); } return; } if (session == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"Keepsake: keepsake.session could not be read, so no copies of kept files were saved this time."); } return; } int num = KeptFiles.Save(Kept, (string p) => session.WaitingFor(p) != null); if (num > 0) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogDebug((object)$"Keepsake: saved a copy of {num} kept file(s)."); } } session.Closed = at ?? DateTime.UtcNow; session.ClosedBy = by; SessionFile.Write(session); } public static List<ConfigItem> Browse(ICollection<string> settingsFiles) { string configPath = Paths.ConfigPath; List<ConfigItem> list = new List<ConfigItem>(); if (!Directory.Exists(configPath)) { return list; } Dictionary<string, ConfigItem> dictionary = new Dictionary<string, ConfigItem>(StringComparer.OrdinalIgnoreCase); string[] files = Directory.GetFiles(configPath, "*", SearchOption.AllDirectories); foreach (string text in files) { string text2 = text.Substring(configPath.Length).TrimStart('\\', '/').Replace('\\', '/'); if (text2.EndsWith(".keepsake.tmp", StringComparison.OrdinalIgnoreCase) || text2.EndsWith(".log", StringComparison.OrdinalIgnoreCase) || (!text2.Contains("/") && (settingsFiles.Contains(text2) || string.Equals(text2, "BepInEx.cfg", StringComparison.OrdinalIgnoreCase)))) { continue; } FileInfo fileInfo; try { fileInfo = new FileInfo(text); } catch (Exception) { continue; } bool flag = IsImage(text2); ConfigItem configItem = new ConfigItem { Path = text2, Size = fileInfo.Length, Files = 1, Images = (flag ? 1 : 0), Changed = fileInfo.LastWriteTime }; list.Add(configItem); string text3 = configItem.Parent; while (text3.Length > 0) { if (!dictionary.TryGetValue(text3, out var value)) { value = (dictionary[text3] = new ConfigItem { Path = text3, IsFolder = true }); } value.Size += configItem.Size; value.Files++; if (flag) { value.Images++; } if (configItem.Changed > value.Changed) { value.Changed = configItem.Changed; } text3 = (text3.Contains("/") ? text3.Substring(0, text3.LastIndexOf('/')) : ""); } } list.AddRange(dictionary.Values); return list.OrderBy((ConfigItem configItem3) => (!configItem3.IsFolder) ? configItem3.Parent : configItem3.Path, NaturalOrder.IgnoreCase).ThenBy((ConfigItem configItem3) => (!configItem3.IsFolder) ? 1 : 0).ThenBy((ConfigItem configItem3) => configItem3.Path, NaturalOrder.IgnoreCase) .ToList(); } public static bool IsImage(string path) { return ImageEndings.Contains(Path.GetExtension(path)); } internal static void Reset() { _kept = null; _unreadable = false; _session = null; _sessionUnreadable = false; } } internal static class GameClose { public static void Run(string by, DateTime? at = null) { Keeper.Flush(); FileKeeper.Close(by, at); SpareCopy.Update(); } } internal static class Keeper { public sealed class BindruneKeep { public Setting Setting; public string Yours; public string Profile; } private static List<Pin> _pins = new List<Pin>(); private static Dictionary<string, Pin> _byId = new Dictionary<string, Pin>(); private static string _stamp; private static bool _loaded; private static bool _unreadable; private static readonly HashSet<ConfigFile> Followed = new HashSet<ConfigFile>(); private static bool _writing; public static Action<string> Changed; public const string CannotSave = "keepsake.pins could not be read or saved, so nothing changed; see the log"; private static HashSet<string> _keptAtLaunch; private static readonly Dictionary<string, string> Released = new Dictionary<string, string>(); public const string BindruneKeepsKeys = "Bindrune keeps your keybinds. Set this key as yours in Bindrune to keep it."; private static ChangeState _changes; private static bool _changesUnreadable; public static int MainThread; private static readonly ConcurrentQueue<ConfigEntryBase> OffThread = new ConcurrentQueue<ConfigEntryBase>(); private static readonly Dictionary<string, string> Pending = new Dictionary<string, string>(); public const float FollowDelay = 1f; private static bool _followedSinceTick; private static float _flushAt; public static IReadOnlyList<Pin> Pins { get { if (!_loaded) { Sync(); } return _pins; } } private static ChangeState Changes { get { if (_changes != null) { return _changes; } ChangeState changeState = ProfileChanges.Read(); _changesUnreadable = changeState == null; if (changeState == null) { changeState = new ChangeState(); } _changes = changeState; return _changes; } } public static Pin Find(string id) { if (!_loaded) { Sync(); } if (id == null || !_byId.TryGetValue(id, out var value)) { return null; } return value; } public static void Sync() { string text = PinFile.Stamp(); if (_loaded && (text == null || text == _stamp)) { return; } List<Pin> list = PinFile.Read(); _loaded = true; _unreadable = list == null; if (_unreadable) { return; } _pins = list; _stamp = text; Index(); foreach (string item in Pending.Keys.ToList()) { if (_byId.TryGetValue(item, out var value)) { value.Value = Pending[item]; } else { Pending.Remove(item); } } if (_keptAtLaunch == null) { _keptAtLaunch = new HashSet<string>(_byId.Keys); } } public static string Pin(Setting setting) { Sync(); if (Find(setting.Id) != null) { return null; } string text = Add(setting); if (text != null) { return text; } if (Save()) { return null; } Remove(setting.Id); return "keepsake.pins could not be read or saved, so nothing changed; see the log"; } public static int PinAll(IEnumerable<Setting> settings) { Sync(); List<string> list = new List<string>(); foreach (Setting setting in settings) { if (Find(setting.Id) == null && Add(setting) == null) { list.Add(setting.Id); } } if (list.Count == 0 || Save()) { return list.Count; } foreach (string item in list) { Remove(item); } return 0; } private static void Remove(string id) { if (_byId.TryGetValue(id, out var value)) { _pins.Remove(value); Index(); } } public static bool CanPin(Setting setting) { if (setting == null || Find(setting.Id) != null || setting.LeftToBindrune) { return false; } return PinFile.Storable(setting.Current); } private static string Add(Setting setting) { if (Find(setting.Id) != null) { return null; } if (setting.LeftToBindrune) { return "Bindrune keeps your keybinds. Set this key as yours in Bindrune to keep it."; } string current = setting.Current; if (current == null) { return "this setting's value could not be read"; } if (!PinFile.Storable(current)) { return "this value has line breaks or tabs in it, which Keepsake cannot store"; } string text = ProfileOf(setting); Pin pin = new Pin { File = setting.File, Section = setting.Section, Key = setting.Key, Value = current, Profile = ((text != null && PinFile.Storable(text)) ? text : current) }; _pins.Add(pin); _byId[pin.Id] = pin; return null; } private static string ProfileOf(Setting setting) { if (Released.TryGetValue(setting.Id, out var value)) { return value; } if (_keptAtLaunch != null && _keptAtLaunch.Contains(setting.Id)) { return null; } return Session.InitialOf(setting.Id); } public static string Unpin(string id) { if (UnpinAll(new string[1] { id }) <= 0 && Find(id) != null) { return "keepsake.pins could not be read or saved, so nothing changed; see the log"; } return null; } public static int UnpinAll(IEnumerable<string> ids) { Sync(); List<Pin> list = new List<Pin>(); foreach (string id in ids) { Pin pin = Find(id); if (pin != null) { _pins.Remove(pin); _byId.Remove(id); list.Add(pin); } } if (list.Count == 0) { return 0; } if (!Save()) { _pins.AddRange(list); Index(); return 0; } bool flag = false; foreach (Pin pin2 in list) { if (pin2.Profile != null) { Released[pin2.Id] = pin2.Profile; } flag |= Changes.Waiting.RemoveAll((ProfileChange c) => c.Id == pin2.Id) > 0; flag |= Changes.Quiet.RemoveAll((QuietSetting q) => q.Id == pin2.Id) > 0; Setting setting = SettingIndex.Find(pin2.Id); if (setting != null && !setting.LeftToBindrune && pin2.Profile != null && setting.Current != pin2.Profile) { Write(setting, pin2.Profile); } } if (flag) { SaveChanges(); } return list.Count; } public static string SetValue(Setting setting, string text) { Sync(); Pin pin = Find(setting.Id); if (pin == null) { return "keep this setting first"; } if (setting.LeftToBindrune) { return "Bindrune keeps your keybinds. Set this key as yours in Bindrune to keep it."; } text = (text ?? "").Trim(); try { TomlTypeConverter.ConvertToValue(text, setting.Entry.SettingType); } catch (Exception) { return "\"" + text + "\" is not a valid value for " + setting.Key; } if (!PinFile.Storable(text)) { return "this value has line breaks or tabs in it, which Keepsake cannot store"; } string value = pin.Value; pin.Value = text; if (!Save()) { pin.Value = value; return "keepsake.pins could not be read or saved, so nothing changed; see the log"; } Write(setting, text); string text2 = setting.Current ?? text; if (text2 != text && !PinFile.Storable(text2)) { Write(setting, value); pin.Value = value; Save(); return setting.Key + " would become a value with line breaks or tabs in it, which Keepsake cannot store, so it stays as it was"; } if (text2 != text) { pin.Value = text2; Save(); } Answered(setting.Id); return null; } public static ProfileChange ProfileChangeOf(string id) { if (id == null || Find(id) == null) { return null; } return Changes.Waiting.FirstOrDefault((ProfileChange c) => c.Id == id); } public static List<string> ProfileChanged() { return (from c in Changes.Waiting select c.Id into id where Find(id) != null select id).ToList(); } public static bool IsQuiet(string id) { if (id != null) { return Changes.IsQuiet(id); } return false; } public static void SetQuiet(string id, bool quiet) { Pin pin = Find(id); if (pin == null || IsQuiet(id) == quiet) { return; } if (quiet) { Changes.Quiet.Add(QuietSetting.Of(pin)); Changes.Waiting.RemoveAll((ProfileChange c) => c.Id == id); } else { Changes.Quiet.RemoveAll((QuietSetting q) => q.Id == id); } SaveChanges(); } private static void NoteChange(Pin pin, string from, string to) { if (ProfileChanges.Merge(Changes, new ProfileChange[1] { ProfileChange.Of(pin, from, to) })) { SaveChanges(); } } private static void Answered(string id) { if (Changes.Waiting.RemoveAll((ProfileChange c) => c.Id == id) > 0) { SaveChanges(); } } private static void SaveChanges() { if (!_changesUnreadable && !_unreadable) { ProfileChanges.KeepOnly(Changes, _byId.Keys); ProfileChanges.Write(Changes); SpareCopy.Follow(listsOnly: true); } } public static string UseProfiles(Setting setting) { Pin pin = Find(setting.Id); if (pin == null || pin.Profile == null) { return "the profile's value is not known"; } return SetValue(setting, pin.Profile); } public static void KeepMine(string id) { Answered(id); } public static int Reconcile() { Sync(); SettingIndex.Refresh(); if (_unreadable) { return 1; } int num = 0; bool flag = false; foreach (Pin pin in _pins) { Setting setting = SettingIndex.Find(pin.Id); if (setting == null) { num++; } else { if (setting.LeftToBindrune) { continue; } string current2 = setting.Current; if (current2 == null || current2 == pin.Value) { continue; } if (pin.Profile != current2) { if (pin.Profile != null) { NoteChange(pin, pin.Profile, current2); } flag = true; } pin.Profile = current2; Write(setting, pin.Value); if (setting.Current != pin.Value) { Plugin.WarnOnce("Keepsake: " + setting.ModName + " [" + pin.Section + "] " + pin.Key + " does not accept your value " + pin.Value + ", so it stays at " + setting.Current + ".", null, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Keeper.cs", 417); } else { Plugin.Log.LogInfo((object)("Keepsake: kept your value for " + setting.ModName + " [" + pin.Section + "] " + pin.Key + ": " + pin.Value + " (it was " + current2 + ").")); } } } if (flag) { Save(); } return num; } public static List<BindruneKeep> BindruneKeys() { if (SettingIndex.BindruneLoaded) { return new List<BindruneKeep>(); } List<BindruneKey> list = BindruneLink.ReadKeys(); if (list.Count == 0) { return new List<BindruneKeep>(); } IEnumerable<KeybindSlot<Setting>> slots = from s in SettingIndex.All where s.IsKeybind && Find(s.Id) == null select new KeybindSlot<Setting> { BindruneId = Keepsake.BindruneKeys.IdOf(s.ModGuid, s.Section, s.Key), IsKeyCode = (s.Entry.SettingType == typeof(KeyCode)), Setting = s }; return (from m in Keepsake.BindruneKeys.Match(list, slots) where PinFile.Storable(m.Yours) select new BindruneKeep { Setting = m.Setting, Yours = m.Yours, Profile = m.Profile }).ToList(); } public static int TakeOverFromBindrune() { Sync(); int num = 0; foreach (BindruneKeep item in BindruneKeys()) { Write(item.Setting, item.Yours); string current2 = item.Setting.Current; if (current2 != null && PinFile.Storable(current2)) { _pins.Add(new Pin { File = item.Setting.File, Section = item.Setting.Section, Key = item.Setting.Key, Value = current2, Profile = ((item.Profile != null && PinFile.Storable(item.Profile)) ? item.Profile : current2) }); num++; Plugin.Log.LogInfo((object)("Keepsake: took over " + current2 + " for " + item.Setting.ModName + " [" + item.Setting.Section + "] " + item.Setting.Key + " from Bindrune.")); } } if (num > 0) { Save(); } return num; } public static void Follow(ConfigFile config) { if (Followed.Add(config)) { config.SettingChanged += OnSettingChanged; } } private static void OnSettingChanged(object sender, SettingChangedEventArgs args) { if (_writing) { return; } ConfigEntryBase val = ((args != null) ? args.ChangedSetting : null); if (val != null && val.ConfigFile.Contains(new KeyValuePair<ConfigDefinition, ConfigEntryBase>(val.Definition, val))) { if (MainThread != 0 && Environment.CurrentManagedThreadId != MainThread) { OffThread.Enqueue(val); } else { FollowChange(val); } } } private static void FollowChange(ConfigEntryBase entry) { try { string text = SettingIndex.Find(entry)?.Id; if (text == null) { string text2 = SettingIndex.FileOf(entry.ConfigFile); if (text2 == null) { return; } text = PinFile.IdOf(text2, entry.Definition.Section, entry.Definition.Key); } Session.Touch(text); Changed?.Invoke(text); Pin pin = Find(text); if (pin == null) { return; } Setting setting = SettingIndex.Find(entry); if (setting == null || !setting.LeftToBindrune) { string serializedValue = entry.GetSerializedValue(); if (PinFile.Storable(serializedValue) && !(pin.Value == serializedValue)) { pin.Value = serializedValue; Pending[text] = serializedValue; _followedSinceTick = true; } } } catch (Exception ex) { Plugin.WarnOnce("Keepsake: following a setting change failed: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Keeper.cs", 572); } } public static void Tick(float now) { ConfigEntryBase result; while (OffThread.TryDequeue(out result)) { FollowChange(result); } if (_followedSinceTick) { _followedSinceTick = false; _flushAt = now + 1f; } if (Pending.Count > 0 && now >= _flushAt) { Flush(); } } public static void Flush() { if (Pending.Count != 0) { Sync(); Save(); } } private static void Write(Setting setting, string serialized) { _writing = true; try { setting.Entry.SetSerializedValue(serialized); ConfigFile configFile = setting.Entry.ConfigFile; if (configFile != null && !configFile.SaveOnConfigSet) { configFile.Save(); } } catch (Exception ex) { Plugin.WarnOnce("Keepsake: could not write " + setting.ModName + " [" + setting.Section + "] " + setting.Key + ": " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Keeper.cs", 626); } finally { _writing = false; } } private static bool Save() { Index(); if (_unreadable) { Plugin.WarnOnce("Keepsake: keepsake.pins could not be read, so changes are not saved to it until it can.", null, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Keeper.cs", 640); return false; } if (!PinFile.Write(_pins)) { return false; } _stamp = PinFile.Stamp(); SpareCopy.Follow(listsOnly: true); foreach (string key in Pending.Keys) { if (_byId.TryGetValue(key, out var value)) { Plugin.Log.LogInfo((object)("Keepsake: your value for " + value.File + " [" + value.Section + "] " + value.Key + " is now " + value.Value + ".")); } } Pending.Clear(); return true; } internal static void Reset() { foreach (ConfigFile item in Followed) { item.SettingChanged -= OnSettingChanged; } Followed.Clear(); _pins = new List<Pin>(); _byId = new Dictionary<string, Pin>(); _stamp = null; _loaded = false; _unreadable = false; _keptAtLaunch = null; Released.Clear(); _changes = null; _changesUnreadable = false; Pending.Clear(); _followedSinceTick = false; _writing = false; _flushAt = 0f; Changed = null; ConfigEntryBase result; while (OffThread.TryDequeue(out result)) { } MainThread = 0; } private static void Index() { Dictionary<string, Pin> dictionary = new Dictionary<string, Pin>(); foreach (Pin pin in _pins) { dictionary[pin.Id] = pin; } _byId = dictionary; } } internal static class KeyLabels { private static readonly Dictionary<KeyCode, string> Cache = new Dictionary<KeyCode, string>(); public static string Shown(Setting setting, string value) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) if (value == null || setting == null || !setting.IsKeybind) { return value; } try { if (setting.Entry.SettingType == typeof(KeyCode)) { return Of((KeyCode)TomlTypeConverter.ConvertToValue(value, typeof(KeyCode))); } KeyboardShortcut val = KeyboardShortcut.Deserialize(value); if ((int)((KeyboardShortcut)(ref val)).MainKey == 0) { return value; } return string.Join(" + ", (from k in ((KeyboardShortcut)(ref val)).Modifiers.Distinct() orderby (int)k select k).Select(Of).Concat(new string[1] { Of(((KeyboardShortcut)(ref val)).MainKey) }).ToArray()); } catch (Exception) { return value; } } public unsafe static string Of(KeyCode key) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) if (Cache.TryGetValue(key, out var value)) { return value; } string text = ((object)(*(KeyCode*)(&key))/*cast due to .constrained prefix*/).ToString(); try { string text2 = ZInput.KeyCodeToDisplayName(key); if (text2 == null || text2.StartsWith("$KeyCode ", StringComparison.Ordinal)) { return text; } string text3 = Pick(text2.Trim(), text); Cache[key] = text3; return text3; } catch { return text; } } private static string Pick(string shown, string name) { if (shown.Length != 1) { return name; } char c = shown[0]; if (char.IsWhiteSpace(c) || char.IsControl(c)) { return name; } return char.ToUpperInvariant(c).ToString(); } } internal sealed class NaturalOrder : IComparer<string> { public static readonly NaturalOrder IgnoreCase = new NaturalOrder(); public int Compare(string a, string b) { if ((object)a == b) { return 0; } if (a == null) { return -1; } if (b == null) { return 1; } int i = 0; int j = 0; while (i < a.Length && j < b.Length) { if (IsDigit(a[i]) && IsDigit(b[j])) { int num = i; int num2 = j; for (; i < a.Length && IsDigit(a[i]); i++) { } for (; j < b.Length && IsDigit(b[j]); j++) { } string text = a.Substring(num, i - num).TrimStart(new char[1] { '0' }); string text2 = b.Substring(num2, j - num2).TrimStart(new char[1] { '0' }); if (text.Length != text2.Length) { return text.Length.CompareTo(text2.Length); } int num3 = string.CompareOrdinal(text, text2); if (num3 != 0) { return num3; } } else { char c = char.ToUpperInvariant(a[i]); char c2 = char.ToUpperInvariant(b[j]); if (c != c2) { return c.CompareTo(c2); } i++; j++; } } int num4 = (a.Length - i).CompareTo(b.Length - j); if (num4 != 0) { return num4; } int num5 = StringComparer.OrdinalIgnoreCase.Compare(a, b); if (num5 == 0) { return string.CompareOrdinal(a, b); } return num5; } private static bool IsDigit(char c) { if (c >= '0') { return c <= '9'; } return false; } } [BepInPlugin("isimp.Keepsake", "Keepsake", "0.5.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInProcess("valheim.exe")] public class Plugin : BaseUnityPlugin { public const string Guid = "isimp.Keepsake"; public const string Version = "0.5.1"; internal static ManualLogSource Log; private static readonly HashSet<string> Warned = new HashSet<string>(); private ConfigEntry<KeyboardShortcut> _openKey; private static ConfigEntry<bool> _playSounds; private ConfigEntry<bool> _profileChangeNotice; private static ConfigEntry<float> _panelWidth; private static ConfigEntry<float> _panelHeight; private bool _reconciled; private bool _reconcileInWorld; private float _reconcileAt; private bool _waitingChecked; private float _waitingCheckAt; private bool _noticeShown; private float _noticeAt; internal static bool PlaySounds { get { if (_playSounds != null) { return _playSounds.Value; } return true; } } internal static Vector2 PanelSize { get { //IL_002a: Unknown result type (might be due to invalid IL or missing references) return new Vector2(_panelWidth?.Value ?? 1320f, _panelHeight?.Value ?? 820f); } set { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) if (_panelWidth != null && _panelHeight != null) { _panelWidth.Value = value.x; _panelHeight.Value = value.y; } } } internal static void WarnOnce(string message, Exception detail = null, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0) { if (Warned.Add(file + ":" + line)) { Log.LogWarning((object)((detail != null) ? (message + "\n" + detail) : message)); } else { Log.LogDebug((object)message); } } private void Awake() { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Expected O, but got Unknown //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Expected O, but got Unknown //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Keeper.MainThread = Environment.CurrentManagedThreadId; PinFile.Log = ((BaseUnityPlugin)this).Logger; SettingIndex.FileFound = Keeper.Follow; SettingIndex.SettingFound = Session.Note; _openKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "OpenKey", new KeyboardShortcut((KeyCode)278, Array.Empty<KeyCode>()), "Opens the Keepsake panel."); _panelWidth = ((BaseUnityPlugin)this).Config.Bind<float>("Panel", "Width", 1320f, new ConfigDescription("Panel width in pixels. Set by dragging the corner handle.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1100f, 3840f), Array.Empty<object>())); _panelHeight = ((BaseUnityPlugin)this).Config.Bind<float>("Panel", "Height", 820f, new ConfigDescription("Panel height in pixels. Set by dragging the corner handle.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(560f, 2160f), Array.Empty<object>())); _playSounds = ((BaseUnityPlugin)this).Config.Bind<bool>("Panel", "PlaySounds", true, "Play the game's interface sounds when the panel opens and closes, and when a setting is kept, released or given a new value."); _profileChangeNotice = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ProfileChangeNotice", true, "Say on screen, once your character appears, when the profile changed values you keep, or kept files were left as they are, and those wait for an answer in the panel."); StartMenuKeys.Patch(new Harmony("isimp.Keepsake")); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; Log.LogInfo((object)"Keepsake loaded."); } private void OnDestroy() { FlushQuietly("destroy"); try { KeepsakePanel.Close(quietly: true); } catch (Exception ex) { WarnOnce("Keepsake: closing the panel failed: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Plugin.cs", 113); } } private void OnApplicationQuit() { FlushQuietly("quit"); } private static void OnProcessExit(object sender, EventArgs e) { try { FileKeeper.Close("exit"); SpareCopy.Update(); } catch (Exception) { } } private static void FlushQuietly(string by) { try { GameClose.Run(by); } catch (Exception ex) { WarnOnce("Keepsake: saving your latest values failed: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Plugin.cs", 148); } } private void Update() { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) try { Keeper.Tick(Time.realtimeSinceStartup); ReconcileOnce(); CheckWaitingOnce(); NoticeProfileChangesOnce(); if (KeyCapture.Active) { KeyCapture.Tick(); return; } if (KeepsakePanel.IsOpen) { KeepsakePanel.Tick(); } if (KeepsakePanel.Typing) { if (Input.GetKeyDown((KeyCode)27)) { KeepsakePanel.Close(); } return; } if (KeepsakePanel.IsOpen && Input.GetKeyDown((KeyCode)27)) { KeepsakePanel.Close(); return; } KeyboardShortcut value = _openKey.Value; if (((KeyboardShortcut)(ref value)).IsDown() && !TypingElsewhere()) { KeepsakePanel.Toggle(); } } catch (Exception ex) { WarnOnce("Keepsake: input check failed: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Plugin.cs", 183); } } private void NoticeProfileChangesOnce() { if (_noticeShown || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)MessageHud.instance == (Object)null) { return; } if (_noticeAt == 0f) { _noticeAt = Time.realtimeSinceStartup + 5f; } if (Time.realtimeSinceStartup < _noticeAt) { return; } _noticeShown = true; if (SpareCopy.RestoredThisLaunch) { MessageHud.instance.ShowMessage((MessageType)1, "Keepsake: the profile was replaced by an update, so what you kept came back from its spare copy.", 0, (Sprite)null, false, true); } if (!_profileChangeNotice.Value) { return; } int count = Keeper.ProfileChanged().Count; int waitingCount = FileKeeper.WaitingCount; if (count != 0 || waitingCount != 0) { List<string> list = new List<string>(); if (count > 0) { list.Add("the profile changed " + ((count == 1) ? "a value" : (count + " values")) + " you keep"); } if (waitingCount > 0) { list.Add(((waitingCount == 1) ? "a kept file waits" : (waitingCount + " kept files wait")) + " for an answer in Files"); } MessageHud.instance.ShowMessage((MessageType)1, "Keepsake: " + string.Join(", and ", list.ToArray()) + ". Press " + OpenKeyLabel() + " to look.", 0, (Sprite)null, false, true); } } private string OpenKeyLabel() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = _openKey.Value; return string.Join(" + ", ((KeyboardShortcut)(ref value)).Modifiers.OrderBy((KeyCode k) => (int)k).Select(KeyLabels.Of).Concat(new string[1] { KeyLabels.Of(((KeyboardShortcut)(ref value)).MainKey) }) .ToArray()); } private void CheckWaitingOnce() { if (_waitingChecked || (Object)(object)Player.m_localPlayer == (Object)null) { return; } if (_waitingCheckAt == 0f) { _waitingCheckAt = Time.realtimeSinceStartup + 10f; } if (Time.realtimeSinceStartup < _waitingCheckAt) { return; } _waitingChecked = true; if (SettingIndex.BindruneLoaded) { Keeper.Sync(); int num = Keeper.Pins.Count((Pin p) => SettingIndex.Find(p.Id)?.IsKeybind ?? false); if (num != 0) { Log.LogWarning((object)($"Keepsake: {num} kept keybind(s) are waiting for Bindrune to take them over, and nothing " + "keeps them until it does. Update Bindrune, or set these keys as yours in Bindrune and release them in Keepsake.")); } } } private static bool TypingElsewhere() { GameObject val = (((Object)(object)EventSystem.current != (Object)null) ? EventSystem.current.currentSelectedGameObject : null); if ((Object)(object)val == (Object)null) { return false; } InputField component = val.GetComponent<InputField>(); if ((Object)(object)component != (Object)null && component.isFocused) { return true; } TMP_InputField component2 = val.GetComponent<TMP_InputField>(); if ((Object)(object)component2 != (Object)null) { return component2.isFocused; } return false; } private void ReconcileOnce() { if (_reconciled) { return; } if (_reconcileInWorld) { if ((Object)(object)Player.m_localPlayer == (Object)null) { _reconcileAt = 0f; return; } if (_reconcileAt == 0f) { _reconcileAt = Time.realtimeSinceStartup + 2f; } if (!(Time.realtimeSinceStartup < _reconcileAt)) { Keeper.Reconcile(); _reconciled = true; } } else if (!((Object)(object)FejdStartup.instance == (Object)null) || !((Object)(object)Player.m_localPlayer == (Object)null)) { if (Keeper.Reconcile() == 0) { _reconciled = true; } else { _reconcileInWorld = true; } } } } internal static class Session { private static readonly Dictionary<string, string> Initial = new Dictionary<string, string>(); private static readonly HashSet<string> Touched = new HashSet<string>(); public static void Note(Setting setting) { if (!Initial.ContainsKey(setting.Id)) { Initial[setting.Id] = setting.Current; } } public static void Touch(string id) { Touched.Add(id); } public static string InitialOf(string id) { if (!Initial.TryGetValue(id, out var value)) { return null; } return value; } public static bool IsChanged(Setting setting) { if (Touched.Contains(setting.Id) && Initial.TryGetValue(setting.Id, out var value)) { return setting.Current != value; } return false; } public static List<Setting> Changed() { return (from s in Touched.Select(SettingIndex.Find) where s != null && IsChanged(s) select s).ToList(); } internal static void Reset() { Initial.Clear(); Touched.Clear(); } } internal enum ServerControl { None, ServerSync, Jotunn } internal sealed class ModInfo { public string Name; public string Version; public string File; public int Count; } internal sealed class Setting { public ConfigEntryBase Entry; public string File; public string ModName; public string ModVersion; public string ModGuid; public string Id; public ServerControl Server; public string SearchText; private string _default; private bool _defaultRead; public string Section => Entry.Definition.Section; public string Key => Entry.Definition.Key; public string Description { get { ConfigDescription description = Entry.Description; return ((description != null) ? description.Description : null) ?? ""; } } public bool IsKeybind { get { if (!(Entry.SettingType == typeof(KeyCode))) { return Entry.SettingType == typeof(KeyboardShortcut); } return true; } } public bool LeftToBindrune { get { if (IsKeybind) { return SettingIndex.BindruneLoaded; } return false; } } public string Default { get { if (_defaultRead) { return _default; } _defaultRead = true; try { _default = TomlTypeConverter.ConvertToString(Entry.DefaultValue, Entry.SettingType); } catch (Exception) { _default = null; } return _default; } } public string Current { get { try { return Entry.GetSerializedValue(); } catch (Exception ex) { Plugin.WarnOnce("Keepsake: could not read " + ModName + " [" + Section + "] " + Key + ": " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/SettingIndex.cs", 92); return null; } } } } internal static class SettingIndex { internal sealed class LoadedConfig { public ConfigFile Config; public string Name; public string Version; public string Guid; } private static readonly Dictionary<string, Setting> ById = new Dictionary<string, Setting>(); private static readonly Dictionary<ConfigEntryBase, Setting> ByEntry = new Dictionary<ConfigEntryBase, Setting>(); private static readonly Dictionary<ConfigFile, string> Files = new Dictionary<ConfigFile, string>(); public static Action<ConfigFile> FileFound; public static Action<Setting> SettingFound; internal static Func<bool> IsBindruneLoaded = BindruneInLoader; internal static Func<IEnumerable<LoadedConfig>> Sources = Loaded; private static Dictionary<string, ModInfo> _mods = new Dictionary<string, ModInfo>(); public static List<Setting> All { get; private set; } = new List<Setting>(); public static List<string> Mods { get; private set; } = new List<string>(); public static bool BindruneLoaded => IsBindruneLoaded(); public static double LastRefreshMs { get; private set; } public static bool LastRefreshSorted { get; private set; } public static Setting Find(string id)
patchers/Keepsake.Preloader.dll
Decompiled 3 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Logging; using Microsoft.CodeAnalysis; using Mono.Cecil; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("isimp")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 isimp")] [assembly: AssemblyDescription("Puts your kept config values back before any mod reads them.")] [assembly: AssemblyFileVersion("0.5.1.0")] [assembly: AssemblyInformationalVersion("0.5.1+8850ea0a5cea437e3ed82133e6cd4478d956b296")] [assembly: AssemblyProduct("Keepsake")] [assembly: AssemblyTitle("Keepsake.Preloader")] [assembly: AssemblyVersion("0.5.1.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 Keepsake { internal sealed class BindruneKey { public string Id; public string Yours; public string Profile; public bool Active; } internal static class BindruneLink { public const string Guid = "isimp.Bindrune"; private const string DllName = "Bindrune.dll"; public const string StateVersion = "# bindrune state v3"; private static bool _warnedVersion; public static string KeysFile => Path.Combine(Paths.BepInExRootPath, "bindrune.keys"); public static bool InstalledOnDisk() { try { string pluginPath = Paths.PluginPath; if (!Directory.Exists(pluginPath)) { return false; } if (File.Exists(Path.Combine(pluginPath, "Bindrune.dll"))) { return true; } if (Directory.GetDirectories(pluginPath).Any((string dir) => File.Exists(Path.Combine(dir, "Bindrune.dll")))) { return true; } return Directory.GetFiles(pluginPath, "Bindrune.dll", SearchOption.AllDirectories).Any((string f) => string.Equals(Path.GetFileName(f), "Bindrune.dll", StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look for Bindrune: " + ex.Message)); } return false; } } public static bool IsKeybindType(string typeName) { if (!(typeName == "KeyCode")) { return typeName == "KeyboardShortcut"; } return true; } public static List<BindruneKey> ReadKeys() { string[] lines; try { if (!File.Exists(KeysFile)) { return new List<BindruneKey>(); } lines = File.ReadAllLines(KeysFile); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(KeysFile) + ": " + ex.Message)); } return new List<BindruneKey>(); } List<BindruneKey> list = ParseKeys(lines); if (list != null) { return list; } if (!_warnedVersion) { _warnedVersion = true; ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: " + Path.GetFileName(KeysFile) + " is not in a version this Keepsake knows (# bindrune state v3), so its keys are not offered. Updating Keepsake fixes this.")); } } return new List<BindruneKey>(); } public static List<BindruneKey> ParseKeys(IEnumerable<string> lines) { List<BindruneKey> list = new List<BindruneKey>(); bool flag = false; bool flag2 = false; foreach (string line in lines) { if (!flag) { if (line.Trim().Length != 0) { if (line.Trim() != "# bindrune state v3") { return null; } flag = true; } continue; } string text = line.Trim(); if (text.StartsWith("[", StringComparison.Ordinal) && text.EndsWith("]", StringComparison.Ordinal)) { flag2 = text == "[keys]"; } else if (flag2 && text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal)) { string[] array = line.Split(new char[1] { '\t' }); if (array.Length >= 2) { list.Add(new BindruneKey { Id = array[0], Yours = array[1].Trim(), Profile = ((array.Length > 2) ? array[2].Trim() : "none"), Active = (array.Length < 4 || array[3].Trim() == "1") }); } } } return list; } } internal sealed class CfgText { private static readonly char[] EqualsSign = new char[1] { '=' }; private readonly string[] _lines; private readonly string _newline; private readonly bool _byteOrderMark; private bool _changed; public string Text => string.Join(_newline, _lines); public bool Changed => _changed; private CfgText(string text, bool byteOrderMark = false) { _newline = (text.Contains("\r\n") ? "\r\n" : "\n"); _lines = text.Replace("\r\n", "\n").Split(new char[1] { '\n' }); _byteOrderMark = byteOrderMark; } public static CfgText Load(string path) { byte[] array = File.ReadAllBytes(path); bool flag = array.Length >= 3 && array[0] == 239 && array[1] == 187 && array[2] == 191; return new CfgText(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetString(array, flag ? 3 : 0, array.Length - (flag ? 3 : 0)), flag); } public static CfgText Parse(string text) { return new CfgText(text); } public bool TryGet(string section, string key, out string value) { return Find(section, key, out value) >= 0; } public string TypeOf(string section, string key) { string value; for (int num = Find(section, key, out value) - 1; num >= 0; num--) { string text = _lines[num].Trim(); if (!text.StartsWith("#", StringComparison.Ordinal)) { break; } if (text.StartsWith("# Setting type:", StringComparison.Ordinal)) { return text.Substring("# Setting type:".Length).Trim(); } } return null; } public bool Set(string section, string key, string value) { string value2; int num = Find(section, key, out value2); if (num < 0) { return false; } if (value2 == value) { return true; } _lines[num] = key + " = " + value; _changed = true; return true; } public void Save(string path) { PinFile.ReplaceText(path, Text, _byteOrderMark); } private int Find(string section, string key, out string value) { value = null; int result = -1; string text = string.Empty; for (int i = 0; i < _lines.Length; i++) { string text2 = _lines[i].Trim(); if (text2.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2.Substring(1, text2.Length - 2); } else if (!(text != section)) { string[] array = text2.Split(EqualsSign, 2); if (array.Length == 2 && !(array[0].Trim() != key)) { result = i; value = array[1].Trim(); } } } return result; } } internal sealed class KeptPath { public string Path; public bool IsFolder; public bool Covers(string path) { if (!string.Equals(Path, path, StringComparison.OrdinalIgnoreCase)) { if (IsFolder) { return path.StartsWith(Path + "/", StringComparison.OrdinalIgnoreCase); } return false; } return true; } } internal sealed class SettleResult { public bool Clean; public int PutBack; public int Updated; public int Waiting; } internal static class KeptFiles { public const string Version = "# keepsake files v1"; private const string VersionPrefix = "# keepsake files v"; public const string CopySuffix = ".kept"; private static readonly char[] Slash = new char[1] { '/' }; private static readonly string[] Header = new string[4] { "# keepsake files v1", "# Files and folders in BepInEx/config that Keepsake keeps through profile syncs, one", "# per line, relative to BepInEx/config. A folder ends with a slash. Copies are kept in", "# BepInEx/keepsake-files." }; public static readonly TimeSpan LogGrace = TimeSpan.FromMinutes(1.0); public static string ListPath => Path.Combine(Paths.BepInExRootPath, "keepsake.files"); public static string StoreRoot => Path.Combine(Paths.BepInExRootPath, "keepsake-files"); public static string Live(string path) { return Path.Combine(Paths.ConfigPath, path.Replace('/', Path.DirectorySeparatorChar)); } public static string Copy(string path) { return Path.Combine(StoreRoot, path.Replace('/', Path.DirectorySeparatorChar)) + ".kept"; } public static string Normalise(string path) { if (string.IsNullOrWhiteSpace(path)) { return null; } string[] array = path.Trim().Replace('\\', '/').Split(Slash, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0 || array.Any((string p) => p == "." || p == ".." || p.Contains(":"))) { return null; } if (path.Trim().StartsWith("/", StringComparison.Ordinal) || path.Trim().StartsWith("\\", StringComparison.Ordinal)) { return null; } return string.Join("/", array); } public static List<KeptPath> Read() { string[] lines; try { if (!File.Exists(ListPath)) { return new List<KeptPath>(); } lines = File.ReadAllLines(ListPath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(ListPath) + ": " + ex.Message)); } return null; } List<KeptPath> list = Parse(lines); if (list == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return list; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(ListPath) + " was written by a newer Keepsake, so it is left as it is.")); } return list; } public static List<KeptPath> Parse(IEnumerable<string> lines) { List<KeptPath> list = new List<KeptPath>(); foreach (string line in lines) { if (line.StartsWith("# keepsake files v", StringComparison.Ordinal) && line.Trim() != "# keepsake files v1") { return null; } if (line.Trim().Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text = line.Trim(); string path = Normalise(text); if (path == null) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: skipped " + text + " in keepsake.files, which is not a path inside BepInEx/config.")); } } else if (!list.Any((KeptPath k) => string.Equals(k.Path, path, StringComparison.OrdinalIgnoreCase))) { list.Add(new KeptPath { Path = path, IsFolder = (text.EndsWith("/", StringComparison.Ordinal) || text.EndsWith("\\", StringComparison.Ordinal)) }); } } return list; } public static string[] Format(IEnumerable<KeptPath> kept) { List<string> list = new List<string>(Header); list.AddRange(from k in kept.OrderBy<KeptPath, string>((KeptPath k) => k.Path, StringComparer.OrdinalIgnoreCase) select (!k.IsFolder) ? k.Path : (k.Path + "/")); return list.ToArray(); } public static bool Write(IEnumerable<KeptPath> kept) { List<KeptPath> list = kept.ToList(); try { if (list.Count == 0) { if (File.Exists(ListPath)) { File.Delete(ListPath); } return true; } PinFile.ReplaceText(ListPath, string.Join(Environment.NewLine, Format(list)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(ListPath) + ": " + ex.Message)); } return false; } } public static List<string> LiveFiles(KeptPath kept) { string live = Live(kept.Path); if (!kept.IsFolder) { if (!File.Exists(live)) { return new List<string>(); } return new List<string> { kept.Path }; } if (!Directory.Exists(live)) { return new List<string>(); } return (from f in Directory.GetFiles(live, "*", SearchOption.AllDirectories) where !f.EndsWith(".keepsake.tmp", StringComparison.OrdinalIgnoreCase) select kept.Path + "/" + f.Substring(live.Length).TrimStart('\\', '/').Replace('\\', '/')).ToList(); } public static List<string> CopiedFiles(KeptPath kept) { if (!kept.IsFolder) { if (!File.Exists(Copy(kept.Path))) { return new List<string>(); } return new List<string> { kept.Path }; } string folder = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(folder)) { return new List<string>(); } return (from f in Directory.GetFiles(folder, "*.kept", SearchOption.AllDirectories) where f.EndsWith(".kept", StringComparison.OrdinalIgnoreCase) select f.Substring(0, f.Length - ".kept".Length) into f select kept.Path + "/" + f.Substring(folder.Length).TrimStart('\\', '/').Replace('\\', '/')).ToList(); } public static bool Differ(string a, string b) { FileInfo fileInfo = new FileInfo(a); FileInfo fileInfo2 = new FileInfo(b); if (!fileInfo.Exists || !fileInfo2.Exists) { return fileInfo.Exists != fileInfo2.Exists; } if (fileInfo.Length == fileInfo2.Length) { return fileInfo.LastWriteTimeUtc != fileInfo2.LastWriteTimeUtc; } return true; } public static void CopyOver(string from, string to) { string directoryName = Path.GetDirectoryName(to); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string text = to + ".keepsake.tmp"; File.Copy(from, text, overwrite: true); if (File.Exists(to)) { File.Replace(text, to, null); } else { File.Move(text, to); } File.SetLastWriteTimeUtc(to, File.GetLastWriteTimeUtc(from)); } public static int Save(IEnumerable<KeptPath> kept, Func<string, bool> waiting = null) { int num = 0; foreach (KeptPath item in kept) { List<string> list; try { list = LiveFiles(item); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through " + item.Path + ": " + ex.Message)); } continue; } foreach (string item2 in list) { try { if ((waiting == null || !waiting(item2)) && Differ(Live(item2), Copy(item2)) && Trash.Put(item2, Copy(item2), TrashReason.Copy)) { CopyOver(Live(item2), Copy(item2)); num++; } } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not save a copy of " + item2 + ": " + ex2.Message)); } } } } return num; } public static bool ClosedCleanly(SessionState state, DateTime? logEnd) { if (state.Closed.HasValue && (!state.Started.HasValue || state.Closed >= state.Started)) { if (logEnd.HasValue) { return logEnd <= state.Closed + LogGrace; } return true; } return false; } public static SettleResult Settle(IEnumerable<KeptPath> kept, SessionState state, DateTime? logEnd) { SettleResult settleResult = new SettleResult { Clean = ClosedCleanly(state, logEnd) }; DateTime? dateTime = (settleResult.Clean ? state.Closed : ((DateTime?)null)); DateTime? end = (settleResult.Clean ? Later(state.Closed, logEnd) : logEnd); HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeptPath item in kept) { List<string> list; try { list = CopiedFiles(item); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the copies of " + item.Path + ": " + ex.Message)); } continue; } foreach (string item2 in list) { seen.Add(item2); try { SettleOne(item2, state, dateTime, end, settleResult); } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not settle " + item2 + ": " + ex2.Message)); } } } } state.Waiting.RemoveAll((WaitingFile w) => !seen.Contains(w.Path)); return settleResult; } private static void SettleOne(string path, SessionState state, DateTime? from, DateTime? end, SettleResult result) { string text = Live(path); string text2 = Copy(path); WaitingFile waitingFile = state.WaitingFor(path); if (waitingFile != null) { if (waitingFile.PutBack) { if (!Trash.Put(path, text, TrashReason.Replaced)) { result.Waiting++; return; } CopyOver(text2, text); state.Waiting.Remove(waitingFile); result.PutBack++; ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)("Keepsake: put back your copy of " + path + ", as you chose.")); } } else if (!Differ(text2, text)) { state.Waiting.Remove(waitingFile); } else { result.Waiting++; } return; } if (!File.Exists(text)) { CopyOver(text2, text); result.PutBack++; ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogInfo((object)("Keepsake: put back your copy of " + path + ", which was missing.")); } return; } if (!Differ(text2, text)) { return; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(text); if (end.HasValue) { DateTime value = lastWriteTimeUtc; DateTime? dateTime = end; if (value <= dateTime) { if (from.HasValue) { value = lastWriteTimeUtc; dateTime = from; if (!(value > dateTime)) { goto IL_0191; } } if (Trash.Put(path, text2, TrashReason.Copy)) { CopyOver(text, text2); result.Updated++; ManualLogSource log3 = PinFile.Log; if (log3 != null) { log3.LogInfo((object)("Keepsake: " + path + " was written while the game ran, so your copy now matches it.")); } } return; } } goto IL_0191; IL_0191: if (result.Clean) { if (Trash.Put(path, text, TrashReason.Replaced)) { CopyOver(text2, text); result.PutBack++; ManualLogSource log4 = PinFile.Log; if (log4 != null) { DateTime value = lastWriteTimeUtc; DateTime? dateTime = from; log4.LogInfo((object)("Keepsake: put back your copy of " + path + ", which changed after the game closed" + ((value <= dateTime) ? ", though it carries an older time, as mod managers give files they extract." : "."))); } } } else { state.Waiting.Add(new WaitingFile { Path = path }); result.Waiting++; ManualLogSource log5 = PinFile.Log; if (log5 != null) { log5.LogInfo((object)("Keepsake: left " + path + " as it is. It changed after a game Keepsake did not see close, so it waits for you to choose between it and your copy in the panel.")); } } } private static DateTime? Later(DateTime? a, DateTime? b) { if (a.HasValue) { if (b.HasValue) { if (!(a > b)) { return b; } return a; } return a; } return b; } public static bool SetAside(KeptPath kept) { try { return CopiedFiles(kept).All((string path) => Trash.Put(path, Copy(path), TrashReason.Released)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the copies of " + kept.Path + ": " + ex.Message)); } return false; } } public static void Forget(KeptPath kept) { try { foreach (string item in CopiedFiles(kept)) { File.Delete(Copy(item)); } if (!kept.IsFolder) { return; } string text = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(text)) { return; } foreach (string item2 in (from d in Directory.GetDirectories(text, "*", SearchOption.AllDirectories) orderby d.Length descending select d).Concat(new string[1] { text })) { if (Directory.GetFileSystemEntries(item2).Length == 0) { Directory.Delete(item2); } } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not remove the copies of " + kept.Path + ": " + ex.Message)); } } } } internal sealed class Pin { public string File; public string Section; public string Key; public string Value; public string Profile; public string Id => PinFile.IdOf(File, Section, Key); } internal static class PinFile { public const string Version = "# keepsake pins v1"; private const string VersionPrefix = "# keepsake pins v"; private static readonly string[] Header = new string[4] { "# keepsake pins v1", "# Settings Keepsake keeps at your own value. Tab separated: the cfg file in", "# BepInEx/config, the section, the setting, your value, then the profile's value.", "# The profile's value may be left off; it is filled in on the next launch." }; public static ManualLogSource Log; private static readonly char[] Unstorable = new char[3] { '\t', '\r', '\n' }; public const string TempSuffix = ".keepsake.tmp"; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.pins"); public static string IdOf(string file, string section, string key) { return file.ToLowerInvariant() + "\t" + section + "\t" + key; } public static bool Storable(string value) { if (value != null && value.IndexOfAny(Unstorable) < 0) { return value == value.Trim(); } return false; } public static List<Pin> Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new List<Pin>(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } List<Pin> list = Parse(lines); if (list == null) { ManualLogSource log2 = Log; if (log2 == null) { return list; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return list; } public static List<Pin> Parse(IEnumerable<string> lines) { List<Pin> list = new List<Pin>(); HashSet<string> hashSet = new HashSet<string>(); foreach (string line in lines) { if (line.StartsWith("# keepsake pins v", StringComparison.Ordinal) && line.Trim() != "# keepsake pins v1") { return null; } if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string[] array = line.Split(new char[1] { '\t' }); if (array.Length < 4) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Keepsake: skipped a line in keepsake.pins that has fewer than four parts: " + line)); } continue; } string text = KeptFiles.Normalise(array[0]); if (text == null) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: skipped a line in keepsake.pins whose file is not inside BepInEx/config: " + line)); } continue; } Pin pin = new Pin { File = text, Section = array[1].Trim(), Key = array[2].Trim(), Value = array[3].Trim(), Profile = ((array.Length > 4) ? array[4].Trim() : null) }; if (!hashSet.Add(pin.Id)) { list.RemoveAll((Pin p) => p.Id == pin.Id); } list.Add(pin); } return list; } public static bool Write(IEnumerable<Pin> pins) { try { ReplaceText(FilePath, string.Join(Environment.NewLine, Format(pins)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } public static string[] Format(IEnumerable<Pin> pins) { List<string> list = new List<string>(Header); list.AddRange(from p in pins.OrderBy<Pin, string>((Pin p) => p.File, StringComparer.OrdinalIgnoreCase).ThenBy<Pin, string>((Pin p) => p.Section, StringComparer.Ordinal).ThenBy<Pin, string>((Pin p) => p.Key, StringComparer.Ordinal) select string.Join("\t", (p.Profile != null) ? new string[5] { p.File, p.Section, p.Key, p.Value, p.Profile } : new string[4] { p.File, p.Section, p.Key, p.Value })); return list.ToArray(); } public static void ReplaceText(string path, string text, bool byteOrderMark = false) { string text2 = path + ".keepsake.tmp"; File.WriteAllText(text2, text, new UTF8Encoding(byteOrderMark)); if (File.Exists(path)) { File.Replace(text2, path, null); } else { File.Move(text2, path); } } public static string Stamp() { try { FileInfo fileInfo = new FileInfo(FilePath); return fileInfo.Exists ? (fileInfo.LastWriteTimeUtc.Ticks + ":" + fileInfo.Length) : "missing"; } catch (Exception) { return null; } } public static string Relative(string cfgPath) { try { string text = Path.GetFullPath(Paths.ConfigPath).TrimEnd('\\', '/'); char directorySeparatorChar = Path.DirectorySeparatorChar; string text2 = text + directorySeparatorChar; string fullPath = Path.GetFullPath(cfgPath); if (!fullPath.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { return null; } return NormaliseFile(fullPath.Substring(text2.Length)); } catch (Exception) { return null; } } public static string Absolute(string file) { return Path.Combine(Paths.ConfigPath, file.Replace('/', Path.DirectorySeparatorChar)); } private static string NormaliseFile(string file) { return file.Trim().Replace('\\', '/'); } } internal sealed class ProfileChange { public string File; public string Section; public string Key; public string From; public string To; public string Id => PinFile.IdOf(File, Section, Key); public static ProfileChange Of(Pin pin, string from, string to) { return new ProfileChange { File = pin.File, Section = pin.Section, Key = pin.Key, From = from, To = to }; } } internal sealed class QuietSetting { public string File; public string Section; public string Key; public string Id => PinFile.IdOf(File, Section, Key); public static QuietSetting Of(Pin pin) { return new QuietSetting { File = pin.File, Section = pin.Section, Key = pin.Key }; } } internal sealed class ChangeState { public readonly List<ProfileChange> Waiting = new List<ProfileChange>(); public readonly List<QuietSetting> Quiet = new List<QuietSetting>(); public bool IsQuiet(string id) { return Quiet.Any((QuietSetting q) => q.Id == id); } } internal static class ProfileChanges { public const string Version = "# keepsake changes v1"; private const string VersionPrefix = "# keepsake changes v"; private static readonly string[] Header = new string[5] { "# keepsake changes v1", "# Profile changes to kept settings. [changes] waits for an answer in the panel: the cfg", "# file in BepInEx/config, the section, the setting, the profile's value before, then its", "# value now. [quiet] lists settings whose profile changes are recorded without asking:", "# the cfg file, the section and the setting. Tab separated." }; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.changes"); public static ChangeState Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new ChangeState(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } ChangeState changeState = Parse(lines); if (changeState == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return changeState; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return changeState; } public static ChangeState Parse(IEnumerable<string> lines) { ChangeState changeState = new ChangeState(); string text = null; foreach (string line in lines) { if (line.StartsWith("# keepsake changes v", StringComparison.Ordinal) && line.Trim() != "# keepsake changes v1") { return null; } if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text2 = line.Trim(); if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2; continue; } string[] array = (from p in line.Split(new char[1] { '\t' }) select p.Trim()).ToArray(); string file = array[0].Replace('\\', '/'); if (text == "[changes]" && array.Length >= 5) { ProfileChange change = new ProfileChange { File = file, Section = array[1], Key = array[2], From = array[3], To = array[4] }; changeState.Waiting.RemoveAll((ProfileChange c) => c.Id == change.Id); changeState.Waiting.Add(change); } else if (text == "[quiet]" && array.Length >= 3) { QuietSetting quietSetting = new QuietSetting { File = file, Section = array[1], Key = array[2] }; if (!changeState.IsQuiet(quietSetting.Id)) { changeState.Quiet.Add(quietSetting); } } } return changeState; } public static string[] Format(ChangeState state) { List<string> list = new List<string>(Header); list.Add(""); list.Add("[changes]"); list.AddRange(from c in state.Waiting.OrderBy<ProfileChange, string>((ProfileChange c) => c.File, StringComparer.OrdinalIgnoreCase).ThenBy<ProfileChange, string>((ProfileChange c) => c.Section, StringComparer.Ordinal).ThenBy<ProfileChange, string>((ProfileChange c) => c.Key, StringComparer.Ordinal) select string.Join("\t", c.File, c.Section, c.Key, c.From, c.To)); list.Add(""); list.Add("[quiet]"); list.AddRange(from q in state.Quiet.OrderBy<QuietSetting, string>((QuietSetting q) => q.File, StringComparer.OrdinalIgnoreCase).ThenBy<QuietSetting, string>((QuietSetting q) => q.Section, StringComparer.Ordinal).ThenBy<QuietSetting, string>((QuietSetting q) => q.Key, StringComparer.Ordinal) select string.Join("\t", q.File, q.Section, q.Key)); return list.ToArray(); } public static bool Write(ChangeState state) { try { if (state.Waiting.Count == 0 && state.Quiet.Count == 0) { if (File.Exists(FilePath)) { File.Delete(FilePath); } return true; } PinFile.ReplaceText(FilePath, string.Join(Environment.NewLine, Format(state)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } public static bool Merge(ChangeState state, IEnumerable<ProfileChange> found) { bool result = false; foreach (ProfileChange change in found) { if (state.IsQuiet(change.Id)) { continue; } ProfileChange profileChange = state.Waiting.FirstOrDefault((ProfileChange c) => c.Id == change.Id); if (profileChange == null) { state.Waiting.Add(change); result = true; continue; } if (change.To == profileChange.From) { state.Waiting.Remove(profileChange); } else { profileChange.To = change.To; } result = true; } return result; } public static bool KeepOnly(ChangeState state, ICollection<string> keptIds) { return state.Waiting.RemoveAll((ProfileChange c) => !keptIds.Contains(c.Id)) + state.Quiet.RemoveAll((QuietSetting q) => !keptIds.Contains(q.Id)) > 0; } } internal sealed class WaitingFile { public string Path; public bool PutBack; } internal sealed class SessionState { public DateTime? Started; public DateTime? Closed; public string ClosedBy; public readonly List<WaitingFile> Waiting = new List<WaitingFile>(); public WaitingFile WaitingFor(string path) { return Waiting.FirstOrDefault((WaitingFile w) => string.Equals(w.Path, path, StringComparison.OrdinalIgnoreCase)); } } internal static class SessionFile { public const string Version = "# keepsake session v1"; private const string VersionPrefix = "# keepsake session v"; private static readonly string[] Header = new string[4] { "# keepsake session v1", "# When the game last started and closed with Keepsake, in UTC, and under [files] the kept", "# files in BepInEx/config waiting for an answer in the panel: the path, then ask, or put back", "# when your copy goes back in at the next launch. Tab separated." }; public static string FilePath => Path.Combine(Paths.BepInExRootPath, "keepsake.session"); public static SessionState Read() { string[] lines; try { if (!File.Exists(FilePath)) { return new SessionState(); } lines = File.ReadAllLines(FilePath); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return null; } SessionState sessionState = Parse(lines); if (sessionState == null) { ManualLogSource log2 = PinFile.Log; if (log2 == null) { return sessionState; } log2.LogWarning((object)("Keepsake: " + Path.GetFileName(FilePath) + " was written by a newer Keepsake, so it is left as it is.")); } return sessionState; } public static SessionState Parse(IEnumerable<string> lines) { SessionState sessionState = new SessionState(); string text = null; foreach (string line in lines) { if (line.StartsWith("# keepsake session v", StringComparison.Ordinal) && line.Trim() != "# keepsake session v1") { return null; } if (line.Trim().Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) { continue; } string text2 = line.Trim(); if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2; continue; } string[] array = (from p in line.Split(new char[1] { '\t' }) select p.Trim()).ToArray(); if (text == null) { if (array.Length >= 2) { if (array[0] == "started") { sessionState.Started = TimeOf(array[1]); } else if (array[0] == "closed") { sessionState.Closed = TimeOf(array[1]); sessionState.ClosedBy = ((array.Length > 2) ? array[2] : null); } } } else if (text == "[files]") { string text3 = KeptFiles.Normalise(array[0]); if (text3 != null && sessionState.WaitingFor(text3) == null) { sessionState.Waiting.Add(new WaitingFile { Path = text3, PutBack = (array.Length > 1 && array[1] == "put back") }); } } } return sessionState; } public static string[] Format(SessionState state) { List<string> list = new List<string>(Header); if (state.Started.HasValue) { list.Add("started\t" + TextOf(state.Started.Value)); } if (state.Closed.HasValue) { list.Add("closed\t" + TextOf(state.Closed.Value) + ((state.ClosedBy != null) ? ("\t" + state.ClosedBy) : "")); } list.Add(""); list.Add("[files]"); list.AddRange(from w in state.Waiting.OrderBy<WaitingFile, string>((WaitingFile w) => w.Path, StringComparer.OrdinalIgnoreCase) select w.Path + "\t" + (w.PutBack ? "put back" : "ask")); return list.ToArray(); } public static bool Write(SessionState state) { try { PinFile.ReplaceText(FilePath, string.Join(Environment.NewLine, Format(state)) + Environment.NewLine); return true; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(FilePath) + ": " + ex.Message)); } return false; } } private static string TextOf(DateTime utc) { return utc.ToString("o", CultureInfo.InvariantCulture); } private static DateTime? TimeOf(string text) { if (!DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return null; } return result; } public static DateTime? LogEnd() { DateTime? dateTime = null; try { string[] files = Directory.GetFiles(Paths.BepInExRootPath, "LogOutput.log*"); foreach (string path in files) { string fileName = Path.GetFileName(path); if (fileName != "LogOutput.log" && !fileName.StartsWith("LogOutput.log.", StringComparison.Ordinal)) { continue; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path); if (dateTime.HasValue) { DateTime value = lastWriteTimeUtc; DateTime? dateTime2 = dateTime; if (!(value > dateTime2)) { continue; } } dateTime = lastWriteTimeUtc; } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read when the last game ended: " + ex.Message)); } } return dateTime; } } internal static class SpareCopy { private const string RestoredKey = "Keepsake.SpareCopyRestored"; private static readonly string[] Lists = new string[3] { "keepsake.pins", "keepsake.changes", "keepsake.files" }; private static readonly string[] KeptLists = new string[2] { "keepsake.pins", "keepsake.files" }; private static readonly string[] Folders = new string[2] { "keepsake-files", "keepsake-trash" }; private static readonly string[] Files = new string[5] { "keepsake.spare", "keepsake.session", "keepsake.changes", "keepsake.files", "keepsake.pins" }; public static bool RestoredThisLaunch { get { object data = AppDomain.CurrentDomain.GetData("Keepsake.SpareCopyRestored"); bool flag = default(bool); int num; if (data is bool) { flag = (bool)data; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } set { AppDomain.CurrentDomain.SetData("Keepsake.SpareCopyRestored", value); } } public static string AnswerPath => Path.Combine(Paths.BepInExRootPath, "keepsake.spare"); public static bool? Wanted { get { try { if (!File.Exists(AnswerPath)) { return null; } return SpareFolder.WantedIn(File.ReadAllLines(AnswerPath)); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + Path.GetFileName(AnswerPath) + ": " + ex.Message)); } return null; } } } public static bool Offered => Folder != null; public static bool Asks { get { if (Offered && KeepsSomething()) { return !Wanted.HasValue; } return false; } } public static string Folder => SpareFolder.For(Paths.BepInExRootPath); private static string Marker => Path.Combine(Paths.BepInExRootPath, "keepsake.restoring"); private static bool KeepsSomething() { return KeptLists.Any(delegate(string name) { try { string path = Path.Combine(Paths.BepInExRootPath, name); return File.Exists(path) && File.ReadLines(path).Any((string l) => l.Trim().Length > 0 && !l.StartsWith("#", StringComparison.Ordinal)); } catch (Exception) { return false; } }); } public static string Answer(bool keep) { try { PinFile.ReplaceText(AnswerPath, string.Join(Environment.NewLine, "# keepsake spare v1", "# Whether Keepsake keeps a spare copy of what it keeps in this profile outside the", "# profile, for Update existing profile in Thunderstore Mod Manager and r2modman.", "copy\t" + (keep ? "yes" : "no")) + Environment.NewLine); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)("Keepsake: could not save " + Path.GetFileName(AnswerPath) + ": " + ex.Message)); } return "your answer could not be saved, see the log"; } string folder = Folder; if (folder == null) { return null; } try { if (keep) { Update(out var failed); return (failed == 0) ? null : "the spare copy could not be made whole yet, see the log; the next save tries again"; } SpareFolder.Remove(folder); return null; } catch (Exception ex2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not " + (keep ? "make" : "remove") + " the spare copy in " + folder + ": " + ex2.Message)); } return keep ? "the spare copy could not be made, see the log" : "the spare copy could not be removed, see the log"; } } private static bool HasLists(string root) { return Lists.Any((string n) => File.Exists(Path.Combine(root, n))); } public static int Restore() { string folder = Folder; if (folder == null || !Directory.Exists(folder) || !HasLists(folder)) { return 0; } string bepInExRootPath = Paths.BepInExRootPath; bool flag = File.Exists(Marker); if (HasLists(bepInExRootPath) && !flag) { return 0; } ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)(flag ? ("Keepsake: bringing back the spare copy in " + folder + " was cut short at the last launch, so it is done again.") : ("Keepsake: this profile holds nothing Keepsake kept, and a spare copy of it is in " + folder + ". The profile folder was replaced, as Update existing profile does, so the spare copy comes back."))); } File.WriteAllText(Marker, folder); int failed; int result = Mirror(folder, bepInExRootPath, "bringing back", out failed); if (failed == 0) { File.Delete(Marker); } return result; } public static int Update() { int failed; return Update(out failed); } private static int Update(out int failed, bool listsOnly = false) { failed = 0; string folder = Folder; if (folder == null || Wanted != true) { return 0; } string bepInExRootPath = Paths.BepInExRootPath; if (!HasLists(bepInExRootPath) || File.Exists(Marker)) { return 0; } return Mirror(bepInExRootPath, folder, "saving", out failed, listsOnly); } public static void Follow(bool listsOnly = false) { try { Update(out var _, listsOnly); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: saving the spare copy failed: " + ex.Message)); } } } private static int Mirror(string from, string to, string doing, out int failed, bool listsOnly = false) { int num = 0; failed = 0; List<string> list = new List<string>(); string[] array = (listsOnly ? Array.Empty<string>() : Folders); string[] array2 = array; foreach (string folder in array2) { list.AddRange(FilesIn(from, folder)); list.AddRange(FilesIn(to, folder)); } list.AddRange(Files); foreach (string item in list.Distinct<string>(StringComparer.OrdinalIgnoreCase)) { string text = Path.Combine(from, item); string text2 = Path.Combine(to, item); try { if (File.Exists(text)) { if (KeptFiles.Differ(text, text2)) { KeptFiles.CopyOver(text, text2); num++; } } else if (File.Exists(text2)) { File.Delete(text2); num++; } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: " + doing + " the spare copy of " + item + " failed: " + ex.Message)); } failed++; } } array2 = array; foreach (string path in array2) { RemoveEmptyFolders(Path.Combine(to, path)); } return num; } private static IEnumerable<string> FilesIn(string root, string folder) { string path = Path.Combine(root, folder); if (!Directory.Exists(path)) { return Enumerable.Empty<string>(); } return (from f in Directory.GetFiles(path, "*", SearchOption.AllDirectories) where !f.EndsWith(".keepsake.tmp", StringComparison.OrdinalIgnoreCase) select f.Substring(root.Length).TrimStart('\\', '/')).ToList(); } private static void RemoveEmptyFolders(string folder) { try { if (!Directory.Exists(folder)) { return; } foreach (string item in from d in Directory.GetDirectories(folder, "*", SearchOption.AllDirectories) orderby d.Length descending select d) { if (Directory.GetFileSystemEntries(item).Length == 0) { Directory.Delete(item); } } } catch (Exception) { } } } internal static class SpareFolder { public const string AnswerVersion = "# keepsake spare v1"; public static string For(string bepInExRoot) { try { DirectoryInfo parent = Directory.GetParent(bepInExRoot); if (parent == null || !File.Exists(Path.Combine(parent.FullName, "mods.yml"))) { return null; } DirectoryInfo parent2 = parent.Parent; if (parent2 == null || !string.Equals(parent2.Name, "profiles", StringComparison.OrdinalIgnoreCase)) { return null; } DirectoryInfo parent3 = parent2.Parent; return (parent3 == null) ? null : Path.Combine(Path.Combine(parent3.FullName, "Keepsake"), parent.Name); } catch (Exception) { return null; } } public static bool? WantedIn(IEnumerable<string> lines) { List<string> list = lines.ToList(); if (list.Count == 0 || list[0].Trim() != "# keepsake spare v1") { return null; } foreach (string[] item in from l in list.Skip(1) where !l.StartsWith("#", StringComparison.Ordinal) select l.Split(new char[1] { '\t' })) { if (item.Length >= 2 && item[0].Trim() == "copy") { return (item[1].Trim() == "yes") ? new bool?(true) : ((item[1].Trim() == "no") ? new bool?(false) : ((bool?)null)); } } return null; } public static void Remove(string spare) { if (Directory.Exists(spare)) { Directory.Delete(spare, recursive: true); } string directoryName = Path.GetDirectoryName(spare); if (directoryName != null && Directory.Exists(directoryName) && Directory.GetFileSystemEntries(directoryName).Length == 0) { Directory.Delete(directoryName); } } } internal enum TrashReason { Copy, Replaced, Released } internal sealed class TrashEntry { public string Path; public string File; public DateTime At; public int Count; public TrashReason Reason; } internal static class Trash { public const int Versions = 5; internal static long MaxFileBytes = 8388608L; internal static long MaxTotalBytes = 104857600L; private const string StampFormat = "yyyyMMdd-HHmmss-fff"; private static readonly Regex VersionName = new Regex("^(?<name>.+)\\.(?<at>\\d{8}-\\d{6}-\\d{3})(?:-(?<count>\\d+))?\\.(?<reason>copy|replaced|released|launch)\\.kept$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static string Root => Path.Combine(Paths.BepInExRootPath, "keepsake-trash"); private static string FilesRoot => Path.Combine(Root, "files"); private static string ListsRoot => Path.Combine(Root, "lists"); private static string ReasonText(TrashReason reason) { return reason.ToString().ToLowerInvariant(); } public static bool Put(string path, string file, TrashReason reason) { try { if (!File.Exists(file)) { return true; } string folder = Path.Combine(FilesRoot, Path.GetDirectoryName(path.Replace('/', Path.DirectorySeparatorChar)) ?? ""); string fileName = Path.GetFileName(path); TrashEntry trashEntry = In(folder, fileName).FirstOrDefault(); if (trashEntry != null && SameBytes(trashEntry.File, file)) { return true; } long length = new FileInfo(file).Length; if (length > MaxFileBytes) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)($"Keepsake: {path} is {length / 1048576} MB, larger than the {MaxFileBytes / 1048576} MB " + "an earlier version may take, so the version it replaces is not set aside.")); } return true; } KeepUnderTotal(SetAside(file, folder, fileName, ReasonText(reason))); return true; } catch (Exception ex) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogWarning((object)("Keepsake: could not set aside the version of " + path + " it was about to replace, so it is left as it is: " + ex.Message)); } return false; } } public static void KeepLists(IEnumerable<string> lists) { foreach (string list in lists) { try { if (File.Exists(list)) { string fileName = Path.GetFileName(list); TrashEntry trashEntry = In(ListsRoot, fileName).FirstOrDefault(); if (trashEntry == null || !SameBytes(trashEntry.File, list)) { SetAside(list, ListsRoot, fileName, "launch"); } } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not keep a copy of " + Path.GetFileName(list) + ": " + ex.Message)); } } } } public static List<TrashEntry> Of(string path) { try { List<TrashEntry> list = In(Path.Combine(FilesRoot, Path.GetDirectoryName(path.Replace('/', Path.DirectorySeparatorChar)) ?? ""), Path.GetFileName(path)); foreach (TrashEntry item in list) { item.Path = path; } return list; } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not look through the earlier versions of " + path + ": " + ex.Message)); } return new List<TrashEntry>(); } } private static void KeepUnderTotal(string added) { if (!Directory.Exists(FilesRoot)) { return; } string name; List<(TrashEntry Entry, long Length)> source = (from file in Directory.GetFiles(FilesRoot, "*.kept", SearchOption.AllDirectories) select (Entry: EntryOf(file, out name), Length: new FileInfo(file).Length) into v where v.Entry != null select v).ToList(); long num = source.Sum(((TrashEntry Entry, long Length) v) => v.Length); foreach (var (trashEntry, num2) in from v in source orderby v.Entry.At, v.Entry.Count select v) { if (num <= MaxTotalBytes) { break; } if (!string.Equals(trashEntry.File, added, StringComparison.OrdinalIgnoreCase)) { File.Delete(trashEntry.File); num -= num2; } } } private static string SetAside(string file, string folder, string name, string reason) { Directory.CreateDirectory(folder); string text = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture); string text2 = Path.Combine(folder, name + "." + text + "." + reason + ".kept"); int num = 1; while (File.Exists(text2)) { text2 = Path.Combine(folder, $"{name}.{text}-{num}.{reason}.kept"); num++; } string text3 = text2 + ".keepsake.tmp"; File.Copy(file, text3, overwrite: true); File.SetLastWriteTimeUtc(text3, File.GetLastWriteTimeUtc(file)); File.Move(text3, text2); foreach (TrashEntry item in In(folder, name).Skip(5)) { File.Delete(item.File); } string[] files = Directory.GetFiles(folder, name + ".*.keepsake.tmp"); foreach (string text4 in files) { if (!string.Equals(text4, text3, StringComparison.OrdinalIgnoreCase)) { File.Delete(text4); } } return text2; } private static List<TrashEntry> In(string folder, string name) { if (!Directory.Exists(folder)) { return new List<TrashEntry>(); } List<TrashEntry> list = new List<TrashEntry>(); string[] files = Directory.GetFiles(folder, name + ".*.kept"); for (int i = 0; i < files.Length; i++) { string name2; TrashEntry trashEntry = EntryOf(files[i], out name2); if (trashEntry != null && string.Equals(name2, name, StringComparison.OrdinalIgnoreCase)) { list.Add(trashEntry); } } return (from e in list orderby e.At descending, e.Count descending select e).ToList(); } private static TrashEntry EntryOf(string file, out string name) { name = null; Match match = VersionName.Match(Path.GetFileName(file)); if (!match.Success) { return null; } if (!DateTime.TryParseExact(match.Groups["at"].Value, "yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return null; } name = match.Groups["name"].Value; return new TrashEntry { File = file, At = result, Count = (match.Groups["count"].Success ? int.Parse(match.Groups["count"].Value, CultureInfo.InvariantCulture) : 0), Reason = ReasonOf(match.Groups["reason"].Value) }; } private static TrashReason ReasonOf(string text) { if (!string.Equals(text, "replaced", StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(text, "released", StringComparison.OrdinalIgnoreCase)) { return TrashReason.Copy; } return TrashReason.Released; } return TrashReason.Replaced; } private static bool SameBytes(string a, string b) { FileInfo fileInfo = new FileInfo(a); FileInfo fileInfo2 = new FileInfo(b); if (fileInfo.Length == fileInfo2.Length) { return File.ReadAllBytes(a).SequenceEqual(File.ReadAllBytes(b)); } return false; } } internal sealed class LaunchResult { public bool FromSpareCopy; } internal static class Launcher { public static LaunchResult Run(DateTime now, DateTime? logEnd, Func<bool> bindruneInstalled) { LaunchResult launchResult = new LaunchResult(); try { launchResult.FromSpareCopy = SpareCopy.Restore() > 0; } catch (Exception arg) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogError((object)$"Keepsake: bringing back the spare copy failed: {arg}"); } } SpareCopy.RestoredThisLaunch = launchResult.FromSpareCopy; Trash.KeepLists(new string[3] { PinFile.FilePath, ProfileChanges.FilePath, KeptFiles.ListPath }); try { Restore(now, logEnd, bindruneInstalled); } catch (Exception arg2) { ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogError((object)$"Keepsake: putting your values back failed: {arg2}"); } } try { SpareCopy.Update(); } catch (Exception ex) { ManualLogSource log3 = PinFile.Log; if (log3 != null) { log3.LogWarning((object)("Keepsake: saving the spare copy failed: " + ex.Message)); } } return launchResult; } private static void Restore(DateTime now, DateTime? logEnd, Func<bool> bindruneInstalled) { Stopwatch stopwatch = Stopwatch.StartNew(); List<Pin> list = PinFile.Read(); List<KeptPath> list2 = KeptFiles.Read(); List<string> list3 = new List<string> { PinFile.FilePath, ProfileChanges.FilePath, KeptFiles.ListPath, SessionFile.FilePath }; if (list != null) { list3.AddRange(list.Select((Pin p) => PinFile.Absolute(p.File))); } if (list2 != null) { list3.AddRange(Restorer.KeptFilePaths(list2)); } int num = Restorer.RemoveLeftovers(list3); if (num > 0) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogInfo((object)$"Keepsake: removed {num} file(s) left half written by a game that stopped mid-save."); } } if (list2 != null) { Restorer.SettleFiles(list2, now, logEnd); } if (list == null) { return; } if (list.Count == 0) { Restorer.RecordChanges(list, new List<ProfileChange>()); return; } RestoreResult restoreResult = Restorer.Apply(list, bindruneInstalled); if (restoreResult.Learned) { PinFile.Write(list); } int num2 = Restorer.RecordChanges(list, restoreResult.Changes); ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogInfo((object)($"Keepsake: {list.Count} kept setting(s), {restoreResult.Restored} put back before the mods loaded" + ((restoreResult.Changes.Count > 0) ? $", {restoreResult.Changes.Count} of them changed by the profile since the last launch" : "") + ((num2 > 0) ? $", {num2} profile change(s) waiting for an answer in the panel" : "") + ((restoreResult.Missing > 0) ? $", {restoreResult.Missing} not in their cfg file yet" : "") + ((restoreResult.ToBindrune > 0) ? $", {restoreResult.ToBindrune} keybind(s) left for Bindrune to take over" : "") + ", in " + stopwatch.Elapsed.TotalMilliseconds.ToString("0.0", CultureInfo.InvariantCulture) + " ms.")); } } } public static class Preloader { public static IEnumerable<string> TargetDLLs { get; } = Array.Empty<string>(); public static void Patch(AssemblyDefinition assembly) { } public static void Initialize() { ManualLogSource val = (PinFile.Log = Logger.CreateLogSource("Keepsake")); try { Launcher.Run(DateTime.UtcNow, SessionFile.LogEnd(), BindruneLink.InstalledOnDisk); } catch (Exception arg) { val.LogError((object)$"Keepsake: the launch failed: {arg}"); } } } internal sealed class RestoreResult { public int Restored; public int Missing; public int ToBindrune; public bool Learned; public readonly List<ProfileChange> Changes = new List<ProfileChange>(); } internal static class Restorer { public static RestoreResult Apply(List<Pin> pins, Func<bool> bindruneInstalled) { RestoreResult restoreResult = new RestoreResult(); bool? flag = null; foreach (IGrouping<string, Pin> item in pins.GroupBy<Pin, string>((Pin p) => p.File, StringComparer.OrdinalIgnoreCase)) { string path = PinFile.Absolute(item.Key); if (!File.Exists(path)) { restoreResult.Missing += item.Count(); continue; } CfgText cfgText; try { cfgText = CfgText.Load(path); } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not read " + item.Key + ": " + ex.Message)); } restoreResult.Missing += item.Count(); continue; } foreach (Pin item2 in item) { if (!cfgText.TryGet(item2.Section, item2.Key, out var value)) { restoreResult.Missing++; continue; } if (BindruneLink.IsKeybindType(cfgText.TypeOf(item2.Section, item2.Key))) { bool valueOrDefault = flag == true; bool num; if (!flag.HasValue) { valueOrDefault = bindruneInstalled(); flag = valueOrDefault; num = valueOrDefault; } else { num = valueOrDefault; } if (num) { restoreResult.ToBindrune++; continue; } } if (value == item2.Value) { if (item2.Profile == null) { item2.Profile = value; restoreResult.Learned = true; } continue; } string profile = item2.Profile; bool flag2 = profile != null && profile != value; if (flag2) { restoreResult.Changes.Add(ProfileChange.Of(item2, profile, value)); } if (profile != value) { restoreResult.Learned = true; } item2.Profile = value; cfgText.Set(item2.Section, item2.Key, item2.Value); restoreResult.Restored++; ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogInfo((object)("Keepsake: kept your value for " + item2.File + " [" + item2.Section + "] " + item2.Key + ": " + item2.Value + " " + (flag2 ? ("(the profile changed it from " + profile + " to " + value + ").") : ("(the profile has " + value + ").")))); } } if (!cfgText.Changed) { continue; } try { cfgText.Save(path); } catch (Exception ex2) { ManualLogSource log3 = PinFile.Log; if (log3 != null) { log3.LogWarning((object)("Keepsake: could not write " + item.Key + ": " + ex2.Message)); } } } return restoreResult; } public static int RecordChanges(List<Pin> pins, List<ProfileChange> found) { ChangeState changeState = ProfileChanges.Read(); if (changeState == null) { return 0; } if (ProfileChanges.Merge(changeState, found) | ProfileChanges.KeepOnly(changeState, new HashSet<string>(pins.Select((Pin p) => p.Id)))) { ProfileChanges.Write(changeState); } return changeState.Waiting.Count; } public static SettleResult SettleFiles(List<KeptPath> kept, DateTime now, DateTime? logEnd) { if (kept.Count == 0) { try { if (File.Exists(SessionFile.FilePath) && SessionFile.Read() != null) { File.Delete(SessionFile.FilePath); } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not remove " + Path.GetFileName(SessionFile.FilePath) + ": " + ex.Message)); } } return new SettleResult(); } SessionState sessionState = SessionFile.Read(); SettleResult settleResult = KeptFiles.Settle(kept, sessionState ?? new SessionState(), logEnd); if (sessionState != null) { sessionState.Started = now; SessionFile.Write(sessionState); } ManualLogSource log2 = PinFile.Log; if (log2 != null) { log2.LogInfo((object)($"Keepsake: {kept.Count} kept file(s) or folder(s), the last game " + (settleResult.Clean ? "closed with Keepsake" : "was not seen closing") + $", {settleResult.PutBack} file(s) put back, {settleResult.Updated} copy(s) brought up to date" + ((settleResult.Waiting > 0) ? $", {settleResult.Waiting} file(s) waiting for an answer in the panel" : "") + " before the mods loaded.")); } return settleResult; } public static IEnumerable<string> KeptFilePaths(IEnumerable<KeptPath> kept) { foreach (KeptPath item in kept) { List<string> list; try { list = KeptFiles.CopiedFiles(item); } catch (Exception) { continue; } foreach (string path in list) { yield return KeptFiles.Live(path); yield return KeptFiles.Copy(path); } } } public static int RemoveLeftovers(IEnumerable<string> written) { int num = 0; foreach (string item in written.Distinct<string>(StringComparer.OrdinalIgnoreCase)) { string text = item + ".keepsake.tmp"; try { if (File.Exists(text)) { File.Delete(text); num++; } } catch (Exception ex) { ManualLogSource log = PinFile.Log; if (log != null) { log.LogWarning((object)("Keepsake: could not remove the leftover " + text + ": " + ex.Message)); } } } return num; } } }