Sfoglia il codice sorgente

Merge pull request #100 from BepInEx/5.0-rc2

5.0 RC2 changes
Bepis 5 anni fa
parent
commit
cb78681676

+ 0 - 3
.gitmodules

@@ -1,6 +1,3 @@
 [submodule "submodules/BepInEx.Harmony"]
 	path = submodules/BepInEx.Harmony
 	url = https://github.com/BepInEx/BepInEx.Harmony
-[submodule "submodules/MonoMod"]
-	path = submodules/MonoMod
-	url = https://github.com/MonoMod/MonoMod.git

+ 17 - 10
BepInEx.Preloader/BepInEx.Preloader.csproj

@@ -23,8 +23,23 @@
     <DocumentationFile>..\bin\BepInEx.Preloader.xml</DocumentationFile>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
-      <HintPath>$(SolutionDir)\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>
+    <Reference Include="Mono.Cecil, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Mdb, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Mdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Pdb, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Pdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Rocks, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Rocks.dll</HintPath>
+    </Reference>
+    <Reference Include="MonoMod.RuntimeDetour, Version=19.11.5.1, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MonoMod.RuntimeDetour.19.11.5.1\lib\net35\MonoMod.RuntimeDetour.dll</HintPath>
+    </Reference>
+    <Reference Include="MonoMod.Utils, Version=19.11.5.1, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MonoMod.Utils.19.11.5.1\lib\net35\MonoMod.Utils.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
@@ -53,14 +68,6 @@
       <Project>{a15d6ee6-f954-415b-8605-8a8470cc87dc}</Project>
       <Name>Harmony</Name>
     </ProjectReference>
-    <ProjectReference Include="..\submodules\MonoMod\MonoMod.RuntimeDetour\MonoMod.RuntimeDetour.csproj">
-      <Project>{d0c584c0-81d7-486e-b70e-d7f9256e0909}</Project>
-      <Name>MonoMod.RuntimeDetour</Name>
-    </ProjectReference>
-    <ProjectReference Include="..\submodules\MonoMod\MonoMod.Utils\MonoMod.Utils.csproj">
-      <Project>{1839cfe2-3db0-45a8-b03d-9aa797479a3a}</Project>
-      <Name>MonoMod.Utils</Name>
-    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <None Include="packages.config" />

+ 28 - 10
BepInEx.Preloader/Entrypoint.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -48,20 +49,32 @@ namespace BepInEx.Preloader
 		/// </param>
 		public static void Main(string[] args)
 		{
-			EnvVars.LoadVars();
+			// We set it to the current directory first as a fallback, but try to use the same location as the .exe file.
+			string silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log";
 
-			// Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons
-			preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH));
+			try
+			{
+				EnvVars.LoadVars();
+
+				silentExceptionLog = Path.Combine(GetCurrentProcessDirectory(), silentExceptionLog);
 
-			AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory;
+				// Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons
+				preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH));
 
-			// In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath
-			// To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code
-			typeof(Entrypoint).Assembly.GetType($"BepInEx.Preloader.{nameof(PreloaderRunner)}")
-							  ?.GetMethod(nameof(PreloaderRunner.PreloaderMain))
-							  ?.Invoke(null, new object[] { args });
+				AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory;
 
-			AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;
+				// In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath
+				// To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code
+				typeof(Entrypoint).Assembly.GetType($"BepInEx.Preloader.{nameof(PreloaderRunner)}")
+								  ?.GetMethod(nameof(PreloaderRunner.PreloaderMain))
+								  ?.Invoke(null, new object[] { args });
+
+				AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;
+			}
+			catch (Exception ex)
+			{
+				File.WriteAllText(silentExceptionLog, ex.ToString());
+			}
 		}
 
 		private static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args)
@@ -77,5 +90,10 @@ namespace BepInEx.Preloader
 				return null;
 			}
 		}
+
+		private static string GetCurrentProcessDirectory()
+		{
+			return Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
+		}
 	}
 }

+ 1 - 1
BepInEx.Preloader/Logger/PreloaderLogWriter.cs

@@ -9,7 +9,7 @@ namespace BepInEx.Preloader
 {
 	public class PreloaderConsoleListener : ILogListener
 	{
-		public List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
+		public static List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
 		protected StringBuilder LogBuilder = new StringBuilder();
 
 		public static TextWriter StandardOut { get; set; }

+ 3 - 3
BepInEx.Preloader/Patching/AssemblyPatcher.cs

@@ -283,17 +283,17 @@ namespace BepInEx.Preloader.Patching
 
 		#region Config
 
-		private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.Bind(
 			"Preloader", "DumpAssemblies",
 			false,
 			"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.");
 
-		private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Bind(
 			"Preloader", "LoadDumpedAssemblies",
 			false,
 			"If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies.");
 
-		private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Bind(
 			"Preloader", "BreakBeforeLoadAssemblies",
 			false,
 			"If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded.");

+ 14 - 6
BepInEx.Preloader/Preloader.cs

@@ -8,6 +8,7 @@ using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.Preloader.Patching;
 using BepInEx.Preloader.RuntimeFixes;
+using HarmonyLib;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
 using MonoMod.RuntimeDetour;
@@ -198,10 +199,17 @@ namespace BepInEx.Preloader
 
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint
+
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
+
 					il.InsertBefore(ins,
+						il.Create(OpCodes.Call, assembly.MainModule.ImportReference(
+							AccessTools.PropertyGetter(typeof(PreloaderConsoleListener), nameof(PreloaderConsoleListener.LogEvents))))); // preloaderLogEvents (load from Preloader.PreloaderLog.LogEvents)
+
+                    il.InsertBefore(ins,
 						il.Create(OpCodes.Call, initMethod)); // Chainloader.Initialize(string gamePath, string managedPath = null, bool startConsole = true)
+
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Call, startMethod));
 				}
@@ -237,32 +245,32 @@ namespace BepInEx.Preloader
 
 		#region Config
 
-		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
 			"Preloader.Entrypoint", "Assembly",
 			IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll",
 			"The local filename of the assembly to target.");
 
-		private static readonly ConfigEntry<string> ConfigEntrypointType = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointType = ConfigFile.CoreConfig.Bind(
 			"Preloader.Entrypoint", "Type",
 			"Application",
 			"The name of the type in the entrypoint assembly to search for the entrypoint method.");
 
-		private static readonly ConfigEntry<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.Bind(
 			"Preloader.Entrypoint", "Method",
 			".cctor",
 			"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.");
 
-		private static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind(
 			"Preloader", "ApplyRuntimePatches",
 			true,
 			"Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.");
 
-		private static readonly ConfigEntry<bool> ConfigShimHarmony = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigShimHarmony = ConfigFile.CoreConfig.Bind(
 			"Preloader", "ShimHarmonySupport",
 			!Utility.CLRSupportsDynamicAssemblies,
 			"If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.");
 
-		private static readonly ConfigEntry<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Bind(
 			"Logging", "PreloaderConsoleOutRedirection",
 			true,
 			"Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.");

+ 3 - 2
BepInEx.Preloader/packages.config

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <packages>
-  <package id="Mono.Cecil" version="0.10.3" targetFramework="net35" />
+  <package id="Mono.Cecil" version="0.10.4" targetFramework="net35" />
+  <package id="MonoMod.RuntimeDetour" version="19.11.5.1" targetFramework="net35" />
+  <package id="MonoMod.Utils" version="19.11.5.1" targetFramework="net35" />
 </packages>

+ 0 - 14
BepInEx.sln

@@ -23,10 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Patcher", "Patcher", "{A907
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.Preloader", "BepInEx.Preloader\BepInEx.Preloader.csproj", "{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.RuntimeDetour", "submodules\MonoMod\MonoMod.RuntimeDetour\MonoMod.RuntimeDetour.csproj", "{D0C584C0-81D7-486E-B70E-D7F9256E0909}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.Utils", "submodules\MonoMod\MonoMod.Utils\MonoMod.Utils.csproj", "{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}"
-EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInExTests", "BepInExTests\BepInExTests.csproj", "{E7CD429A-D057-48E3-8C51-E5C934E8E07B}"
 	ProjectSection(ProjectDependencies) = postProject
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9} = {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}
@@ -62,14 +58,6 @@ Global
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Release|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = Release|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU
 		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|x86
 		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|x86
 		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|x86
@@ -82,8 +70,6 @@ Global
 		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB} = {A9071994-3533-4C1B-89DC-D817B676AB41}
 		{54161CFE-FF42-4DDE-B161-3A49545DB5CD} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
 		{A15D6EE6-F954-415B-8605-8A8470CC87DC} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {55AC11EF-F568-4C79-A356-7ED9510145B1}

+ 11 - 2
BepInEx/BepInEx.csproj

@@ -39,8 +39,17 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
-      <HintPath>$(SolutionDir)\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>
+    <Reference Include="Mono.Cecil, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Mdb, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Mdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Pdb, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Pdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Rocks, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Rocks.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="UnityEngine">

+ 30 - 8
BepInEx/Bootstrap/Chainloader.cs

@@ -33,7 +33,7 @@ namespace BepInEx.Bootstrap
 				lock (_plugins)
 				{
 					_plugins.RemoveAll(x => x == null);
-					return _plugins;
+					return _plugins.ToList();
 				}
 			}
 		}
@@ -52,7 +52,7 @@ namespace BepInEx.Bootstrap
 		/// <summary>
 		/// Initializes BepInEx to be able to start the chainloader.
 		/// </summary>
-		public static void Initialize(string gameExePath, bool startConsole = true)
+		public static void Initialize(string gameExePath, bool startConsole = true, ICollection<LogEventArgs> preloaderLogEvents = null)
 		{
 			if (_initialized)
 				return;
@@ -96,6 +96,27 @@ namespace BepInEx.Bootstrap
 				Logger.Sources.Add(new UnityLogSource());
 
 
+			// Temporarily disable the console log listener as we replay the preloader logs
+
+			var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
+			
+			if (logListener != null)
+				Logger.Listeners.Remove(logListener);
+
+			var preloaderLogSource = Logger.CreateLogSource("Preloader");
+
+			foreach (var preloaderLogEvent in preloaderLogEvents)
+			{
+				preloaderLogSource.Log(preloaderLogEvent.Level, preloaderLogEvent.Data);
+			}
+
+			Logger.Sources.Remove(preloaderLogSource);
+
+			if (logListener != null)
+				Logger.Listeners.Add(logListener);
+
+
+
 			Logger.LogMessage("Chainloader ready");
 
 			_initialized = true;
@@ -210,7 +231,8 @@ namespace BepInEx.Bootstrap
 
 				Logger.LogInfo($"{pluginInfos.Count} plugins to load");
 
-				var dependencyDict = new Dictionary<string, IEnumerable<string>>();
+				// We use a sorted dictionary to ensure consistent load order
+				var dependencyDict = new SortedDictionary<string, IEnumerable<string>>(StringComparer.InvariantCultureIgnoreCase);
 				var pluginsByGUID = new Dictionary<string, PluginInfo>();
 
 				foreach (var pluginInfoGroup in pluginInfos.GroupBy(info => info.Metadata.GUID))
@@ -357,27 +379,27 @@ namespace BepInEx.Bootstrap
 		#region Config
 
 
-		private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Bind(
 			"Logging", "UnityLogListening",
 			true,
 			"Enables showing unity log messages in the BepInEx logging system.");
 
-		private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind(
 			"Logging.Disk", "WriteUnityLog",
 			false,
 			"Include unity log messages in log file output.");
 
-		private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
 			"Logging.Disk", "AppendLog",
 			false,
 			"Appends to the log file instead of overwriting, on game startup.");
 
-		private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
 			"Logging.Disk", "Enabled",
 			true,
 			"Enables writing log messages to disk.");
 
-		private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
 			"Logging.Disk", "DisplayedLogLevel",
 			LogLevel.Info,
 			"Only displays the specified log level and above in the console output.");

+ 1 - 1
BepInEx/Bootstrap/TypeLoader.cs

@@ -251,7 +251,7 @@ namespace BepInEx.Bootstrap
 
 		#region Config
 
-		private static readonly ConfigEntry<bool> EnableAssemblyCache = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<bool> EnableAssemblyCache = ConfigFile.CoreConfig.Bind(
 			"Caching", "EnableAssemblyCache", 
 			true, 
 			"Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.");

+ 276 - 76
BepInEx/Configuration/ConfigFile.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.IO;
@@ -11,7 +12,7 @@ namespace BepInEx.Configuration
 	/// <summary>
 	/// A helper class to handle persistent data. All public methods are thread-safe.
 	/// </summary>
-	public class ConfigFile
+	public class ConfigFile : IDictionary<ConfigDefinition, ConfigEntryBase>
 	{
 		private readonly BepInPlugin _ownerMetadata;
 
@@ -22,28 +23,33 @@ namespace BepInEx.Configuration
 		/// </summary>
 		protected Dictionary<ConfigDefinition, ConfigEntryBase> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntryBase>();
 
-		private Dictionary<ConfigDefinition, string> HomelessEntries { get; } = new Dictionary<ConfigDefinition, string>();
+		private Dictionary<ConfigDefinition, string> OrphanedEntries { get; } = new Dictionary<ConfigDefinition, string>();
 
 		/// <summary>
 		/// Create a list with all config entries inside of this config file.
 		/// </summary>
-		[Obsolete("Use GetConfigEntries instead")]
+		[Obsolete("Use Keys instead")]
 		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions
 		{
 			get
 			{
-				lock (_ioLock) return Entries.Keys.ToList().AsReadOnly();
+				lock (_ioLock)
+				{
+					return Entries.Keys.ToList().AsReadOnly();
+				}
 			}
 		}
-
+		
 		/// <summary>
 		/// Create an array with all config entries inside of this config file. Should be only used for metadata purposes.
 		/// If you want to access and modify an existing setting then use <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/> 
 		/// instead with no description.
 		/// </summary>
+		[Obsolete("Use Values instead")]
 		public ConfigEntryBase[] GetConfigEntries()
 		{
-			lock (_ioLock) return Entries.Values.ToArray();
+			lock (_ioLock)
+				return Entries.Values.ToArray();
 		}
 
 		/// <summary>
@@ -87,7 +93,6 @@ namespace BepInEx.Configuration
 		#region Save/Load
 
 		private readonly object _ioLock = new object();
-		private bool _disableSaving;
 
 		/// <summary>
 		/// Reloads the config from disk. Unsaved changes are lost.
@@ -96,47 +101,38 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				HomelessEntries.Clear();
+				OrphanedEntries.Clear();
+
+				string currentSection = string.Empty;
 
-				try
+				foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
 				{
-					_disableSaving = true;
+					string line = rawLine.Trim();
 
-					string currentSection = string.Empty;
+					if (line.StartsWith("#")) //comment
+						continue;
 
-					foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
+					if (line.StartsWith("[") && line.EndsWith("]")) //section
 					{
-						string line = rawLine.Trim();
-
-						if (line.StartsWith("#")) //comment
-							continue;
-
-						if (line.StartsWith("[") && line.EndsWith("]")) //section
-						{
-							currentSection = line.Substring(1, line.Length - 2);
-							continue;
-						}
+						currentSection = line.Substring(1, line.Length - 2);
+						continue;
+					}
 
-						string[] split = line.Split('='); //actual config line
-						if (split.Length != 2)
-							continue; //empty/invalid line
+					string[] split = line.Split('='); //actual config line
+					if (split.Length != 2)
+						continue; //empty/invalid line
 
-						string currentKey = split[0].Trim();
-						string currentValue = split[1].Trim();
+					string currentKey = split[0].Trim();
+					string currentValue = split[1].Trim();
 
-						var definition = new ConfigDefinition(currentSection, currentKey);
+					var definition = new ConfigDefinition(currentSection, currentKey);
 
-						Entries.TryGetValue(definition, out ConfigEntryBase entry);
+					Entries.TryGetValue(definition, out ConfigEntryBase entry);
 
-						if (entry != null)
-							entry.SetSerializedValue(currentValue);
-						else
-							HomelessEntries[definition] = currentValue;
-					}
-				}
-				finally
-				{
-					_disableSaving = false;
+					if (entry != null)
+						entry.SetSerializedValue(currentValue);
+					else
+						OrphanedEntries[definition] = currentValue;
 				}
 			}
 
@@ -150,8 +146,6 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				if (_disableSaving) return;
-
 				string directoryName = Path.GetDirectoryName(ConfigFilePath);
 				if (directoryName != null) Directory.CreateDirectory(directoryName);
 
@@ -165,7 +159,7 @@ namespace BepInEx.Configuration
 					}
 
 					var allConfigEntries = Entries.Select(x => new { x.Key, entry = x.Value, value = x.Value.GetSerializedValue() })
-						.Concat(HomelessEntries.Select(x => new { x.Key, entry = (ConfigEntryBase)null, value = x.Value }));
+						.Concat(OrphanedEntries.Select(x => new { x.Key, entry = (ConfigEntryBase)null, value = x.Value }));
 
 					foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key))
 					{
@@ -198,26 +192,65 @@ namespace BepInEx.Configuration
 		/// </summary>
 		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
 		/// <param name="configDefinition">Section and Key of the setting.</param>
+		[Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
 		public ConfigEntry<T> GetSetting<T>(ConfigDefinition configDefinition)
 		{
+			return TryGetEntry<T>(configDefinition, out var entry)
+				? entry
+				: null;
+		}
+
+		/// <summary>
+		/// Access one of the existing settings. If the setting has not been added yet, null is returned.
+		/// If the setting exists but has a different type than T, an exception is thrown.
+		/// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>.
+		/// </summary>
+		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
+		/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
+		/// <param name="key">Name of the setting.</param>
+		[Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
+		public ConfigEntry<T> GetSetting<T>(string section, string key)
+		{
+			return TryGetEntry<T>(section, key, out var entry)
+				? entry
+				: null;
+		}
+
+		/// <summary>
+		/// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true.
+		/// If the setting exists but has a different type than T, an exception is thrown.
+		/// New settings should be added with <see cref="Bind{T}"/>.
+		/// </summary>
+		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
+		/// <param name="configDefinition">Section and Key of the setting.</param>
+		/// <param name="entry">The ConfigEntry value to return.</param>
+		public bool TryGetEntry<T>(ConfigDefinition configDefinition, out ConfigEntry<T> entry)
+		{
 			lock (_ioLock)
 			{
-				Entries.TryGetValue(configDefinition, out var entry);
-				return (ConfigEntry<T>)entry;
+				if (Entries.TryGetValue(configDefinition, out var rawEntry))
+				{
+					entry = (ConfigEntry<T>)rawEntry;
+					return true;
+				}
+
+				entry = null;
+				return false;
 			}
 		}
 
 		/// <summary>
 		/// Access one of the existing settings. If the setting has not been added yet, null is returned.
 		/// If the setting exists but has a different type than T, an exception is thrown.
-		/// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>.
+		/// New settings should be added with <see cref="Bind{T}"/>.
 		/// </summary>
 		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
 		/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
 		/// <param name="key">Name of the setting.</param>
-		public ConfigEntry<T> GetSetting<T>(string section, string key)
+		/// <param name="entry">The ConfigEntry value to return.</param>
+		public bool TryGetEntry<T>(string section, string key, out ConfigEntry<T> entry)
 		{
-			return GetSetting<T>(new ConfigDefinition(section, key));
+			return TryGetEntry<T>(new ConfigDefinition(section, key), out entry);
 		}
 
 		/// <summary>
@@ -228,39 +261,30 @@ namespace BepInEx.Configuration
 		/// <param name="configDefinition">Section and Key of the setting.</param>
 		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
 		/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
-		public ConfigEntry<T> AddSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
+		public ConfigEntry<T> Bind<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
 		{
 			if (!TomlTypeConverter.CanConvert(typeof(T)))
 				throw new ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}");
 
 			lock (_ioLock)
 			{
-				if (Entries.ContainsKey(configDefinition))
-					throw new ArgumentException("The setting " + configDefinition + " has already been created. Use GetSetting to get it.");
+				if (Entries.TryGetValue(configDefinition, out var rawEntry))
+					return (ConfigEntry<T>)rawEntry;
 
-				try
-				{
-					_disableSaving = true;
-
-					var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription);
-					Entries[configDefinition] = entry;
-
-					if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue))
-					{
-						entry.SetSerializedValue(homelessValue);
-						HomelessEntries.Remove(configDefinition);
-					}
+				var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription);
 
-					_disableSaving = false;
-					if (SaveOnConfigSet)
-						Save();
+				Entries[configDefinition] = entry;
 
-					return entry;
-				}
-				finally
+				if (OrphanedEntries.TryGetValue(configDefinition, out string homelessValue))
 				{
-					_disableSaving = false;
+					entry.SetSerializedValue(homelessValue);
+					OrphanedEntries.Remove(configDefinition);
 				}
+
+				if (SaveOnConfigSet)
+					Save();
+
+				return entry;
 			}
 		}
 
@@ -273,8 +297,45 @@ namespace BepInEx.Configuration
 		/// <param name="key">Name of the setting.</param>
 		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
 		/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
+		public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
+			=> Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
+
+		/// <summary>
+		/// Create a new setting. The setting is saved to drive and loaded automatically.
+		/// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
+		/// </summary>
+		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
+		/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
+		/// <param name="key">Name of the setting.</param>
+		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
+		/// <param name="description">Simple description of the setting shown to the user.</param>
+		public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, string description)
+			=> Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
+
+		/// <summary>
+		/// Create a new setting. The setting is saved to drive and loaded automatically.
+		/// Each definition can be used to add only one setting, trying to add a second setting will throw an exception.
+		/// </summary>
+		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
+		/// <param name="configDefinition">Section and Key of the setting.</param>
+		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
+		/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
+		[Obsolete("Use Bind instead")]
+		public ConfigEntry<T> AddSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
+			=> Bind(configDefinition, defaultValue, configDescription);
+
+		/// <summary>
+		/// Create a new setting. The setting is saved to drive and loaded automatically.
+		/// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
+		/// </summary>
+		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
+		/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
+		/// <param name="key">Name of the setting.</param>
+		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
+		/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
+		[Obsolete("Use Bind instead")]
 		public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
-			=> AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription);
+			=> Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
 
 		/// <summary>
 		/// Create a new setting. The setting is saved to drive and loaded automatically.
@@ -285,27 +346,28 @@ namespace BepInEx.Configuration
 		/// <param name="key">Name of the setting.</param>
 		/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
 		/// <param name="description">Simple description of the setting shown to the user.</param>
+		[Obsolete("Use Bind instead")]
 		public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, string description)
-			=> AddSetting(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
+			=> Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
 
         /// <summary>
-        /// Access a setting. Use AddSetting and GetSetting instead.
+        /// Access a setting. Use Bind instead.
         /// </summary>
-        [Obsolete("Use AddSetting and GetSetting instead")]
+        [Obsolete("Use Bind instead")]
 		public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
 		{
 			lock (_ioLock)
 			{
 				var definition = new ConfigDefinition(section, key, description);
-				var setting = GetSetting<T>(definition) ?? AddSetting(definition, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
+				var setting = Bind(definition, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
 				return new ConfigWrapper<T>(setting);
 			}
 		}
 
 		/// <summary>
-		/// Access a setting. Use AddSetting and GetSetting instead.
+		/// Access a setting. Use Bind instead.
 		/// </summary>
-		[Obsolete("Use AddSetting and GetSetting instead")]
+		[Obsolete("Use Bind instead")]
 		public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
 			=> Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
 
@@ -354,5 +416,143 @@ namespace BepInEx.Configuration
 		}
 
 		#endregion
+
+		/// <inheritdoc />
+		public IEnumerator<KeyValuePair<ConfigDefinition, ConfigEntryBase>> GetEnumerator()
+		{
+			// We can't really do a read lock for this
+			return Entries.GetEnumerator();
+		}
+
+		IEnumerator IEnumerable.GetEnumerator()
+		{
+			return GetEnumerator();
+		}
+
+		void ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.Add(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
+		{
+			lock (_ioLock)
+				Entries.Add(item.Key, item.Value);
+		}
+
+		/// <inheritdoc />
+		public bool Contains(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
+		{
+			lock (_ioLock)
+				return ((ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)Entries).Contains(item);
+		}
+
+		void ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.CopyTo(KeyValuePair<ConfigDefinition, ConfigEntryBase>[] array, int arrayIndex)
+		{
+			lock (_ioLock)
+				((ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)Entries).CopyTo(array, arrayIndex);
+		}
+
+		bool ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.Remove(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
+		{
+			lock (_ioLock)
+				return Entries.Remove(item.Key);
+		}
+
+		/// <inheritdoc />
+		public int Count
+		{
+			get
+			{
+				lock (_ioLock)
+					return Entries.Count;
+			}
+		}
+
+		/// <inheritdoc />
+		public bool IsReadOnly => false;
+
+		/// <inheritdoc />
+		public bool ContainsKey(ConfigDefinition key)
+		{
+			lock (_ioLock)
+				return Entries.ContainsKey(key);
+		}
+
+		/// <inheritdoc />
+		public void Add(ConfigDefinition key, ConfigEntryBase value)
+		{
+			throw new InvalidOperationException("Directly adding a config entry is not supported");
+		}
+
+		/// <inheritdoc />
+		public bool Remove(ConfigDefinition key)
+		{
+			lock (_ioLock)
+				return Entries.Remove(key);
+		}
+
+		/// <inheritdoc />
+		public void Clear()
+		{
+			lock (_ioLock)
+				Entries.Clear();
+		}
+
+		bool IDictionary<ConfigDefinition, ConfigEntryBase>.TryGetValue(ConfigDefinition key, out ConfigEntryBase value)
+		{
+			lock (_ioLock)
+				return Entries.TryGetValue(key, out value);
+		}
+
+		/// <inheritdoc />
+		ConfigEntryBase IDictionary<ConfigDefinition, ConfigEntryBase>.this[ConfigDefinition key]
+		{
+			get
+			{
+				lock (_ioLock)
+					return Entries[key];
+			}
+			set => throw new InvalidOperationException("Directly setting a config entry is not supported");
+		}
+
+		/// <inheritdoc />
+		public ConfigEntryBase this[ConfigDefinition key]
+		{
+			get
+			{
+				lock (_ioLock)
+					return Entries[key];
+			}
+		}
+
+		/// <summary>
+		/// 
+		/// </summary>
+		/// <param name="section"></param>
+		/// <param name="key"></param>
+		public ConfigEntryBase this[string section, string key]
+			=> this[new ConfigDefinition(section, key)];
+
+		/// <summary>
+		/// Returns the ConfigDefinitions that the ConfigFile contains.
+		/// <para>Creates a new array when the property is accessed. Thread-safe.</para>
+		/// </summary>
+		public ICollection<ConfigDefinition> Keys
+		{
+			get
+			{
+				lock (_ioLock)
+					return Entries.Keys.ToArray();
+			}
+		}
+
+		/// <summary>
+		/// Returns the ConfigEntryBase values that the ConfigFile contains.
+		/// <para>Creates a new array when the property is accessed. Thread-safe.</para>
+		/// </summary>
+		ICollection<ConfigEntryBase> IDictionary<ConfigDefinition, ConfigEntryBase>.Values
+		{
+			get
+			{
+				lock (_ioLock)
+					return Entries.Values.ToArray();
+			}
+		}
 	}
-}
+}

+ 1 - 1
BepInEx/Configuration/ConfigWrapper.cs

@@ -6,7 +6,7 @@ namespace BepInEx.Configuration
 	/// Provides access to a single setting inside of a <see cref="Configuration.ConfigFile"/>.
 	/// </summary>
 	/// <typeparam name="T">Type of the setting.</typeparam>
-	[Obsolete("Use ConfigFile from new AddSetting overloads instead")]
+	[Obsolete("Use ConfigFile from new Bind overloads instead")]
 	public sealed class ConfigWrapper<T>
 	{
 		/// <summary>

+ 1 - 1
BepInEx/Configuration/KeyboardShortcut.cs

@@ -12,7 +12,7 @@ namespace BepInEx.Configuration
 	/// triggered when the user presses the exact combination. For example, <c>F + LeftCtrl</c> will trigger only if user 
 	/// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger.
 	/// 
-	/// Can be used as a value of a setting in <see cref="ConfigFile.AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/> 
+	/// Can be used as a value of a setting in <see cref="ConfigFile.Bind{T}(ConfigDefinition,T,ConfigDescription)"/> 
 	/// to allow user to change this shortcut and have the changes saved.
 	/// 
 	/// How to use: Use <see cref="IsDown"/> in this class instead of <see cref="Input.GetKeyDown(KeyCode)"/> in the Update loop.

+ 2 - 2
BepInEx/ConsoleUtil/ConsoleWindow.cs

@@ -13,12 +13,12 @@ namespace UnityInjector.ConsoleUtil
 {
 	internal class ConsoleWindow
 	{
-		public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.AddSetting(
+		public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind(
 			"Logging.Console", "Enabled",
 			false,
 			"Enables showing a console for log output.");
 
-		public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.AddSetting(
+		public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.Bind(
 			"Logging.Console", "ShiftJisEncoding",
 			false,
 			"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.");

+ 1 - 1
BepInEx/Logging/ConsoleLogListener.cs

@@ -23,7 +23,7 @@ namespace BepInEx.Logging
 
 		public void Dispose() { }
 
-		private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
+		private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
 			"Logging.Console","DisplayedLogLevel",
 			LogLevel.Info,
 			"Only displays the specified log level and above in the console output.");

+ 1 - 2
BepInEx/packages.config

@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <packages>
-  <package id="Mono.Cecil" version="0.10.3" targetFramework="net35" />
+  <package id="Mono.Cecil" version="0.10.4" targetFramework="net35" />
 </packages>

+ 17 - 17
BepInExTests/Configuration/ConfigFileTests.cs

@@ -46,7 +46,7 @@ namespace BepInEx.Configuration.Tests
 		{
 			var c = MakeConfig();
 
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 			var lines = File.ReadAllLines(c.ConfigFilePath);
 			Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
 			Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
@@ -69,7 +69,7 @@ namespace BepInEx.Configuration.Tests
 		public void AutoSaveTest()
 		{
 			var c = MakeConfig();
-			c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 
 			var eventFired = new AutoResetEvent(false);
 			c.ConfigReloaded += (sender, args) => eventFired.Set();
@@ -85,9 +85,9 @@ namespace BepInEx.Configuration.Tests
 			var c = MakeConfig();
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n");
 			c.Reload();
-			var w = c.AddSetting("Cat", "Key", 0, "Test");
+			var w = c.Bind("Cat", "Key", 0, "Test");
 			Assert.AreEqual(w.Value, 1);
-			var w2 = c.AddSetting("Cat", "Key2", 0, new ConfigDescription("Test"));
+			var w2 = c.Bind("Cat", "Key2", 0, new ConfigDescription("Test"));
 			Assert.AreEqual(w2.Value, 0);
 		}
 
@@ -95,7 +95,7 @@ namespace BepInEx.Configuration.Tests
 		public void ReadTest2()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 			Assert.AreEqual(w.Value, 0);
 
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n");
@@ -121,7 +121,7 @@ namespace BepInEx.Configuration.Tests
 		public void EventTestWrapper()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n");
 
@@ -141,7 +141,7 @@ namespace BepInEx.Configuration.Tests
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\nHomeless=0");
 			c.Reload();
 
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 
 			c.Save();
 
@@ -154,7 +154,7 @@ namespace BepInEx.Configuration.Tests
 			var c = MakeConfig();
 			var eventFired = false;
 
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test"));
 			w.SettingChanged += (sender, args) => eventFired = true;
 
 			Assert.IsFalse(eventFired);
@@ -169,7 +169,7 @@ namespace BepInEx.Configuration.Tests
 		public void ValueRangeTest()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
 
 			Assert.AreEqual(0, w.Value);
 			w.Value = 2;
@@ -185,7 +185,7 @@ namespace BepInEx.Configuration.Tests
 		public void ValueRangeBadTypeTest()
 		{
 			var c = MakeConfig();
-			c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<float>(1, 2)));
+			c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<float>(1, 2)));
 			Assert.Fail();
 		}
 
@@ -193,7 +193,7 @@ namespace BepInEx.Configuration.Tests
 		public void ValueRangeDefaultTest()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(1, 2)));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(1, 2)));
 
 			Assert.AreEqual(w.Value, 1);
 		}
@@ -206,7 +206,7 @@ namespace BepInEx.Configuration.Tests
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n");
 			c.Reload();
 
-			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
+			var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
 
 			Assert.AreEqual(w.Value, 1);
 
@@ -220,7 +220,7 @@ namespace BepInEx.Configuration.Tests
 		public void ValueListTest()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList<string>("lel", "kek", "wew", "why")));
+			var w = c.Bind("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList<string>("lel", "kek", "wew", "why")));
 
 			Assert.AreEqual("kek", w.Value);
 			w.Value = "wew";
@@ -240,7 +240,7 @@ namespace BepInEx.Configuration.Tests
 			Assert.AreEqual(shortcut, d);
 
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift));
+			var w = c.Bind("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift));
 			Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value);
 
 			w.Value = shortcut;
@@ -255,7 +255,7 @@ namespace BepInEx.Configuration.Tests
 
 			var c = MakeConfig();
 
-			var w = c.AddSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test"));
 
 			Assert.AreEqual("", w.GetSerializedValue());
 
@@ -280,7 +280,7 @@ namespace BepInEx.Configuration.Tests
 			const string testVal = "new line\n test \t\0";
 
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", testVal, new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", testVal, new ConfigDescription("Test"));
 
 			Assert.AreEqual(testVal, w.Value);
 			Assert.IsFalse(w.GetSerializedValue().Any(x => x == '\n'));
@@ -298,7 +298,7 @@ namespace BepInEx.Configuration.Tests
 		public void UnescapedPathString()
 		{
 			var c = MakeConfig();
-			var w = c.AddSetting("Cat", "Key", "", new ConfigDescription("Test"));
+			var w = c.Bind("Cat", "Key", "", new ConfigDescription("Test"));
 
 			var unescaped = @"D:\test\p ath";
 			foreach (string testVal in new[] { unescaped, @"D:\\test\\p ath" })

+ 2 - 2
README.md

@@ -22,9 +22,9 @@ Unity plugin framework
 **[User and developer guides](https://github.com/BepInEx/BepInEx/wiki)**
 
 ## Used libraries
-- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.11.0.0 ([68a4e2d](https://github.com/NeighTools/UnityDoorstop/commit/68a4e2db1f09e5f5cc2f479d293b09764d09c80b))
+- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.12.0.0 ([18402d7](https://github.com/NeighTools/UnityDoorstop/commit/18402d704bde71475aa47fe02235b286c414ba89))
 - [pardeike/Harmony](https://github.com/pardeike/Harmony) - pre-2.0 ([443f551](https://github.com/pardeike/Harmony/commit/443f551ec45ecf409755b5979a4466343197de03))
-- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.08.02.03 ([9462a0f](https://github.com/MonoMod/MonoMod/commit/9462a0f75b669606b59a4648a3461338d2be32c2))
+- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.11.05.01 equivalent ([3f33800](https://github.com/MonoMod/MonoMod/commit/3f33800bf213ffe5120c098274bf61d0feeede15))
 - [jbevain/cecil](https://github.com/jbevain/cecil) - 0.10.3 ([fb289a7](https://github.com/jbevain/cecil/commit/fb289a7cd80ceb6af5c86e7c7ecce9bf1e98b8fe))
 
 ## Credits

+ 0 - 1
submodules/MonoMod

@@ -1 +0,0 @@
-Subproject commit 9462a0f75b669606b59a4648a3461338d2be32c2