Browse Source

Split GetSetting and AddSetting; Roll back to separate ConfigWrapper and ConfigEntry for better compatibility and naming

ManlyMarco 4 years ago
parent
commit
f9eee41b33

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

@@ -277,17 +277,17 @@ namespace BepInEx.Preloader.Patching
 
 		#region Config
 
-		private static readonly ConfigWrapper<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.AddSetting(
 			"Preloader", "DumpAssemblies",
 			false,
 			new ConfigDescription("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 ConfigWrapper<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.AddSetting(
 			"Preloader", "LoadDumpedAssemblies",
 			false,
 			new ConfigDescription("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 ConfigWrapper<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.AddSetting(
 			"Preloader", "BreakBeforeLoadAssemblies",
 			false,
 			new ConfigDescription("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."));

+ 6 - 6
BepInEx.Preloader/Preloader.cs

@@ -237,32 +237,32 @@ namespace BepInEx.Preloader
 
 		#region Config
 
-		private static readonly ConfigWrapper<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.AddSetting(
 			"Preloader.Entrypoint", "Assembly",
 			IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll",
 			new ConfigDescription("The local filename of the assembly to target."));
 
-		private static readonly ConfigWrapper<string> ConfigEntrypointType = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointType = ConfigFile.CoreConfig.AddSetting(
 			"Preloader.Entrypoint", "Type",
 			"Application",
 			new ConfigDescription("The name of the type in the entrypoint assembly to search for the entrypoint method."));
 
-		private static readonly ConfigWrapper<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.AddSetting(
 			"Preloader.Entrypoint", "Method",
 			".cctor",
 			new ConfigDescription("The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."));
 
-		private static readonly ConfigWrapper<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.AddSetting(
 			"Preloader", "ApplyRuntimePatches",
 			true,
 			new ConfigDescription("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 ConfigWrapper<bool> ConfigShimHarmony = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigShimHarmony = ConfigFile.CoreConfig.AddSetting(
 			"Preloader", "ShimHarmonySupport",
 			!Utility.CLRSupportsDynamicAssemblies,
 			new ConfigDescription("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 ConfigWrapper<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.AddSetting(
 			"Logging", "PreloaderConsoleOutRedirection",
 			true,
 			new ConfigDescription("Redirects text from Console.Out during preloader patch loading to the BepInEx logging system."));

+ 3 - 1
BepInEx.sln.DotSettings

@@ -211,5 +211,7 @@
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 5 - 5
BepInEx/Bootstrap/Chainloader.cs

@@ -334,27 +334,27 @@ namespace BepInEx.Bootstrap
 		#region Config
 
 
-		private static readonly ConfigWrapper<bool> ConfigUnityLogging = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.AddSetting(
 			"Logging", "UnityLogListening",
 			true,
 			new ConfigDescription("Enables showing unity log messages in the BepInEx logging system."));
 
-		private static readonly ConfigWrapper<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Disk", "WriteUnityLog",
 			false,
 			new ConfigDescription("Include unity log messages in log file output."));
 
-		private static readonly ConfigWrapper<bool> ConfigDiskAppend = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Disk", "AppendLog",
 			false,
 			new ConfigDescription("Appends to the log file instead of overwriting, on game startup."));
 
-		private static readonly ConfigWrapper<bool> ConfigDiskLogging = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Disk", "Enabled",
 			true,
 			new ConfigDescription("Enables writing log messages to disk."));
 
-		private static readonly ConfigWrapper<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Disk", "DisplayedLogLevel",
 			LogLevel.Info,
 			new ConfigDescription("Only displays the specified log level and above in the console output."));

+ 1 - 1
BepInEx/Bootstrap/TypeLoader.cs

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

+ 1 - 1
BepInEx/Configuration/AcceptableValueBase.cs

@@ -31,6 +31,6 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Get the string for use in config files.
 		/// </summary>
-		public abstract string ToSerializedString();
+		public abstract string ToDescriptionString();
 	}
 }

+ 1 - 1
BepInEx/Configuration/AcceptableValueList.cs

@@ -41,7 +41,7 @@ namespace BepInEx.Configuration
 		}
 
 		/// <inheritdoc />
-		public override string ToSerializedString()
+		public override string ToDescriptionString()
 		{
 			return "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray());
 		}

+ 1 - 1
BepInEx/Configuration/AcceptableValueRange.cs

@@ -51,7 +51,7 @@ namespace BepInEx.Configuration
 		}
 
 		/// <inheritdoc />
-		public override string ToSerializedString()
+		public override string ToDescriptionString()
 		{
 			return $"# Acceptable value range: From {MinValue} to {MaxValue}";
 		}

+ 2 - 5
BepInEx/Configuration/ConfigDescription.cs

@@ -36,11 +36,8 @@ namespace BepInEx.Configuration
 		public object[] Tags { get; }
 
 		/// <summary>
-		/// Convert the description object into a form suitable for writing into a config file.
+		/// An empty description.
 		/// </summary>
-		public string ToSerializedString()
-		{
-			return $"## {Description.Replace("\n", "\n## ")}";
-		}
+		public static ConfigDescription Empty { get; } = new ConfigDescription("");
 	}
 }

+ 61 - 24
BepInEx/Configuration/ConfigEntryBase.cs

@@ -6,6 +6,52 @@ using BepInEx.Logging;
 namespace BepInEx.Configuration
 {
 	/// <summary>
+	/// Provides access to a single setting inside of a <see cref="Configuration.ConfigFile"/>.
+	/// </summary>
+	/// <typeparam name="T">Type of the setting.</typeparam>
+	public sealed class ConfigEntry<T> : ConfigEntryBase
+	{
+		/// <summary>
+		/// Fired when the setting is changed. Does not detect changes made outside from this object.
+		/// </summary>
+		public event EventHandler SettingChanged;
+
+		private T _typedValue;
+
+		/// <summary>
+		/// Value of this setting.
+		/// </summary>
+		public T Value
+		{
+			get => _typedValue;
+			set
+			{
+				value = ClampValue(value);
+				if (Equals(_typedValue, value))
+					return;
+
+				_typedValue = value;
+				OnSettingChanged(this);
+			}
+		}
+
+		/// <inheritdoc />
+		public override object BoxedValue
+		{
+			get => Value;
+			set => Value = (T)value;
+		}
+
+		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, ConfigDescription configDescription) : base(configFile, definition, typeof(T), defaultValue, configDescription)
+		{
+			configFile.SettingChanged += (sender, args) =>
+			{
+				if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args);
+			};
+		}
+	}
+
+	/// <summary>
 	/// Container for a single setting of a <see cref="Configuration.ConfigFile"/>. 
 	/// Each config entry is linked to one config file.
 	/// </summary>
@@ -14,15 +60,20 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Types of defaultValue and definition.AcceptableValues have to be the same as settingType.
 		/// </summary>
-		internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue)
+		internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue, ConfigDescription configDescription)
 		{
 			ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile));
 			Definition = definition ?? throw new ArgumentNullException(nameof(definition));
 			SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType));
 
-			// Free type check
-			BoxedValue = defaultValue;
+			Description = configDescription ?? ConfigDescription.Empty;
+			if (Description.AcceptableValues != null && !SettingType.IsAssignableFrom(Description.AcceptableValues.ValueType))
+				throw new ArgumentException("configDescription.AcceptableValues is for a different type than the type of this setting");
+
 			DefaultValue = defaultValue;
+
+			// Free type check and automatically calls ClampValue in case AcceptableValues were provided
+			BoxedValue = defaultValue;
 		}
 
 		/// <summary>
@@ -38,7 +89,7 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Description / metadata of this setting.
 		/// </summary>
-		public ConfigDescription Description { get; private set; }
+		public ConfigDescription Description { get; }
 
 		/// <summary>
 		/// Type of the <see cref="BoxedValue"/> that this setting holds.
@@ -75,28 +126,16 @@ namespace BepInEx.Configuration
 			}
 			catch (Exception e)
 			{
-				Logging.Logger.Log(LogLevel.Warning, $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}");
+				Logger.Log(LogLevel.Warning, $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}");
 			}
 		}
 
-		internal void SetDescription(ConfigDescription configDescription)
-		{
-			if (configDescription == null) throw new ArgumentNullException(nameof(configDescription));
-			if (configDescription.AcceptableValues != null && !SettingType.IsAssignableFrom(configDescription.AcceptableValues.ValueType))
-				throw new ArgumentException("configDescription.AcceptableValues is for a different type than the type of this setting");
-
-			Description = configDescription;
-
-			// Automatically calls ClampValue in case it changed
-			BoxedValue = BoxedValue;
-		}
-
 		/// <summary>
 		/// If necessary, clamp the value to acceptable value range. T has to be equal to settingType.
 		/// </summary>
 		protected T ClampValue<T>(T value)
 		{
-			if (Description?.AcceptableValues != null)
+			if (Description.AcceptableValues != null)
 				return (T)Description.AcceptableValues.Clamp(value);
 			return value;
 		}
@@ -114,18 +153,16 @@ namespace BepInEx.Configuration
 		/// </summary>
 		public void WriteDescription(StreamWriter writer)
 		{
-			bool hasDescription = Description != null;
-
-			if (hasDescription)
-				writer.WriteLine(Description.ToSerializedString());
+			if (!string.IsNullOrEmpty(Description.Description))
+				writer.WriteLine($"## {Description.Description.Replace("\n", "\n## ")}");
 
 			writer.WriteLine("# Setting type: " + SettingType.Name);
 
 			writer.WriteLine("# Default value: " + DefaultValue);
 
-			if (hasDescription && Description.AcceptableValues != null)
+			if (Description.AcceptableValues != null)
 			{
-				writer.WriteLine(Description.AcceptableValues.ToSerializedString());
+				writer.WriteLine(Description.AcceptableValues.ToDescriptionString());
 			}
 			else if (SettingType.IsEnum)
 			{

+ 48 - 38
BepInEx/Configuration/ConfigFile.cs

@@ -9,7 +9,7 @@ using BepInEx.Logging;
 namespace BepInEx.Configuration
 {
 	/// <summary>
-	/// A helper class to handle persistent data.
+	/// A helper class to handle persistent data. All public methods are thread-safe.
 	/// </summary>
 	public class ConfigFile
 	{
@@ -38,7 +38,7 @@ namespace BepInEx.Configuration
 
 		/// <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="GetSetting{T}(ConfigDefinition,T,ConfigDescription)"/> 
+		/// If you want to access and modify an existing setting then use <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/> 
 		/// instead with no description.
 		/// </summary>
 		public ConfigEntryBase[] GetConfigEntries()
@@ -192,45 +192,46 @@ namespace BepInEx.Configuration
 		#region Wraps
 
 		/// <summary>
-		/// Create a new setting or access one of the existing ones. The setting is saved to drive and loaded automatically.
-		/// If you are the creator of the setting, provide a ConfigDescription object to give user information about the setting.
-		/// If you are using a setting created by another plugin/class, do not provide any ConfigDescription.
+		/// 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>
+		public ConfigEntry<T> GetSetting<T>(string section, string key)
+		{
+			lock (_ioLock)
+			{
+				Entries.TryGetValue(new ConfigDefinition(section, key), out var entry);
+				return (ConfigEntry<T>)entry;
+			}
+		}
+
+		/// <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.</param>
-		public ConfigWrapper<T> GetSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
+		public ConfigEntry<T> AddSetting<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.");
+
 				try
 				{
 					_disableSaving = true;
 
-					Entries.TryGetValue(configDefinition, out var existingEntry);
-
-					if (existingEntry != null && !(existingEntry is ConfigWrapper<T>))
-						throw new ArgumentException("The defined setting already exists with a different setting type - " + existingEntry.SettingType.Name);
-
-					var entry = (ConfigWrapper<T>)existingEntry;
-
-					if (entry == null)
-					{
-						entry = new ConfigWrapper<T>(this, configDefinition, defaultValue);
-						Entries[configDefinition] = entry;
-					}
-
-					if (configDescription != null)
-					{
-						if (entry.Description != null)
-							Logger.Log(LogLevel.Warning, $"Tried to add configDescription to setting {configDefinition} when it already had one defined. Only add configDescription once or a random one will be used.");
-						else
-							entry.SetDescription(configDescription);
-					}
+					var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription);
+					Entries[configDefinition] = entry;
 
 					if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue))
 					{
@@ -252,27 +253,36 @@ namespace BepInEx.Configuration
 		}
 
 		/// <summary>
-		/// Create a new setting or access one of the existing ones. The setting is saved to drive and loaded automatically.
-		/// If you are the creator of the setting, provide a ConfigDescription object to give user information about the setting.
-		/// If you are using a setting created by another plugin/class, do not provide any ConfigDescription.
+		/// 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.</param>
-		public ConfigWrapper<T> GetSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
-			=> GetSetting(new ConfigDefinition(section, key), defaultValue, configDescription);
+		public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
+			=> AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription);
 
-		/// <inheritdoc cref="GetSetting{T}(string,string,T,ConfigDescription)"/>
-		[Obsolete("Use GetSetting instead")]
+		/// <summary>
+		/// Access a setting. Use AddSetting and GetSetting instead.
+		/// </summary>
+		[Obsolete("Use AddSetting and GetSetting instead")]
 		public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
-			=> GetSetting(new ConfigDefinition(section ?? "", key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
+		{
+			lock (_ioLock)
+			{
+				var setting = GetSetting<T>(section, key) ?? AddSetting(section, key, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
+				return new ConfigWrapper<T>(setting);
+			}
+		}
 
-		/// <inheritdoc cref="GetSetting{T}(ConfigDefinition,T,ConfigDescription)"/>
-		[Obsolete("Use GetSetting instead")]
+		/// <summary>
+		/// Access a setting. Use AddSetting and GetSetting instead.
+		/// </summary>
+		[Obsolete("Use AddSetting and GetSetting instead")]
 		public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
-			=> GetSetting(configDefinition, defaultValue);
+			=> Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
 
 		#endregion
 
@@ -289,7 +299,7 @@ namespace BepInEx.Configuration
 		public event EventHandler<SettingChangedEventArgs> SettingChanged;
 
 		internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase)
-		{
+		{ThreadingHelper.SynchronizingObject.InvokeRequired
 			if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase));
 
 			if (SaveOnConfigSet)

+ 23 - 22
BepInEx/Configuration/ConfigWrapper.cs

@@ -6,44 +6,45 @@ 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>
-	public sealed class ConfigWrapper<T> : ConfigEntryBase
+	[Obsolete("Use ConfigFile from new AddSetting overloads instead")]
+	public sealed class ConfigWrapper<T>
 	{
 		/// <summary>
+		/// Entry of this setting in the <see cref="Configuration.ConfigFile"/>.
+		/// </summary>
+		public ConfigEntry<T> ConfigEntry { get; }
+
+		/// <summary>
+		/// Unique definition of this setting.
+		/// </summary>
+		public ConfigDefinition Definition => ConfigEntry.Definition;
+
+		/// <summary>
+		/// Config file this setting is inside of.
+		/// </summary>
+		public ConfigFile ConfigFile => ConfigEntry.ConfigFile;
+
+		/// <summary>
 		/// Fired when the setting is changed. Does not detect changes made outside from this object.
 		/// </summary>
 		public event EventHandler SettingChanged;
 
-		private T _typedValue;
-
 		/// <summary>
 		/// Value of this setting.
 		/// </summary>
 		public T Value
 		{
-			get => _typedValue;
-			set
-			{
-				value = ClampValue(value);
-				if (Equals(_typedValue, value))
-					return;
-
-				_typedValue = value;
-				OnSettingChanged(this);
-			}
+			get => ConfigEntry.Value;
+			set => ConfigEntry.Value = value;
 		}
 
-		/// <inheritdoc />
-		public override object BoxedValue
+		internal ConfigWrapper(ConfigEntry<T> configEntry)
 		{
-			get => Value;
-			set => Value = (T)value;
-		}
+			ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry));
 
-		internal ConfigWrapper(ConfigFile configFile, ConfigDefinition definition, T defaultValue) : base(configFile, definition, typeof(T), defaultValue)
-		{
-			configFile.SettingChanged += (sender, args) =>
+			configEntry.ConfigFile.SettingChanged += (sender, args) =>
 			{
-				if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args);
+				if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args);
 			};
 		}
 	}

+ 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.GetSetting{T}(ConfigDefinition,T,ConfigDescription)"/> 
+	/// Can be used as a value of a setting in <see cref="ConfigFile.AddSetting{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 ConfigWrapper<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.GetSetting(
+		public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Console", "Enabled",
 			false,
 			new ConfigDescription("Enables showing a console for log output."));
 
-		public static readonly ConfigWrapper<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.GetSetting(
+		public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Console", "ShiftJisEncoding",
 			false,
 			new ConfigDescription("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 ConfigWrapper<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting(
+		private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
 			"Logging.Console","DisplayedLogLevel",
 			LogLevel.Info,
 			new ConfigDescription("Only displays the specified log level and above in the console output."));

+ 17 - 17
BepInExTests/Configuration/ConfigFileTests.cs

@@ -46,7 +46,7 @@ namespace BepInEx.Configuration.Tests
 		{
 			var c = MakeConfig();
 
-			var w = c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
 			Assert.AreEqual(w.Value, 1);
-			var w2 = c.GetSetting("Cat", "Key2", 0, new ConfigDescription("Test"));
+			var w2 = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test"));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<float>(1, 2)));
+			c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(1, 2)));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList<string>("lel", "kek", "wew", "why")));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift));
+			var w = c.AddSetting("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.GetSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test"));
+			var w = c.AddSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test"));
 
 			Assert.AreEqual("", w.ConfigEntry.GetSerializedValue());
 
@@ -280,7 +280,7 @@ namespace BepInEx.Configuration.Tests
 			const string testVal = "new line\n test \t\0";
 
 			var c = MakeConfig();
-			var w = c.GetSetting("Cat", "Key", testVal, new ConfigDescription("Test"));
+			var w = c.AddSetting("Cat", "Key", testVal, new ConfigDescription("Test"));
 
 			Assert.AreEqual(testVal, w.Value);
 			Assert.IsFalse(w.ConfigEntry.GetSerializedValue().Any(x => x == '\n'));
@@ -305,7 +305,7 @@ namespace BepInEx.Configuration.Tests
 				File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n");
 				c.Reload();
 
-				var w = c.GetSetting("Cat", "Key", "", new ConfigDescription("Test"));
+				var w = c.AddSetting("Cat", "Key", "", new ConfigDescription("Test"));
 
 				Assert.AreEqual(unescaped, w.Value);
 

+ 1 - 1
submodules/MonoMod

@@ -1 +1 @@
-Subproject commit 9462a0f75b669606b59a4648a3461338d2be32c2
+Subproject commit a70072cdf759ac0cfa80991fcd2cca67d3eec130