Browse Source

Basic API restructure and preliminary TOML serializer

Bepis 5 years ago
parent
commit
894a42ea58

+ 13 - 8
BepInEx.Preloader/Patching/AssemblyPatcher.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Reflection;
+using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.Preloader.RuntimeFixes;
 using Mono.Cecil;
@@ -23,12 +24,6 @@ namespace BepInEx.Preloader.Patching
 		public static List<PatcherPlugin> PatcherPlugins { get; } = new List<PatcherPlugin>();
 
 		/// <summary>
-		///     Configuration value of whether assembly dumping is enabled or not.
-		/// </summary>
-		private static bool DumpingEnabled =>
-			Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader"));
-
-		/// <summary>
 		///     Adds a single assembly patcher to the pool of applicable patches.
 		/// </summary>
 		/// <param name="patcher">Patcher to apply.</param>
@@ -140,7 +135,7 @@ namespace BepInEx.Preloader.Patching
 				string filename = kv.Key;
 				var assembly = kv.Value;
 
-				if (DumpingEnabled && patchedAssemblies.Contains(filename))
+				if (ConfigDumpAssemblies.Value && patchedAssemblies.Contains(filename))
 					using (var mem = new MemoryStream())
 					{
 						string dirPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies");
@@ -167,7 +162,7 @@ namespace BepInEx.Preloader.Patching
 		}
 
 		/// <summary>
-		///     Loads an individual assembly defintion into the CLR.
+		///     Loads an individual assembly definition into the CLR.
 		/// </summary>
 		/// <param name="assembly">The assembly to load.</param>
 		public static void Load(AssemblyDefinition assembly)
@@ -178,5 +173,15 @@ namespace BepInEx.Preloader.Patching
 				Assembly.Load(assemblyStream.ToArray());
 			}
 		}
+
+		#region Config
+
+		private static ConfigWrapper<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.Wrap(
+			"Preloader",
+			"DumpAssemblies",
+			"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.",
+			false);
+
+		#endregion
 	}
 }

+ 74 - 16
BepInEx.Preloader/Preloader.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Reflection;
 using System.Text;
+using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.Preloader.Patching;
 using BepInEx.Preloader.RuntimeFixes;
@@ -29,13 +30,15 @@ namespace BepInEx.Preloader
 		{
 			try
 			{
+				InitConfig();
+
 				AllocateConsole();
 
 				UnityPatches.Apply();
 
 				Logger.Sources.Add(TraceLogSource.CreateSource());
 
-				PreloaderLog = new PreloaderConsoleListener(Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx")));
+				PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
 
 				Logger.Listeners.Add(PreloaderLog);
 
@@ -65,18 +68,21 @@ namespace BepInEx.Preloader
 
 				Logger.LogMessage("Preloader started");
 
-				string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader");
 
 				AssemblyPatcher.AddPatcher(new PatcherPlugin
-					{ TargetDLLs = new[] { entrypointAssembly }, Patcher = PatchEntrypoint });
+					{ TargetDLLs = new[] { ConfigEntrypointAssembly.Value }, Patcher = PatchEntrypoint });
+
 				AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);
 
 				Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");
 
+
 				AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
 
+
 				AssemblyPatcher.DisposePatchers();
 
+
 				Logger.LogMessage("Preloader finished");
 
 				Logger.Listeners.Remove(PreloaderLog);
@@ -213,8 +219,8 @@ namespace BepInEx.Preloader
 			if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
 				throw new Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!");
 
-			string entrypointType = Config.GetEntry("entrypoint-type", "Application", "Preloader");
-			string entrypointMethod = Config.GetEntry("entrypoint-method", ".cctor", "Preloader");
+			string entrypointType = ConfigEntrypointType.Value;
+			string entrypointMethod = ConfigEntrypointMethod.Value;
 
 			bool isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor";
 
@@ -269,14 +275,14 @@ namespace BepInEx.Preloader
 
 					var ins = il.Body.Instructions.First();
 
-					il.InsertBefore(ins, il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath
 					il.InsertBefore(ins,
-						il.Create(OpCodes
-							.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
+						il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath
 					il.InsertBefore(ins,
-						il.Create(OpCodes.Call,
-							initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true)
-					il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod));
+						il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
+					il.InsertBefore(ins,
+						il.Create(OpCodes.Call, initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true)
+					il.InsertBefore(ins,
+						il.Create(OpCodes.Call, startMethod));
 				}
 			}
 		}
@@ -286,10 +292,7 @@ namespace BepInEx.Preloader
 		/// </summary>
 		public static void AllocateConsole()
 		{
-			bool console = Utility.SafeParseBool(Config.GetEntry("console", "false", "BepInEx"));
-			bool shiftjis = Utility.SafeParseBool(Config.GetEntry("console-shiftjis", "false", "BepInEx"));
-
-			if (!console)
+			if (!ConfigConsoleEnabled.Value)
 				return;
 
 			try
@@ -298,7 +301,7 @@ namespace BepInEx.Preloader
 
 				var encoding = (uint)Encoding.UTF8.CodePage;
 
-				if (shiftjis)
+				if (ConfigConsoleShiftJis.Value)
 					encoding = 932;
 
 				ConsoleEncoding.ConsoleCodePage = encoding;
@@ -310,5 +313,60 @@ namespace BepInEx.Preloader
 				Logger.LogError(ex);
 			}
 		}
+
+		#region Config
+
+		private static ConfigWrapper<string> ConfigEntrypointAssembly;
+
+		private static ConfigWrapper<string> ConfigEntrypointType;
+
+		private static ConfigWrapper<string> ConfigEntrypointMethod;
+
+		private static ConfigWrapper<bool> ConfigPreloaderCOutLogging;
+
+		private static ConfigWrapper<bool> ConfigConsoleEnabled;
+
+		private static ConfigWrapper<bool> ConfigConsoleShiftJis;
+
+		private static void InitConfig()
+		{
+			ConfigEntrypointAssembly = ConfigFile.CoreConfig.Wrap(
+				"Preloader.Entrypoint",
+				"Assembly",
+				"The local filename of the assembly to target.",
+				"UnityEngine.dll");
+
+			ConfigEntrypointType = ConfigFile.CoreConfig.Wrap(
+				"Preloader.Entrypoint",
+				"Type",
+				"The name of the type in the entrypoint assembly to search for the entrypoint method.",
+				"Application");
+
+			ConfigEntrypointMethod = ConfigFile.CoreConfig.Wrap(
+				"Preloader.Entrypoint",
+				"Method",
+				"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.",
+				".cctor");
+
+			ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap(
+				"Logging",
+				"PreloaderConsoleOutRedirection",
+				"Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.",
+				true);
+
+			ConfigConsoleEnabled = ConfigFile.CoreConfig.Wrap(
+				"Logging.Console",
+				"Enabled",
+				"Enables showing a console for log output.",
+				false);
+
+			ConfigConsoleShiftJis = ConfigFile.CoreConfig.Wrap(
+				"Logging.Console",
+				"ShiftJisEncoding",
+				"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.",
+				false);
+		}
+
+		#endregion
 	}
 }

+ 4 - 2
BepInEx/BepInEx.csproj

@@ -46,9 +46,11 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Configuration\ConfigDefinition.cs" />
+    <Compile Include="Configuration\ConfigFile.cs" />
+    <Compile Include="Configuration\ConfigWrapper.cs" />
+    <Compile Include="Configuration\TomlTypeConverter.cs" />
     <Compile Include="Contract\Attributes.cs" />
-    <Compile Include="Config.cs" />
-    <Compile Include="ConfigWrapper.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.PInvoke.cs" />

+ 20 - 3
BepInEx/Bootstrap/Chainloader.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Reflection;
 using System.Text;
+using BepInEx.Configuration;
 using BepInEx.Logging;
 using UnityEngine;
 using UnityInjector.ConsoleUtil;
@@ -42,7 +43,7 @@ namespace BepInEx.Bootstrap
 			//Set vitals
 			Paths.SetExecutablePath(containerExePath);
 
-			Paths.SetPluginPath(Config.GetEntry("chainloader-plugins-directory", "plugins", "BepInEx"));
+			Paths.SetPluginPath(ConfigPluginsDirectory.Value);
 
 			//Start logging
 
@@ -64,7 +65,7 @@ namespace BepInEx.Bootstrap
 			if (!TraceLogSource.IsListening)
 				Logger.Sources.Add(TraceLogSource.CreateSource());
 
-			if (bool.Parse(Config.GetEntry("chainloader-log-unity-messages", "false", "BepInEx")))
+			if (ConfigUnityLogging.Value)
 				Logger.Sources.Add(new UnityLogSource());
 
 
@@ -109,7 +110,7 @@ namespace BepInEx.Bootstrap
 				var globalPluginTypes = TypeLoader.LoadTypes<BaseUnityPlugin>(Paths.PluginPath).ToList();
 
 				var selectedPluginTypes = globalPluginTypes
-				                          .Where(plugin =>
+										  .Where(plugin =>
 										  {
 											  //Ensure metadata exists
 											  var metadata = MetadataHelper.GetMetadata(plugin);
@@ -188,5 +189,21 @@ namespace BepInEx.Bootstrap
 
 			_loaded = true;
 		}
+
+		#region Config
+
+		private static ConfigWrapper<string> ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap(
+				"Paths",
+				"PluginsDirectory",
+				"The relative directory to the BepInEx folder where plugins are loaded.",
+				"plugins");
+
+		private static ConfigWrapper<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Wrap(
+				"Logging",
+				"UnityLogListening",
+				"Enables showing unity log messages in the BepInEx logging system.",
+				true);
+
+		#endregion
 	}
 }

+ 0 - 227
BepInEx/Config.cs

@@ -1,227 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.RegularExpressions;
-using BepInEx.Logging;
-
-namespace BepInEx
-{
-	/// <summary>
-	/// A helper class to handle persistent data.
-	/// </summary>
-	public static class Config
-	{
-		private static readonly Dictionary<string, Dictionary<string, string>> cache = new Dictionary<string, Dictionary<string, string>>();
-
-		private static string ConfigPath => Path.Combine(Paths.BepInExRootPath, "config.ini");
-
-		private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+");
-
-		private static void RaiseConfigReloaded()
-		{
-			ConfigReloaded?.Invoke();
-		}
-
-		/// <summary>
-		/// An event that is fired every time the config is reloaded.
-		/// </summary>
-		public static event Action ConfigReloaded;
-
-		/// <summary>
-		/// If enabled, writes the config to disk every time a value is set.
-		/// </summary>
-		public static bool SaveOnConfigSet { get; set; } = true;
-
-		static Config()
-		{
-			if (File.Exists(ConfigPath))
-			{
-				ReloadConfig();
-			}
-			else
-			{
-				SaveConfig();
-			}
-		}
-
-		/// <summary>
-		/// Returns the value of the key if found, otherwise returns the default value.
-		/// </summary>
-		/// <param name="key">The key to search for.</param>
-		/// <param name="defaultValue">The default value to return if the key is not found.</param>
-		/// <param name="section">The section of the config to search the key for.</param>
-		/// <returns>The value of the key.</returns>
-		public static string GetEntry(string key, string defaultValue = "", string section = "")
-		{
-			try
-			{
-				key = Sanitize(key);
-				section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section);
-
-				if (!cache.TryGetValue(section, out Dictionary<string, string> subdict))
-				{
-					SetEntry(key, defaultValue, section);
-					return defaultValue;
-				}
-
-				if (subdict.TryGetValue(key, out string value))
-					return value;
-
-				SetEntry(key, defaultValue, section);
-				return defaultValue;
-			}
-			catch (Exception ex)
-			{
-				Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to read config entry!");
-				Logger.LogError(ex);
-				return defaultValue;
-			}
-		}
-
-		/// <summary>
-		/// Reloads the config from disk. Unwritten changes are lost.
-		/// </summary>
-		public static void ReloadConfig()
-		{
-			cache.Clear();
-
-			string currentSection = "";
-
-			foreach (string rawLine in File.ReadAllLines(ConfigPath))
-			{
-				string line = rawLine.Trim();
-
-				bool commentIndex = line.StartsWith(";") || line.StartsWith("#");
-
-				if (commentIndex) //trim comment
-					continue;
-
-				if (line.StartsWith("[") && line.EndsWith("]")) //section
-				{
-					currentSection = line.Substring(1, line.Length - 2);
-					continue;
-				}
-
-				string[] split = line.Split('='); //actual config line
-				if (split.Length != 2)
-					continue; //empty/invalid line
-
-				if (!cache.ContainsKey(currentSection))
-					cache[currentSection] = new Dictionary<string, string>();
-
-				cache[currentSection][split[0]] = split[1];
-			}
-
-			RaiseConfigReloaded();
-		}
-
-		/// <summary>
-		/// Writes the config to disk.
-		/// </summary>
-		public static void SaveConfig()
-		{
-			using (StreamWriter writer = new StreamWriter(File.Create(ConfigPath), System.Text.Encoding.UTF8))
-				foreach (var sectionKv in cache)
-				{
-					writer.WriteLine($"[{sectionKv.Key}]");
-
-					foreach (var entryKv in sectionKv.Value)
-						writer.WriteLine($"{entryKv.Key}={entryKv.Value}");
-
-					writer.WriteLine();
-				}
-		}
-
-		/// <summary>
-		/// Sets the value of the key in the config.
-		/// </summary>
-		/// <param name="key">The key to set the value to.</param>
-		/// <param name="value">The value to set.</param>
-		public static void SetEntry(string key, string value, string section = "")
-		{
-			try
-			{
-				key = Sanitize(key);
-				section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section);
-
-				if (!cache.TryGetValue(section, out Dictionary<string, string> subdict))
-				{
-					subdict = new Dictionary<string, string>();
-					cache[section] = subdict;
-				}
-
-				subdict[key] = value;
-
-				if (SaveOnConfigSet)
-					SaveConfig();
-			}
-			catch (Exception ex)
-			{
-				Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to save config entry!");
-				Logger.LogError(ex);
-			}
-		}
-
-		/// <summary>
-		/// Returns wether a value is currently set.
-		/// </summary>
-		/// <param name="key">The key to check against</param>
-		/// <param name="section">The section to check in</param>
-		/// <returns>True if the key is present</returns>
-		public static bool HasEntry(string key, string section = "")
-		{
-			key = Sanitize(key);
-			section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section);
-
-			return cache.ContainsKey(section) && cache[section].ContainsKey(key);
-		}
-
-
-		/// <summary>
-		/// Removes a value from the config.
-		/// </summary>
-		/// <param name="key">The key to remove</param>
-		/// <param name="section">The section to remove from</param>
-		/// <returns>True if the key was removed</returns>
-		public static bool UnsetEntry(string key, string section = "")
-		{
-			key = Sanitize(key);
-			section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section);
-
-			if (!HasEntry(key, section))
-				return false;
-
-			cache[section].Remove(key);
-			return true;
-		}
-
-		/// <summary>
-		/// Replaces any potentially breaking input with underscores.
-		/// </summary>
-		/// <param name="text">The text to sanitize.</param>
-		/// <returns>Sanitized text.</returns>
-		public static string Sanitize(string text)
-		{
-			return sanitizeKeyRegex.Replace(text, "_");
-		}
-
-		#region Extensions
-
-		public static string GetEntry(this BaseUnityPlugin plugin, string key, string defaultValue = "")
-		{
-			return GetEntry(key, defaultValue, MetadataHelper.GetMetadata(plugin).GUID);
-		}
-
-		public static void SetEntry(this BaseUnityPlugin plugin, string key, string value)
-		{
-			SetEntry(key, value, MetadataHelper.GetMetadata(plugin).GUID);
-		}
-
-		public static bool HasEntry(this BaseUnityPlugin plugin, string key)
-		{
-			return HasEntry(key, MetadataHelper.GetMetadata(plugin).GUID);
-		}
-
-		#endregion Extensions
-	}
-}

+ 0 - 170
BepInEx/ConfigWrapper.cs

@@ -1,170 +0,0 @@
-using System;
-using System.ComponentModel;
-using BepInEx.Logging;
-
-namespace BepInEx
-{
-	public interface IConfigConverter<T>
-	{
-		string ConvertToString(T value);
-		T ConvertFromString(string str);
-	}
-
-	public class ConfigWrapper<T>
-	{
-		private readonly Func<string, T> _strToObj;
-		private readonly Func<T, string> _objToStr;
-		private readonly string _defaultStr;
-		private readonly T _default;
-		private T _lastValue;
-		private bool _lastValueSet;
-
-		public string Key { get; protected set; }
-
-		public string Section { get; protected set; }
-
-		public T Value
-		{
-			get { return GetValue(); }
-			set { SetValue(value); }
-		}
-
-		public ConfigWrapper(string key, T @default = default(T))
-		{
-			var cvt = TypeDescriptor.GetConverter(typeof(T));
-
-			if (!cvt.CanConvertFrom(typeof(string)))
-				throw new ArgumentException("Default TypeConverter can't convert from String");
-
-			if (!cvt.CanConvertTo(typeof(string)))
-				throw new ArgumentException("Default TypeConverter can't convert to String");
-
-			_strToObj = (str) => (T)cvt.ConvertFromInvariantString(str);
-			_objToStr = (obj) => cvt.ConvertToInvariantString(obj);
-
-			_defaultStr = _objToStr(@default);
-			_default = @default;
-			Key = key;
-		}
-
-		public ConfigWrapper(string key, Func<string, T> strToObj, Func<T, string> objToStr, T @default = default(T))
-		{
-			if (objToStr == null)
-				throw new ArgumentNullException("objToStr");
-
-			if (strToObj == null)
-				throw new ArgumentNullException("strToObj");
-
-			_strToObj = strToObj;
-			_objToStr = objToStr;
-
-			_defaultStr = _objToStr(@default);
-			Key = key;
-		}
-
-		public ConfigWrapper(string key, IConfigConverter<T> converter, T @default = default(T))
-			: this(key, converter.ConvertFromString, converter.ConvertToString, @default) { }
-
-
-		public ConfigWrapper(string key, BaseUnityPlugin plugin, T @default = default(T))
-			: this(key, @default)
-		{
-			Section = MetadataHelper.GetMetadata(plugin).GUID;
-		}
-
-		public ConfigWrapper(string key, BaseUnityPlugin plugin, Func<string, T> strToObj, Func<T, string> objToStr, T @default = default(T))
-			: this(key, strToObj, objToStr, @default)
-		{
-			Section = MetadataHelper.GetMetadata(plugin).GUID;
-		}
-
-		public ConfigWrapper(string key, BaseUnityPlugin plugin, IConfigConverter<T> converter, T @default = default(T))
-			: this(key, converter.ConvertFromString, converter.ConvertToString, @default)
-		{
-			Section = MetadataHelper.GetMetadata(plugin).GUID;
-		}
-
-		public ConfigWrapper(string key, string section, T @default = default(T))
-			: this(key, @default)
-		{
-			Section = section;
-		}
-
-		public ConfigWrapper(string key, string section, Func<string, T> strToObj, Func<T, string> objToStr, T @default = default(T))
-			: this(key, strToObj, objToStr, @default)
-		{
-			Section = section;
-		}
-
-		public ConfigWrapper(string key, string section, IConfigConverter<T> converter, T @default = default(T))
-			: this(key, converter.ConvertFromString, converter.ConvertToString, @default)
-		{
-			Section = section;
-		}
-
-		protected virtual bool GetKeyExists()
-		{
-			return Config.HasEntry(Key, Section);
-		}
-
-		protected virtual T GetValue()
-		{
-			try
-			{
-				var strVal = Config.GetEntry(Key, _defaultStr, Section);
-				var obj = _strToObj(strVal);
-
-				// Always update in case config was changed from outside
-				_lastValue = obj;
-				_lastValueSet = true;
-
-				return obj;
-			}
-			catch (Exception ex)
-			{
-				Logger.LogError("ConfigWrapper Get Converter Exception: " + ex.Message);
-				return _default;
-			}
-		}
-
-		protected virtual void SetValue(T value)
-		{
-			try
-			{
-				// Always write just in case config was changed from outside
-				var strVal = _objToStr(value);
-				Config.SetEntry(Key, strVal, Section);
-
-				if (_lastValueSet && Equals(_lastValue, value))
-					return;
-
-				_lastValue = value;
-				_lastValueSet = true;
-
-				OnSettingChanged();
-			}
-			catch (Exception ex)
-			{
-				Logger.LogError("ConfigWrapper Set Converter Exception: " + ex.Message);
-			}
-		}
-
-		public void Clear()
-		{
-			Config.UnsetEntry(Key, Section);
-
-			_lastValueSet = false;
-			OnSettingChanged();
-		}
-
-		/// <summary>
-		/// Fired when the setting is changed. Does not detect changes made outside from this object.
-		/// </summary>
-		public event EventHandler SettingChanged;
-
-		private void OnSettingChanged()
-		{
-			SettingChanged?.Invoke(this, EventArgs.Empty);
-		}
-	}
-}

+ 51 - 0
BepInEx/Configuration/ConfigDefinition.cs

@@ -0,0 +1,51 @@
+namespace BepInEx.Configuration
+{
+	public class ConfigDefinition
+	{
+		public string Section { get; }
+
+		public string Key { get; }
+
+		public string Description { get; internal set; }
+
+		public ConfigDefinition(string section, string key, string description = null)
+		{
+			Key = key;
+			Section = section;
+
+			Description = description;
+		}
+
+		public override bool Equals(object obj)
+		{
+			if (ReferenceEquals(null, obj))
+				return false;
+			if (ReferenceEquals(this, obj))
+				return true;
+			if (obj.GetType() != this.GetType())
+				return false;
+
+			if (!(obj is ConfigDefinition other))
+				return false;
+
+			return string.Equals(Key, other.Key)
+			       && string.Equals(Section, other.Section);
+		}
+
+		public override int GetHashCode()
+		{
+			unchecked
+			{
+				int hashCode = Key != null ? Key.GetHashCode() : 0;
+				hashCode = (hashCode * 397) ^ (Section != null ? Section.GetHashCode() : 0);
+				return hashCode;
+			}
+		}
+
+		public static bool operator ==(ConfigDefinition left, ConfigDefinition right)
+			=> Equals(left, right);
+
+		public static bool operator !=(ConfigDefinition left, ConfigDefinition right)
+			=> !Equals(left, right);
+	}
+}

+ 148 - 0
BepInEx/Configuration/ConfigFile.cs

@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace BepInEx.Configuration
+{
+	/// <summary>
+	/// A helper class to handle persistent data.
+	/// </summary>
+	public class ConfigFile
+	{
+		private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+");
+
+		internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath);
+
+		protected internal Dictionary<ConfigDefinition, string> Cache { get; } = new Dictionary<ConfigDefinition, string>();
+
+		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions => Cache.Keys.ToList().AsReadOnly();
+
+		/// <summary>
+		/// An event that is fired every time the config is reloaded.
+		/// </summary>
+		public event EventHandler ConfigReloaded;
+
+		public string ConfigFilePath { get; }
+
+		/// <summary>
+		/// If enabled, writes the config to disk every time a value is set.
+		/// </summary>
+		public bool SaveOnConfigSet { get; set; } = true;
+
+		public ConfigFile(string configPath)
+		{
+			ConfigFilePath = configPath;
+
+			if (File.Exists(ConfigFilePath))
+			{
+				Reload();
+			}
+			else
+			{
+				Save();
+			}
+		}
+
+		private object _ioLock = new object();
+
+		/// <summary>
+		/// Reloads the config from disk. Unsaved changes are lost.
+		/// </summary>
+		public void Reload()
+		{
+			lock (_ioLock)
+			{
+				Dictionary<ConfigDefinition, string> descriptions = Cache.ToDictionary(x => x.Key, x => x.Key.Description);
+
+				string currentSection = "";
+
+				foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
+				{
+					string line = rawLine.Trim();
+
+					if (line.StartsWith("#")) //comment
+						continue;
+
+					if (line.StartsWith("[") && line.EndsWith("]")) //section
+					{
+						currentSection = line.Substring(1, line.Length - 2);
+						continue;
+					}
+
+					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();
+
+					var definition = new ConfigDefinition(currentSection, currentKey);
+
+					if (descriptions.ContainsKey(definition))
+						definition.Description = descriptions[definition];
+
+					Cache[definition] = currentValue;
+				}
+
+				ConfigReloaded?.Invoke(this, EventArgs.Empty);
+			}
+		}
+
+		/// <summary>
+		/// Writes the config to disk.
+		/// </summary>
+		public void Save()
+		{
+			lock (_ioLock)
+			{
+				if (!Directory.Exists(Paths.ConfigPath))
+					Directory.CreateDirectory(Paths.ConfigPath);
+
+				using (StreamWriter writer = new StreamWriter(File.Create(ConfigFilePath), System.Text.Encoding.UTF8))
+					foreach (var sectionKv in Cache.GroupBy(x => x.Key.Section).OrderBy(x => x.Key))
+					{
+						writer.WriteLine($"[{sectionKv.Key}]");
+
+						foreach (var entryKv in sectionKv)
+						{
+							writer.WriteLine();
+
+							if (!string.IsNullOrEmpty(entryKv.Key.Description))
+								writer.WriteLine($"# {entryKv.Key.Description.Replace("\n", "\n# ")}");
+
+							writer.WriteLine($"{entryKv.Key.Key} = {entryKv.Value}");
+						}
+
+						writer.WriteLine();
+					}
+			}
+		}
+
+		public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
+		{
+			if (!Cache.ContainsKey(configDefinition))
+			{
+				Cache.Add(configDefinition, TomlTypeConverter.ConvertToString(defaultValue));
+				Save();
+			}
+			else
+			{
+				var original = Cache.Keys.First(x => x.Equals(configDefinition));
+
+				if (original.Description != configDefinition.Description)
+				{
+					original.Description = configDefinition.Description;
+					Save();
+				}
+			}
+
+			return new ConfigWrapper<T>(this, configDefinition);
+		}
+
+		public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
+			=> Wrap<T>(new ConfigDefinition(section, key, description), defaultValue);
+	}
+}

+ 39 - 0
BepInEx/Configuration/ConfigWrapper.cs

@@ -0,0 +1,39 @@
+using System;
+
+namespace BepInEx.Configuration
+{
+	public class ConfigWrapper<T>
+	{
+		public ConfigDefinition Definition { get; protected set; }
+
+		public ConfigFile ConfigFile { get; protected set; }
+
+		/// <summary>
+		/// Fired when the setting is changed. Does not detect changes made outside from this object.
+		/// </summary>
+		public event EventHandler SettingChanged;
+
+		public T Value
+		{
+			get => TomlTypeConverter.ConvertToValue<T>(ConfigFile.Cache[Definition]);
+			set
+			{
+				ConfigFile.Cache[Definition] = TomlTypeConverter.ConvertToString(value);
+
+				if (ConfigFile.SaveOnConfigSet)
+					ConfigFile.Save();
+
+				SettingChanged?.Invoke(this, EventArgs.Empty);
+			}
+		}
+
+		public ConfigWrapper(ConfigFile configFile, ConfigDefinition definition)
+		{
+			if (!TomlTypeConverter.SupportedTypes.Contains(typeof(T)))
+				throw new ArgumentException("Unsupported config wrapper type");
+
+			ConfigFile = configFile;
+			Definition = definition;
+		}
+	}
+}

+ 63 - 0
BepInEx/Configuration/TOMLTypeConverter.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections.ObjectModel;
+
+namespace BepInEx.Configuration
+{
+	internal static class TomlTypeConverter
+	{
+		public static ReadOnlyCollection<Type> SupportedTypes { get; } = new ReadOnlyCollection<Type>(new[]
+		{
+			typeof(string),
+			typeof(int),
+			typeof(bool)
+		});
+
+		public static string ConvertToString(object value)
+		{
+			Type valueType = value.GetType();
+
+			if (!SupportedTypes.Contains(valueType))
+				throw new InvalidOperationException($"Cannot convert from type {valueType}");
+
+			if (value is string s)
+			{
+				return s;
+			}
+
+			if (value is int i)
+			{
+				return i.ToString();
+			}
+
+			if (value is bool b)
+			{
+				return b.ToString().ToLowerInvariant();
+			}
+
+			throw new NotImplementedException("Supported type does not have a converter");
+		}
+
+		public static T ConvertToValue<T>(string value)
+		{
+			if (!SupportedTypes.Contains(typeof(T)))
+				throw new InvalidOperationException($"Cannot convert to type {typeof(T)}");
+
+			if (typeof(T) == typeof(string))
+			{
+				return (T)(object)value;
+			}
+
+			if (typeof(T) == typeof(int))
+			{
+				return (T)(object)int.Parse(value);
+			}
+
+			if (typeof(T) == typeof(bool))
+			{
+				return (T)(object)bool.Parse(value);
+			}
+
+			throw new NotImplementedException("Supported type does not have a converter");
+		}
+	}
+}

+ 6 - 1
BepInEx/Contract/BaseUnityPlugin.cs

@@ -1,4 +1,5 @@
-using BepInEx.Logging;
+using BepInEx.Configuration;
+using BepInEx.Logging;
 using UnityEngine;
 
 namespace BepInEx
@@ -10,11 +11,15 @@ namespace BepInEx
 	{
 		protected ManualLogSource Logger { get; }
 
+		protected ConfigFile Config { get; }
+
 		protected BaseUnityPlugin()
 		{
 			var metadata = MetadataHelper.GetMetadata(this);
 
 			Logger = Logging.Logger.CreateLogSource(metadata.Name);
+
+			Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".ini"));
 		}
 	}
 }

+ 12 - 0
BepInEx/Logging/ConsoleLogListener.cs

@@ -1,4 +1,5 @@
 using System;
+using BepInEx.Configuration;
 using BepInEx.ConsoleUtil;
 
 namespace BepInEx.Logging
@@ -8,8 +9,13 @@ namespace BepInEx.Logging
 	/// </summary>
 	public class ConsoleLogListener : ILogListener
 	{
+		protected LogLevel DisplayedLogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), ConfigConsoleDisplayedLevel.Value, true);
+
 		public void LogEvent(object sender, LogEventArgs eventArgs)
 		{
+			if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel)
+				return;
+
 			string log = $"[{eventArgs.Level, -7}:{((ILogSource)sender).SourceName, 10}] {eventArgs.Data}\r\n";
 
 			Kon.ForegroundColor = eventArgs.Level.GetConsoleColor();
@@ -18,5 +24,11 @@ namespace BepInEx.Logging
 		}
 
 		public void Dispose() { }
+
+		private static ConfigWrapper<string> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap(
+			"Logging.Console",
+			"DisplayedLogLevel",
+			"Only displays the specified log level and above in the console output.",
+			"Info");
 	}
 }

+ 17 - 5
BepInEx/Paths.cs

@@ -14,11 +14,13 @@ namespace BepInEx
 			ProcessName = Path.GetFileNameWithoutExtension(executablePath);
 			GameRootPath = Path.GetDirectoryName(executablePath);
 			ManagedPath = Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed");
-			BepInExRootPath = Utility.CombinePaths(GameRootPath, "BepInEx");
-			PluginPath = Utility.CombinePaths(BepInExRootPath, "plugins");
-			PatcherPluginPath = Utility.CombinePaths(BepInExRootPath, "patchers");
-			BepInExAssemblyDirectory = Utility.CombinePaths(BepInExRootPath, "core");
-			BepInExAssemblyPath = Utility.CombinePaths(BepInExAssemblyDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.dll");
+			BepInExRootPath = Path.Combine(GameRootPath, "BepInEx");
+			ConfigPath = Path.Combine(BepInExRootPath, "config");
+			BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg");
+			PluginPath = Path.Combine(BepInExRootPath, "plugins");
+			PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers");
+			BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core");
+			BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.dll");
 		}
 
 		internal static void SetPluginPath(string pluginPath)
@@ -57,6 +59,16 @@ namespace BepInEx
 		public static string ManagedPath { get; private set; }
 
 		/// <summary>
+		///		The path to the config directory.
+		/// </summary>
+		public static string ConfigPath { get; private set; }
+
+		/// <summary>
+		///		The path to the global BepInEx configuration file.
+		/// </summary>
+		public static string BepInExConfigPath { get; private set; }
+
+		/// <summary>
 		///     The path to the patcher plugin folder which resides in the BepInEx folder.
 		/// </summary>
 		public static string PatcherPluginPath { get; private set; }

+ 1 - 1
BepInEx/Utility.cs

@@ -46,7 +46,7 @@ namespace BepInEx
 		/// <returns>True if the value parameter is null or empty, or if value consists exclusively of white-space characters.</returns>
 		public static bool IsNullOrWhiteSpace(this string self)
 		{
-			return self == null || self.Trim().Length == 0;
+			return self == null || self.All(char.IsWhiteSpace);
 		}
 
 		public static IEnumerable<TNode> TopologicalSort<TNode>(IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> dependencySelector)