Decompiled source of Keepsake v0.4.2

plugins/Keepsake.dll

Decompiled 13 hours ago
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.Threading;
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.4.2.0")]
[assembly: AssemblyInformationalVersion("0.4.2+ff133d6146eee7b02584d82c2980f4853cab488f")]
[assembly: AssemblyProduct("Keepsake")]
[assembly: AssemblyTitle("Keepsake")]
[assembly: AssemblyVersion("0.4.2.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
{
	public sealed class BindruneKey
	{
		public string Id;

		public string Yours;

		public string Profile;

		public bool Active;
	}
	public 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("[") && text.EndsWith("]"))
				{
					flag2 = text == "[keys]";
				}
				else if (flag2 && text.Length != 0 && !text.StartsWith("#"))
				{
					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;
		}
	}
	public sealed class CfgText
	{
		private readonly string[] _lines;

		private readonly string _newline;

		private bool _changed;

		public string Text => string.Join(_newline, _lines);

		public bool Changed => _changed;

		private CfgText(string text)
		{
			_newline = (text.Contains("\r\n") ? "\r\n" : "\n");
			_lines = text.Replace("\r\n", "\n").Split(new char[1] { '\n' });
		}

		public static CfgText Load(string path)
		{
			return new CfgText(File.ReadAllText(path));
		}

		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("#"))
				{
					break;
				}
				if (text.StartsWith("# Setting type:"))
				{
					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);
		}

		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("#"))
				{
					continue;
				}
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					text = text2.Substring(1, text2.Length - 2);
				}
				else if (!(text != section))
				{
					string[] array = text2.Split(new char[1] { '=' }, 2);
					if (array.Length == 2 && !(array[0].Trim() != key))
					{
						result = i;
						value = array[1].Trim();
					}
				}
			}
			return result;
		}
	}
	public 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;
		}
	}
	public sealed class SettleResult
	{
		public bool Clean;

		public int PutBack;

		public int Updated;

		public int Waiting;
	}
	public 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 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(new char[1] { '/' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0 || array.Any((string p) => p == "." || p == ".." || p.Contains(":")))
			{
				return null;
			}
			if (path.Trim().StartsWith("/") || path.Trim().StartsWith("\\"))
			{
				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") && line.Trim() != "# keepsake files v1")
				{
					return null;
				}
				if (line.Trim().Length == 0 || line.StartsWith("#"))
				{
					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("/") || text.EndsWith("\\"))
					});
				}
			}
			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)))
						{
							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)
				{
					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_0169;
						}
					}
					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_0169;
			IL_0169:
			if (result.Clean)
			{
				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 void Forget(KeptPath kept)
		{
			try
			{
				if (!kept.IsFolder)
				{
					if (File.Exists(Copy(kept.Path)))
					{
						File.Delete(Copy(kept.Path));
					}
					return;
				}
				string path = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar));
				if (Directory.Exists(path))
				{
					Directory.Delete(path, recursive: true);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = PinFile.Log;
				if (log != null)
				{
					log.LogWarning((object)("Keepsake: could not remove the copies of " + kept.Path + ": " + ex.Message));
				}
			}
		}
	}
	public 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);
	}
	public 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;

		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(new char[3] { '\t', '\r', '\n' }) < 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") && line.Trim() != "# keepsake pins v1")
				{
					return null;
				}
				if (line.Length == 0 || line.StartsWith("#"))
				{
					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;
				}
				Pin pin = new Pin
				{
					File = NormaliseFile(array[0]),
					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)
		{
			string text2 = path + ".keepsake.tmp";
			File.WriteAllText(text2, text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			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('\\', '/');
		}
	}
	public 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
			};
		}
	}
	public 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
			};
		}
	}
	public 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);
		}
	}
	public 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") && line.Trim() != "# keepsake changes v1")
				{
					return null;
				}
				if (line.Length == 0 || line.StartsWith("#"))
				{
					continue;
				}
				string text2 = line.Trim();
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					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;
		}
	}
	public sealed class WaitingFile
	{
		public string Path;

		public bool PutBack;
	}
	public 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));
		}
	}
	public 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") && line.Trim() != "# keepsake session v1")
				{
					return null;
				}
				if (line.Trim().Length == 0 || line.StartsWith("#"))
				{
					continue;
				}
				string text2 = line.Trim();
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					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."))
					{
						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;
		}
	}
	public sealed class KeybindSlot<T>
	{
		public string BindruneId;

		public bool IsKeyCode;

		public T Setting;
	}
	public sealed class KeybindMatch<T>
	{
		public T Setting;

		public string Yours;

		public string Profile;
	}
	public 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;
			}
		}
	}
	public 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('/'));
			}
		}
	}
	public 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 });
			return null;
		}

		public static void Release(string path)
		{
			Load();
			if (_unreadable)
			{
				return;
			}
			KeptPath entry = Kept.FirstOrDefault((KeptPath k) => string.Equals(k.Path, path, StringComparison.OrdinalIgnoreCase));
			if (entry == null)
			{
				return;
			}
			_kept.Remove(entry);
			if (KeptFiles.Write(_kept))
			{
				KeptFiles.Forget(entry);
				SessionState session = Session;
				if (session != null && session.Waiting.RemoveAll((WaitingFile w) => entry.Covers(w.Path)) > 0)
				{
					SessionFile.Write(session);
				}
			}
		}

		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;
			if (!SessionFile.Write(Session))
			{
				return "keepsake.session could not be saved, see the log";
			}
			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";
			}
			KeptFiles.CopyOver(KeptFiles.Live(path), KeptFiles.Copy(path));
			Session.Waiting.Remove(waitingFile);
			if (!SessionFile.Write(Session))
			{
				return "keepsake.session could not be saved, see the log";
			}
			return null;
		}

		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;
			}
			int num = KeptFiles.Save(Kept, (string p) => session?.WaitingFor(p) != null);
			if (num > 0)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogDebug((object)$"Keepsake: saved a copy of {num} kept file(s).");
				}
			}
			if (session != null)
			{
				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;
		}
	}
	public 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;

		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();
			string text = Add(setting);
			if (text == null)
			{
				Save();
			}
			return text;
		}

		public static int PinAll(IEnumerable<Setting> settings)
		{
			Sync();
			int num = 0;
			foreach (Setting setting in settings)
			{
				if (Find(setting.Id) == null && Add(setting) == null)
				{
					num++;
				}
			}
			if (num > 0)
			{
				Save();
			}
			return num;
		}

		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 void Unpin(string id)
		{
			UnpinAll(new string[1] { id });
		}

		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;
			}
			Save();
			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;
			}
			Write(setting, text);
			string value = setting.Current ?? text;
			if (!PinFile.Storable(value))
			{
				return "this value has line breaks or tabs in it, which Keepsake cannot store";
			}
			pin.Value = value;
			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);
			}
		}

		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();
			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", 364);
					}
					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)
			{
				if (MainThread != 0 && Thread.CurrentThread.ManagedThreadId != 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", 513);
			}
		}

		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", 567);
			}
			finally
			{
				_writing = false;
			}
		}

		private static void 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", 580);
			}
			else
			{
				if (!PinFile.Write(_pins))
				{
					return;
				}
				_stamp = PinFile.Stamp();
				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();
			}
		}

		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;
			_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();
		}
	}
	public 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.4.2")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInProcess("valheim.exe")]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "isimp.Keepsake";

		public const string Version = "0.4.2";

		public 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;

		public static bool PlaySounds
		{
			get
			{
				if (_playSounds != null)
				{
					return _playSounds.Value;
				}
				return true;
			}
		}

		public 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;
				}
			}
		}

		public 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_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Expected O, but got Unknown
			//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_0109: Expected O, but got Unknown
			//IL_0154: Unknown result type (might be due to invalid IL or missing references)
			//IL_015e: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Keeper.MainThread = Thread.CurrentThread.ManagedThreadId;
			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", 108);
			}
		}

		private void OnApplicationQuit()
		{
			FlushQuietly("quit");
		}

		private static void OnProcessExit(object sender, EventArgs e)
		{
			try
			{
				FileKeeper.Close("exit");
			}
			catch (Exception)
			{
			}
		}

		private static void FlushQuietly(string by)
		{
			try
			{
				Keeper.Flush();
				FileKeeper.Close(by);
			}
			catch (Exception ex)
			{
				WarnOnce("Keepsake: saving your latest values failed: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/Plugin.cs", 142);
			}
		}

		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", 177);
			}
		}

		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 (!_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;
				}
			}
		}
	}
	public 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();
		}
	}
	public enum ServerControl
	{
		None,
		ServerSync,
		Jotunn
	}
	public sealed class ModInfo
	{
		public string Name;

		public string Version;

		public string File;

		public int Count;
	}
	public 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", 91);
					return null;
				}
			}
		}
	}
	public 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)
		{
			if (id == null || !ById.TryGetValue(id, out var value))
			{
				return null;
			}
			return value;
		}

		public static Setting Find(ConfigEntryBase entry)
		{
			if (entry == null || !ByEntry.TryGetValue(entry, out var value))
			{
				return null;
			}
			return value;
		}

		private static bool BindruneInLoader()
		{
			if (Chainloader.PluginInfos.TryGetValue("isimp.Bindrune", out var value))
			{
				return (Object)(object)((value != null) ? value.Instance : null) != (Object)null;
			}
			return false;
		}

		public static string FileOf(ConfigFile config)
		{
			if (config == null || !Files.TryGetValue(config, out var value))
			{
				return null;
			}
			return value;
		}

		private static IEnumerable<LoadedConfig> Loaded()
		{
			foreach (PluginInfo item in Chainloader.PluginInfos.Values.ToList())
			{
				object obj;
				if (item == null)
				{
					obj = null;
				}
				else
				{
					BaseUnityPlugin instance = item.Instance;
					obj = ((instance != null) ? instance.Config : null);
				}
				ConfigFile val = (ConfigFile)obj;
				if (val != null)
				{
					LoadedConfig obj2 = new LoadedConfig
					{
						Config = val
					};
					BepInPlugin metadata = item.Metadata;
					obj2.Name = ((metadata != null) ? metadata.Name : null);
					BepInPlugin metadata2 = item.Metadata;
					obj2.Version = ((metadata2 == null) ? null : metadata2.Version?.ToString());
					BepInPlugin metadata3 = item.Metadata;
					obj2.Guid = ((metadata3 != null) ? metadata3.GUID : null);
					yield return obj2;
				}
			}
		}

		public static void Refresh()
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			List<Setting> list = new List<Setting>();
			bool flag = false;
			ById.Clear();
			foreach (LoadedConfig item in Sources())
			{
				ConfigFile config = item.Config;
				if (!Files.TryGetValue(config, out var value))
				{
					value = PinFile.Relative(config.ConfigFilePath);
					Files[config] = value;
					if (value != null)
					{
						FileFound?.Invoke(config);
					}
				}
				if (value == null)
				{
					continue;
				}
				string text = item.Name ?? value;
				KeyValuePair<ConfigDefinition, ConfigEntryBase>[] array;
				try
				{
					array = ((IEnumerable<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)config).ToArray();
				}
				catch (Exception ex)
				{
					Plugin.WarnOnce("Keepsake: could not read the settings of " + text + ": " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/SettingIndex.cs", 199);
					continue;
				}
				KeyValuePair<ConfigDefinition, ConfigEntryBase>[] array2 = array;
				foreach (KeyValuePair<ConfigDefinition, ConfigEntryBase> keyValuePair in array2)
				{
					ConfigEntryBase value2 = keyValuePair.Value;
					if (value2 != null)
					{
						if (!ByEntry.TryGetValue(value2, out var value3))
						{
							value3 = new Setting
							{
								Entry = value2,
								File = value,
								ModName = text,
								ModVersion = (item.Version ?? ""),
								ModGuid = (item.Guid ?? ""),
								Id = PinFile.IdOf(value, value2.Definition.Section, value2.Definition.Key),
								Server = ServerOf(value2)
							};
							value3.SearchText = (text + " " + value + " " + value3.Section + " " + value3.Key + " " + value3.Description).ToLowerInvariant();
							ByEntry[value2] = value3;
							SettingFound?.Invoke(value3);
							flag = true;
						}
						if (!ById.ContainsKey(value3.Id))
						{
							ById[value3.Id] = value3;
							list.Add(value3);
						}
					}
				}
			}
			LastRefreshSorted = flag || list.Count != All.Count;
			if (LastRefreshSorted)
			{
				Arrange(list);
			}
			stopwatch.Stop();
			LastRefreshMs = stopwatch.Elapsed.TotalMilliseconds;
		}

		private static void Arrange(List<Setting> all)
		{
			All = all.OrderBy((Setting s) => s.ModName, NaturalOrder.IgnoreCase).ThenBy((Setting s) => s.Section, NaturalOrder.IgnoreCase).ThenBy((Setting s) => s.Key, NaturalOrder.IgnoreCase)
				.ToList();
			Mods = All.Select((Setting s) => s.ModName).Distinct().ToList();
			Dictionary<string, ModInfo> dictionary = new Dictionary<string, ModInfo>();
			foreach (Setting item in All)
			{
				if (!dictionary.TryGetValue(item.ModName, out var value))
				{
					value = new ModInfo
					{
						Name = item.ModName,
						Version = item.ModVersion,
						File = item.File
					};
					dictionary[item.ModName] = value;
				}
				value.Count++;
			}
			_mods = dictionary;
		}

		internal static void Reset()
		{
			Sources = Loaded;
			IsBindruneLoaded = BindruneInLoader;
			ById.Clear();
			ByEntry.Clear();
			Files.Clear();
			All = new List<Setting>();
			Mods = new List<string>();
			_mods = new Dictionary<string, ModInfo>();
		}

		public static ModInfo Mod(string name)
		{
			if (name == null || !_mods.TryGetValue(name, out var value))
			{
				return null;
			}
			return value;
		}

		private static ServerControl ServerOf(ConfigEntryBase entry)
		{
			ConfigDescription description = entry.Description;
			object[] array = ((description != null) ? description.Tags : null);
			if (array == null)
			{
				return ServerControl.None;
			}
			object[] array2 = array;
			foreach (object obj in array2)
			{
				if (obj == null)
				{
					continue;
				}
				Type type = obj.GetType();
				if (DerivesFrom(type, "OwnConfigEntryBase"))
				{
					if (ReadBool(obj, type, "SynchronizedConfig") == true)
					{
						return ServerControl.ServerSync;
					}
				}
				else if (type.Name == "ConfigurationManagerAttributes" && ReadBool(obj, type, "IsAdminOnly") == true)
				{
					return ServerControl.Jotunn;
				}
			}
			return ServerControl.None;
		}

		private static bool DerivesFrom(Type type, string name)
		{
			Type type2 = type;
			while (type2 != null)
			{
				if (type2.Name == name)
				{
					return true;
				}
				type2 = type2.BaseType;
			}
			return false;
		}

		private static bool? ReadBool(object target, Type type, string name)
		{
			try
			{
				Type type2 = type;
				while (type2 != null)
				{
					FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
					if (field != null && field.FieldType == typeof(bool))
					{
						return (bool)field.GetValue(target);
					}
					PropertyInfo property = type2.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
					if (property != null && property.PropertyType == typeof(bool))
					{
						return (bool)property.GetValue(target, null);
					}
					type2 = type2.BaseType;
				}
			}
			catch (Exception ex)
			{
				Plugin.WarnOnce("Keepsake: could not read " + name + " on " + type.FullName + ": " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/SettingIndex.cs", 336);
			}
			return null;
		}
	}
	internal static class StartMenuKeys
	{
		public static void Patch(Harmony harmony)
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			try
			{
				MethodInfo methodInfo = AccessTools.Method(typeof(FejdStartup), "UpdateKeyboard", (Type[])null, (Type[])null);
				if (methodInfo == null)
				{
					Plugin.WarnOnce("Keepsake: the start menu's keyboard handling was not found, so Return may reach the menu while you type in the panel.", null, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/StartMenuKeys.cs", 28);
				}
				else
				{
					harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(StartMenuKeys), "Skip", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				}
			}
			catch (Exception ex)
			{
				Plugin.WarnOnce("Keepsake: could not hold the start menu's keys back while you type: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/StartMenuKeys.cs", 37);
			}
		}

		private static bool Skip()
		{
			return !KeepsakePanel.HoldsKeyboard;
		}
	}
}
namespace Keepsake.UI
{
	public enum Source
	{
		Kept,
		ProfileChanged,
		Changed,
		Files,
		All,
		Mod
	}
	public static class KeepsakePanel
	{
		private enum FileFilter
		{
			NoImages,
			ImagesOnly,
			All
		}

		private sealed class Row
		{
			public string Id;

			public Setting Setting;

			public Pin Pin;

			public string ModName;

			public string Section;

			public string Key;

			public ConfigItem Item;
		}

		private sealed class SettingItem
		{
			public string Heading;

			public Row Row;
		}

		private sealed class SourceItem
		{
			public string Heading;

			public string Text;

			public Source Source;

			public string Mod;

			public int Count;

			public string CountText;

			public bool HasKept;
		}

		private sealed class SettingView : RowView
		{
			public Image Background;

			public Button Button;

			public Image Bar;

			public Text Key;

			public RectTransform KeyRect;

			public Text Value;

			public RectTransform ValueRect;

			public Text Tag;

			public SettingItem Item;
		}

		private sealed class SourceView : RowView
		{
			public Image Background;

			public Button Button;

			public Text Name;

			public Text Count;

			public SourceItem Item;
		}

		private sealed class HeaderButton
		{
			public string Text;

			public float Width;

			public Action Act;
		}

		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static UnityAction <>9__59_0;

			public static UnityAction <>9__59_1;

			public static UnityAction <>9__59_2;

			public static Action<Vector2> <>9__64_0;

			public static Func<string> <>9__81_1;

			public static Func<string> <>9__81_2;

			public static UnityAction <>9__81_0;

			public static Func<Setting, string> <>9__98_0;

			public static Func<ConfigItem, bool> <>9__102_0;

			public static Func<ConfigItem, bool> <>9__102_1;

			public static Func<ConfigItem, string> <>9__106_1;

			public static Func<ConfigItem, Row> <>9__106_3;

			public static Func<ConfigItem, bool> <>9__107_0;

			public static Func<Row, bool> <>9__107_1;

			public static UnityAction<int> <>9__108_0;

			public static Func<Row, Setting> <>9__170_0;

			public static Func<Row, bool> <>9__170_1;

			public static Func<Row, string> <>9__170_2;

			public static Func<Row, bool> <>9__170_3;

			public static Func<Row, string> <>9__170_4;

			public static Func<Row, string> <>9__171_0;

			public static Func<Pin, bool> <>9__171_1;

			public static Func<string, Row> <>9__174_2;

			public static Func<Setting, bool> <>9__174_5;

			public static Func<Pin, string> <>9__175_0;

			public static Func<string, bool> <>9__175_1;

			public static Func<Row, string> <>9__179_0;

			public static Func<Row, string> <>9__179_1;

			public static Func<Row, string> <>9__179_2;

			internal void <Build>b__59_0()
			{
				Close();
			}

			internal void <Build>b__59_1()
			{
				KeyCapture.Cancel();
				_selectedId = null;
				_showBindruneOffer = true;
				_note = null;
				_settingList?.Rebind();
				ShowDetail();
				ShowNote();
			}

			internal void <Build>b__59_2()
			{
				if ((Object)(object)_search != (Object)null)
				{
					_search.text = "";
				}
			}

			internal void <BuildResizeGrip>b__64_0(Vector2 size)
			{
				//IL_0000: Unknown result type (might be due to invalid IL or missing references)
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				_size = size;
				Plugin.PanelSize = size;
				Rebuild();
			}

			internal void <BindruneOffer>b__81_0()
			{
				Act(() => (Keeper.TakeOverFromBindrune() <= 0) ? "No key could be taken over." : null, "sfx_gui_moveitem", () => "The keys from Bindrune are kept here now.");
			}

			internal string <BindruneOffer>b__81_1()
			{
				if (Keeper.TakeOverFromBindrune() <= 0)
				{
					return "No key could be taken over.";
				}
				return null;
			}

			internal string <BindruneOffer>b__81_2()
			{
				return "The keys from Bindrune are kept here now.";
			}

			internal string <StartReadingFiles>b__98_0(Setting s)
			{
				return s.File;
			}

			internal bool <FilesSourceItem>b__102_0(ConfigItem i)
			{
				if (!i.IsFolder)
				{
					return FileKeeper.KeptBy(i.Path) != null;
				}
				return false;
			}

			internal bool <FilesSourceItem>b__102_1(ConfigItem i)
			{
				return !i.IsFolder;
			}

			internal string <FileRows>b__106_1(ConfigItem f)
			{
				return f.Parent;
			}

			internal Row <FileRows>b__106_3(ConfigItem i)
			{
				return new Row
				{
					Id = i.Id,
					Item = i
				};
			}

			internal bool <UpdateFilesHeader>b__107_0(ConfigItem i)
			{
				return !i.IsFolder;
			}

			internal bool <UpdateFilesHeader>b__107_1(Row r)
			{
				return !r.Item.IsFolder;
			}

			internal void <BuildFileFilter>b__108_0(int i)
			{
				_fileFilter = (FileFilter)Mathf.Clamp(i, 0, FileFilterNames.Length - 1);
				RequestPopulate(reset: true);
			}

			internal Setting <UpdateHeaderButtons>b__170_0(Row r)
			{
				return r.Setting;
			}

			internal bool <UpdateHeaderButtons>b__170_1(Row r)
			{
				if (r.Setting != null)
				{
					return Keeper.Find(r.Id) != null;
				}
				return false;
			}

			internal string <UpdateHeaderButtons>b__170_2(Row r)
			{
				return r.Id;
			}

			internal bool <UpdateHeaderButtons>b__170_3(Row r)
			{
				return r.Setting == null;
			}

			internal string <UpdateHeaderButtons>b__170_4(Row r)
			{
				return r.Id;
			}

			internal string <UpdateHeader>b__171_0(Row r)
			{
				return r.ModName;
			}

			internal bool <UpdateHeader>b__171_1(Pin p)
			{
				return SettingIndex.Find(p.Id)?.ModName == _mod;
			}

			internal Row <PopulateLists>b__174_2(string id)
			{
				return RowOf(Keeper.Find(id));
			}

			internal bool <PopulateLists>b__174_5(Setting s)
			{
				return s.ModName == _mod;
			}

			internal string <SourceItems>b__175_0(Pin p)
			{
				return SettingIndex.Find(p.Id)?.ModName;
			}

			internal bool <SourceItems>b__175_1(string m)
			{
				return m != null;
			}

			internal string <Sorted>b__179_0(Row r)
			{
				return r.ModName;
			}

			internal string <Sorted>b__179_1(Row r)
			{
				return r.Section;
			}

			internal string <Sorted>b__179_2(Row r)
			{
				return r.Key;
			}
		}

		private const float Margin = 26f;

		private const float Gap = 12f;

		private const float TopChrome = 110f;

		private const float FooterHeight = 44f;

		private const float LeftWidth = 270f;

		private const float DetailWidth = 420f;

		private static Vector2 _size;

		private static GameObject _root;

		private static RectTransform _detail;

		private static InputField _search;

		private static Text _summary;

		private static Text _footer;

		private static float _repopulateAt;

		private static bool _pendingReset;

		private static bool _rebindNextFrame;

		private static Source _source = Source.Kept;

		private static string _mod;

		private static Source _sourceBeforeSearch = Source.Kept;

		private static string _modBeforeSearch;

		private static string _selectedId;

		private static string _query = "";

		private static List<Keeper.BindruneKeep> _bindruneKeys;

		private static bool _showBindruneOffer;

		private static GameObject _bindruneButton;

		private static string _note;

		private static bool _noteIsProblem;

		private static int _closedFrame = -1;

		private static double _openMs;

		private static double _refreshMs;

		private static bool _refreshSorted;

		private static bool _timingLogged;

		private static bool _listsOnly;

		private static bool _detailStale;

		private static float _sourceScroll;

		private static float _settingScroll;

		private const int MaxChoiceButtons = 4;

		private static readonly string[] FileFilterNames = new string[3] { "No images", "Images only", "All files" };

		private static FileFilter _fileFilter = FileFilter.NoImages;

		private static GameObject _fileFilterObject;

		private const float FileFilterWidth = 150f;

		private static List<ConfigItem> _configItems;

		private static Task<List<ConfigItem>> _configItemsReading;

		private static readonly List<Texture2D> _previews = new List<Texture2D>();

		private const float CompareHeight = 170f;

		private const long PreviewImageBytes = 8388608L;

		private const int ViewerBytes = 65536;

		private const float ViewerHeight = 320f;

		private const int ViewerBlock = 4000;

		private static readonly Color ViewerBackground = new Color(0.07f, 0.06f, 0.05f, 0.94f);

		private static readonly Color ViewerText = new Color(0.87f, 0.85f, 0.79f);

		private static Font _viewerFont;

		private static MethodInfo _loadImage;

		private static bool _loadImageLooked;

		private const float SourceRowHeight = 28f;

		private const float RowHeight = 30f;

		private const float TagColumn = 70f;

		private const float CountColumn = 52f;

		private const float HeaderHeight = 52f;

		private static Text _headerTitle;

		private static Text _headerInfo;

		private static readonly GameObject[] HeaderButtons = (GameObject[])(object)new GameObject[2];

		private static readonly Action[] HeaderActions = new Action[2];

		private static List<Setting> _keepable = new List<Setting>();

		private static List<string> _releasable = new List<string>();

		private static List<string> _unloaded = new List<string>();

		private static VirtualList<SourceItem, SourceView> _sourceList;

		private static VirtualList<SettingItem, SettingView> _settingList;

		private static GameObject _emptyMessage;

		private static int _redraws;

		private static double _slowestMs;

		private static int _slowestCount;

		private static double _firstMs;

		private static int _firstCount;

		private static readonly Color Dim = new Color(1f, 1f, 1f, 0.5f);

		private static readonly Color Kept = new Color(1f, 0.75f, 0.38f);

		private static readonly Color Problem = new Color(1f, 0.45f, 0.4f);

		private static readonly Color Selected = new Color(1f, 0.7f, 0.2f, 0.32f);

		private static readonly Color Clearish = new Color(0f, 0f, 0f, 0.01f);

		private static readonly Color KeptRow = new Color(1f, 0.7f, 0.2f, 0.1f);

		private static readonly Color DefaultBar = new Color(0.44f, 0.69f, 1f, 0.9f);

		private static float PanelWidth => Size.x;

		private static float PanelHeight => Size.y;

		private static float BodyHeight => PanelHeight - 110f - 44f;

		private static float MiddleWidth => PanelWidth - 52f - 270f - 420f - 24f;

		private static Vector2 Size
		{
			get
			{
				//IL_0000: Unknown result type (might be due to invalid IL or missing references)
				//IL_0005: Unknown result type (might be due to invalid IL or missing references)
				//IL_0051: Unknown result type (might be due to invalid IL or missing references)
				//IL_0011: Unknown result type (might be due to invalid IL or missing references)
				//IL_002c: 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)
				//IL_004c: Unknown result type (might be due to invalid IL or missing references)
				if (_size == Vector2.zero)
				{
					_size = new Vector2(Mathf.Min(Plugin.PanelSize.x, (float)Screen.width * 0.95f), Mathf.Min(Plugin.PanelSize.y, (float)Screen.height * 0.95f));
				}
				return _size;
			}
		}

		public static bool IsOpen => (Object)(object)_root != (Object)null;

		public static bool HoldsKeyboard
		{
			get
			{
				if (!IsOpen)
				{
					return Time.frameCount == _closedFrame;
				}
				return true;
			}
		}

		public static bool Typing
		{
			get
			{
				if ((Object)(object)_root == (Object)null)
				{
					return false;
				}
				GameObject val = (((Object)(object)EventSystem.current != (Object)null) ? EventSystem.current.currentSelectedGameObject : null);
				InputField val2 = (((Object)(object)val != (Object)null) ? val.GetComponent<InputField>() : null);
				if ((Object)(object)val2 != (Object)null)
				{
					return val2.isFocused;
				}
				return false;
			}
		}

		private static float DetailInner => 380f;

		private static List<ConfigItem> ConfigItems
		{
			get
			{
				if (_configItems != null)
				{
					return _configItems;
				}
				if (_configItemsReading == null)
				{
					StartReadingFiles();
				}
				try
				{
					_configItems = _configItemsReading.Result;
				}
				catch (Exception ex)
				{
					Plugin.WarnOnce("Keepsake: could not look through BepInEx/config: " + ex.GetBaseException().Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/UI/PanelFiles.cs", 64);
					_configItems = new List<ConfigItem>();
				}
				_configItemsReading = null;
				return _configItems;
			}
		}

		private static Font ViewerFont
		{
			get
			{
				if ((Object)(object)_viewerFont != (Object)null)
				{
					return _viewerFont;
				}
				try
				{
					_viewerFont = Font.CreateDynamicFontFromOSFont(new string[6] { "Consolas", "Cascadia Mono", "Lucida Console", "Courier New", "DejaVu Sans Mono", "Liberation Mono" }, 13);
				}
				catch (Exception ex)
				{
					Plugin.WarnOnce("Keepsake: no fixed width font was found for showing files: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/UI/PanelFiles.cs", 418);
				}
				if (!((Object)(object)_viewerFont != (Object)null))
				{
					return GUIManager.Instance.AveriaSerif;
				}
				return _viewerFont;
			}
		}

		private static void RefreshBindruneKeys()
		{
			try
			{
				_bindruneKeys = Keeper.BindruneKeys();
			}
			catch (Exception ex)
			{
				Plugin.WarnOnce("Keepsake: could not read Bindrune's keys: " + ex.Message, ex, "/home/runner/work/Keepsake/Keepsake/src/Keepsake/UI/KeepsakePanel.cs", 97);
				_bindruneKeys = null;
			}
		}

		public static void Toggle()
		{
			if (IsOpen)
			{
				Close();
			}
			else
			{
				Open();
			}
		}

		public static void Open()
		{
			if (GUIManager.Instance == null || (Object)(object)GUIManager.CustomGUIFront == (Object)null)
			{
				Plugin.Log.LogWarning((object)"Keepsake: the GUI is not ready yet.");
				return;
			}
			Stopwatch stopwatch = Stopwatch.StartNew();
			Keeper.Reconcile();
			_refreshMs = SettingIndex.LastRefreshMs;
			_refreshSorted = SettingIndex.LastRefreshSorted;
			Keeper.Changed = OnChangedElsewhere;
			KeyCapture.Changed = ShowDetail;
			RefreshBindruneKeys();
			_showBindruneOffer = false;
			StartReadingFiles();
			_note = null;
			_redraws = 0;
			_slowestMs = 0.0;
			_slowestCount = 0;
			Build();
			GUIManager.BlockInput(true);
			_openMs = stopwatch.Elapsed.TotalMilliseconds;
			if ((Object)(object)_root != (Object)null)
			{
				Sfx.Play("sfx_gui_inventory_open");
			}
		}

		public static void Close(bool quietly = false)
		{
			Keeper.Changed = null;
			KeyCapture.Cancel();
			KeyCapture.Changed = null;
			if (!((Object)(object)_root == (Object)null))
			{
				if (!quietly)
				{
					Sfx.Play("sfx_gui_inventory_close");
				}
				_closedFrame = Time.frameCount;
				RememberScroll();
				ClearPreview();
				Object.Destroy((Object)(object)_root);
				GUIManager.BlockInput(false);
				_root = null;
				_sourceList = null;
				_settingList = null;
				_emptyMessage = null;
				_detail = null;
				_search = null;
				_bindruneButton = null;
				_repopulateAt = 0f;
				string text = $"Keepsake: the panel opened in {_openMs:0.0} ms, {_refreshMs:0.0} ms of it reading every mod's " + string.Format("settings{0} and {1:0.0} ms drawing the lists and ", _refreshSorted ? " and sorting them" : "", _firstMs) + $"their rows for {_firstCount} settings" + ((_redraws > 1) ? $"; {_redraws - 1} redraws after that, the slowest {_slowestMs:0.0} ms with {_slowestCount} settings." : ".");
				if (_timingLogged)
				{
					Plugin.Log.LogDebug((object)text);
				}
				else
				{
					Plugin.Log.LogInfo((obj

patchers/Keepsake.Preloader.dll

Decompiled 13 hours ago
using 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 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.4.2.0")]
[assembly: AssemblyInformationalVersion("0.4.2+ff133d6146eee7b02584d82c2980f4853cab488f")]
[assembly: AssemblyProduct("Keepsake")]
[assembly: AssemblyTitle("Keepsake.Preloader")]
[assembly: AssemblyVersion("0.4.2.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
{
	public sealed class BindruneKey
	{
		public string Id;

		public string Yours;

		public string Profile;

		public bool Active;
	}
	public 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("[") && text.EndsWith("]"))
				{
					flag2 = text == "[keys]";
				}
				else if (flag2 && text.Length != 0 && !text.StartsWith("#"))
				{
					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;
		}
	}
	public sealed class CfgText
	{
		private readonly string[] _lines;

		private readonly string _newline;

		private bool _changed;

		public string Text => string.Join(_newline, _lines);

		public bool Changed => _changed;

		private CfgText(string text)
		{
			_newline = (text.Contains("\r\n") ? "\r\n" : "\n");
			_lines = text.Replace("\r\n", "\n").Split(new char[1] { '\n' });
		}

		public static CfgText Load(string path)
		{
			return new CfgText(File.ReadAllText(path));
		}

		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("#"))
				{
					break;
				}
				if (text.StartsWith("# Setting type:"))
				{
					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);
		}

		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("#"))
				{
					continue;
				}
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					text = text2.Substring(1, text2.Length - 2);
				}
				else if (!(text != section))
				{
					string[] array = text2.Split(new char[1] { '=' }, 2);
					if (array.Length == 2 && !(array[0].Trim() != key))
					{
						result = i;
						value = array[1].Trim();
					}
				}
			}
			return result;
		}
	}
	public 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;
		}
	}
	public sealed class SettleResult
	{
		public bool Clean;

		public int PutBack;

		public int Updated;

		public int Waiting;
	}
	public 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 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(new char[1] { '/' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0 || array.Any((string p) => p == "." || p == ".." || p.Contains(":")))
			{
				return null;
			}
			if (path.Trim().StartsWith("/") || path.Trim().StartsWith("\\"))
			{
				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") && line.Trim() != "# keepsake files v1")
				{
					return null;
				}
				if (line.Trim().Length == 0 || line.StartsWith("#"))
				{
					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("/") || text.EndsWith("\\"))
					});
				}
			}
			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)))
						{
							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)
				{
					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_0169;
						}
					}
					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_0169;
			IL_0169:
			if (result.Clean)
			{
				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 void Forget(KeptPath kept)
		{
			try
			{
				if (!kept.IsFolder)
				{
					if (File.Exists(Copy(kept.Path)))
					{
						File.Delete(Copy(kept.Path));
					}
					return;
				}
				string path = Path.Combine(StoreRoot, kept.Path.Replace('/', Path.DirectorySeparatorChar));
				if (Directory.Exists(path))
				{
					Directory.Delete(path, recursive: true);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = PinFile.Log;
				if (log != null)
				{
					log.LogWarning((object)("Keepsake: could not remove the copies of " + kept.Path + ": " + ex.Message));
				}
			}
		}
	}
	public 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);
	}
	public 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;

		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(new char[3] { '\t', '\r', '\n' }) < 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") && line.Trim() != "# keepsake pins v1")
				{
					return null;
				}
				if (line.Length == 0 || line.StartsWith("#"))
				{
					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;
				}
				Pin pin = new Pin
				{
					File = NormaliseFile(array[0]),
					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)
		{
			string text2 = path + ".keepsake.tmp";
			File.WriteAllText(text2, text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			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('\\', '/');
		}
	}
	public 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
			};
		}
	}
	public 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
			};
		}
	}
	public 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);
		}
	}
	public 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") && line.Trim() != "# keepsake changes v1")
				{
					return null;
				}
				if (line.Length == 0 || line.StartsWith("#"))
				{
					continue;
				}
				string text2 = line.Trim();
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					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;
		}
	}
	public sealed class WaitingFile
	{
		public string Path;

		public bool PutBack;
	}
	public 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));
		}
	}
	public 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") && line.Trim() != "# keepsake session v1")
				{
					return null;
				}
				if (line.Trim().Length == 0 || line.StartsWith("#"))
				{
					continue;
				}
				string text2 = line.Trim();
				if (text2.StartsWith("[") && text2.EndsWith("]"))
				{
					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."))
					{
						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;
		}
	}
	public static class Preloader
	{
		public static IEnumerable<string> TargetDLLs { get; } = new string[0];

		public static void Patch(AssemblyDefinition assembly)
		{
		}

		public static void Initialize()
		{
			ManualLogSource val = (PinFile.Log = Logger.CreateLogSource("Keepsake"));
			try
			{
				Restore(val);
			}
			catch (Exception arg)
			{
				val.LogError((object)$"Keepsake: putting your values back failed: {arg}");
			}
		}

		private static void Restore(ManualLogSource log)
		{
			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)
			{
				log.LogInfo((object)$"Keepsake: removed {num} file(s) left half written by a game that stopped mid-save.");
			}
			if (list2 != null)
			{
				Restorer.SettleFiles(list2, DateTime.UtcNow, SessionFile.LogEnd());
			}
			if (list == null)
			{
				return;
			}
			if (list.Count == 0)
			{
				Restorer.RecordChanges(list, new List<ProfileChange>());
				return;
			}
			RestoreResult restoreResult = Restorer.Apply(list, BindruneLink.InstalledOnDisk);
			if (restoreResult.Learned)
			{
				PinFile.Write(list);
			}
			int num2 = Restorer.RecordChanges(list, restoreResult.Changes);
			log.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:0.0} ms."));
		}
	}
	public sealed class RestoreResult
	{
		public int Restored;

		public int Missing;

		public int ToBindrune;

		public bool Learned;

		public readonly List<ProfileChange> Changes = new List<ProfileChange>();
	}
	public 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;
		}
	}
}