Explorar o código

Refactor ConfigEntry to be generic; Remove file watching

ManlyMarco %!s(int64=4) %!d(string=hai) anos
pai
achega
5aa47a1ba1

+ 15 - 3
BepInEx/Configuration/AcceptableValueBase.cs

@@ -1,10 +1,17 @@
-namespace BepInEx.Configuration
+using System;
+
+namespace BepInEx.Configuration
 {
 	/// <summary>
-	/// Base type of all classes represeting and enforcing acceptable values of config settings.
+	/// Base type of all classes representing and enforcing acceptable values of config settings.
 	/// </summary>
 	public abstract class AcceptableValueBase
 	{
+		protected AcceptableValueBase(Type valueType)
+		{
+			ValueType = valueType;
+		}
+
 		/// <summary>
 		/// Change the value to be acceptable, if it's not already.
 		/// </summary>
@@ -16,8 +23,13 @@
 		public abstract bool IsValid(object value);
 
 		/// <summary>
+		/// Type of the supported values.
+		/// </summary>
+		public Type ValueType { get; } 
+
+		/// <summary>
 		/// Get the string for use in config files.
 		/// </summary>
 		public abstract string ToSerializedString();
 	}
-}
+}

+ 2 - 2
BepInEx/Configuration/AcceptableValueList.cs

@@ -14,7 +14,7 @@ namespace BepInEx.Configuration
 		/// Specify the list of acceptable values for a setting.
 		/// If the setting does not equal any of the values, it will be set to the first one.
 		/// </summary>
-		public AcceptableValueList(params T[] acceptableValues)
+		public AcceptableValueList(params T[] acceptableValues) : base(typeof(T))
 		{
 			if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues));
 			if (acceptableValues.Length == 0) throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues));
@@ -43,4 +43,4 @@ namespace BepInEx.Configuration
 			return "# Acceptable values: " + string.Join(", ", _acceptableValues.Select(x => x.ToString()).ToArray());
 		}
 	}
-}
+}

+ 2 - 2
BepInEx/Configuration/AcceptableValueRange.cs

@@ -9,7 +9,7 @@ namespace BepInEx.Configuration
 	{
 		/// <param name="minValue">Lowest acceptable value</param>
 		/// <param name="maxValue">Highest acceptable value</param>
-		public AcceptableValueRange(T minValue, T maxValue)
+		public AcceptableValueRange(T minValue, T maxValue) : base(typeof(T))
 		{
 			if (maxValue == null)
 				throw new ArgumentNullException(nameof(maxValue));
@@ -56,4 +56,4 @@ namespace BepInEx.Configuration
 			return $"# Acceptable value range: From {MinValue} to {MaxValue}";
 		}
 	}
-}
+}

+ 1 - 1
BepInEx/Configuration/ConfigDescription.cs

@@ -3,7 +3,7 @@
 namespace BepInEx.Configuration
 {
 	/// <summary>
-	/// Metadata of a <see cref="ConfigEntry"/>.
+	/// Metadata of a <see cref="ConfigEntryBase"/>.
 	/// </summary>
 	public class ConfigDescription
 	{

+ 75 - 143
BepInEx/Configuration/ConfigEntry.cs

@@ -9,50 +9,22 @@ namespace BepInEx.Configuration
 	/// Container for a single setting of a <see cref="Configuration.ConfigFile"/>. 
 	/// Each config entry is linked to one config file.
 	/// </summary>
-	public sealed class ConfigEntry
+	public abstract class ConfigEntryBase
 	{
-		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) : this(configFile, definition)
-		{
-			SetTypeAndDefaultValue(settingType, defaultValue, true);
-		}
-
-		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition)
+		/// <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)
 		{
 			ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile));
 			Definition = definition ?? throw new ArgumentNullException(nameof(definition));
-		}
-
-		internal void SetTypeAndDefaultValue(Type settingType, object defaultValue, bool uniqueDefaultValue)
-		{
-			if (settingType == null) throw new ArgumentNullException(nameof(settingType));
-
-			if (settingType == SettingType)
-			{
-				if (uniqueDefaultValue)
-					DefaultValue = defaultValue;
-				return;
-			}
-
-			if (SettingType != null)
-			{
-				throw new ArgumentException($"Tried to define setting \"{Definition}\" as type {settingType.Name} " +
-											$"while it was already defined as type {SettingType.Name}. Use the same " +
-											"Type for all Wrappers of a single setting.");
-			}
+			SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType));
 
-			if (defaultValue == null && settingType.IsValueType)
-				throw new ArgumentException("defaultValue is null while settingType is a value type");
-
-			if (defaultValue != null && !settingType.IsInstanceOfType(defaultValue))
-				throw new ArgumentException("defaultValue can not be assigned to type " + settingType.Name);
-
-			SettingType = settingType;
+			// Free type check
+			Value = defaultValue;
 			DefaultValue = defaultValue;
 		}
 
-		private object _convertedValue;
-		private string _serializedValue;
-
 		/// <summary>
 		/// Config file this entry is a part of.
 		/// </summary>
@@ -71,138 +43,69 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Type of the <see cref="Value"/> that this setting holds.
 		/// </summary>
-		public Type SettingType { get; private set; }
+		public Type SettingType { get; }
 
 		/// <summary>
 		/// Default value of this setting (set only if the setting was not changed before).
 		/// </summary>
-		public object DefaultValue { get; private set; }
-
-		/// <summary>
-		/// Is the type of this setting defined, and by extension can <see cref="Value"/> of this setting be accessed.
-		/// Setting is defined when any <see cref="ConfigWrapper{T}"/> objects reference it.
-		/// </summary>
-		public bool IsDefined => SettingType != null;
+		public object DefaultValue { get; }
 
 		/// <summary>
 		/// Get or set the value of the setting.
-		/// Can't be used when <see cref="IsDefined"/> is false.
 		/// </summary>
-		public object Value
-		{
-			get
-			{
-				ProcessSerializedValue();
-
-				return _convertedValue;
-			}
-			set => SetValue(value, true, this);
-		}
-
-		internal void SetValue(object newValue, bool fireEvent, object sender)
-		{
-			bool wasChanged = ProcessSerializedValue();
-			newValue = ClampValue(newValue);
-			wasChanged = wasChanged || !Equals(newValue, _convertedValue);
-
-			if (wasChanged)
-			{
-				_convertedValue = newValue;
-
-				if (fireEvent)
-					OnSettingChanged(sender);
-			}
-		}
+		public abstract object Value { get; set; }
 
 		/// <summary>
 		/// Get the serialized representation of the value.
 		/// </summary>
 		public string GetSerializedValue()
 		{
-			if (_serializedValue != null)
-				return _serializedValue;
-
-			if (!IsDefined)
-				return null;
-
 			return TomlTypeConverter.ConvertToString(Value, SettingType);
 		}
 
 		/// <summary>
 		/// Set the value by using its serialized form.
 		/// </summary>
-		public void SetSerializedValue(string newValue) => SetSerializedValue(newValue, true, this);
-
-		internal void SetSerializedValue(string newValue, bool fireEvent, object sender)
+		public void SetSerializedValue(string value)
 		{
-			string current = GetSerializedValue();
-			if (string.Equals(current, newValue)) return;
-
-			_serializedValue = newValue;
-
-			if (!IsDefined) return;
-
-			if (ProcessSerializedValue())
+			try
 			{
-				if (fireEvent)
-					OnSettingChanged(sender);
+				var newValue = TomlTypeConverter.ConvertToValue(value, SettingType);
+				Value = newValue;
 			}
-		}
-
-		private bool ProcessSerializedValue()
-		{
-			if (!IsDefined)
-				throw new InvalidOperationException("Can't get the value before the SettingType is specified");
-
-			if (_serializedValue != null)
+			catch (Exception e)
 			{
-				string value = _serializedValue;
-				_serializedValue = null;
-
-				if (value != "")
-				{
-					try
-					{
-						var newValue = TomlTypeConverter.ConvertToValue(value, SettingType);
-						newValue = ClampValue(newValue);
-						if (!Equals(newValue, _convertedValue))
-						{
-							_convertedValue = newValue;
-							return true;
-						}
-						return false;
-					}
-					catch (Exception e)
-					{
-						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}");
 			}
-
-			if (_convertedValue == null && DefaultValue != null)
-			{
-				_convertedValue = DefaultValue;
-				return true;
-			}
-
-			return false;
 		}
 
 		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;
-			SetValue(ClampValue(Value), true, this);
+
+			// Automatically calls ClampValue in case it changed
+			Value = Value;
 		}
 
-		private object ClampValue(object value)
+		/// <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)
-				return Description.AcceptableValues.Clamp(value);
+				return (T)Description.AcceptableValues.Clamp(value);
 			return value;
 		}
 
-		private void OnSettingChanged(object sender)
+		/// <summary>
+		/// Trigger setting changed event.
+		/// </summary>
+		protected void OnSettingChanged(object sender)
 		{
 			ConfigFile.OnSettingChanged(sender, this);
 		}
@@ -228,20 +131,49 @@ namespace BepInEx.Configuration
 				writer.WriteLine("# Default value: " + DefaultValue);
 			}
 
-			if (hasDescription)
+			if (hasDescription && Description.AcceptableValues != null)
+			{
+				writer.WriteLine(Description.AcceptableValues.ToSerializedString());
+			}
+			else if (hasType)
 			{
-				if (Description.AcceptableValues != null)
-				{
-					writer.WriteLine(Description.AcceptableValues.ToSerializedString());
-				}
-				else if (hasType)
-				{
-					/*if (SettingType == typeof(bool))
-						writer.WriteLine("# Acceptable values: True, False");
-					else*/ if (SettingType.IsEnum)
-						writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType)));
-				}
+				/*if (SettingType == typeof(bool))
+					writer.WriteLine("# Acceptable values: True, False");
+				else*/
+				if (SettingType.IsEnum)
+					writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType)));
 			}
 		}
 	}
+
+	/// <inheritdoc />
+	public class ConfigEntry<T> : ConfigEntryBase
+	{
+		private T _typedValue;
+		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue) : base(configFile, definition, typeof(T), defaultValue) { }
+
+		/// <summary>
+		/// Get or set the value of the setting without boxing.
+		/// </summary>
+		public T TypedValue
+		{
+			get => _typedValue;
+			set
+			{
+				value = ClampValue(value);
+				if (Equals(_typedValue, value))
+					return;
+
+				_typedValue = value;
+				OnSettingChanged(this);
+			}
+		}
+
+		/// <inheritdoc />
+		public override object Value
+		{
+			get => TypedValue;
+			set => TypedValue = (T)value;
+		}
+	}
 }

+ 101 - 143
BepInEx/Configuration/ConfigFile.cs

@@ -20,20 +20,31 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// All config entries inside 
 		/// </summary>
-		protected Dictionary<ConfigDefinition, ConfigEntry> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntry>();
+		protected Dictionary<ConfigDefinition, ConfigEntryBase> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntryBase>();
+
+		private Dictionary<ConfigDefinition, string> HomelessEntries { get; } = new Dictionary<ConfigDefinition, string>();
 
 		/// <summary>
 		/// Create a list with all config entries inside of this config file.
 		/// </summary>
 		[Obsolete("Use GetConfigEntries instead")]
-		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions => Entries.Keys.ToList().AsReadOnly();
+		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions
+		{
+			get
+			{
+				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="Wrap{T}(ConfigDefinition,T,ConfigDescription)"/> 
 		/// instead with no description.
 		/// </summary>
-		public ConfigEntry[] GetConfigEntries() => Entries.Values.ToArray();
+		public ConfigEntryBase[] GetConfigEntries()
+		{
+			lock (_ioLock) return Entries.Values.ToArray();
+		}
 
 		/// <summary>
 		/// Full path to the config file. The file might not exist until a setting is added and changed, or <see cref="Save"/> is called.
@@ -68,13 +79,12 @@ namespace BepInEx.Configuration
 			{
 				Save();
 			}
-
-			StartWatching();
 		}
 
 		#region Save/Load
 
 		private readonly object _ioLock = new object();
+		private bool _disableSaving;
 
 		/// <summary>
 		/// Reloads the config from disk. Unsaved changes are lost.
@@ -83,38 +93,45 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				string currentSection = string.Empty;
-
-				foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
+				try
 				{
-					string line = rawLine.Trim();
+					_disableSaving = true;
 
-					if (line.StartsWith("#")) //comment
-						continue;
+					string currentSection = string.Empty;
 
-					if (line.StartsWith("[") && line.EndsWith("]")) //section
+					foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
 					{
-						currentSection = line.Substring(1, line.Length - 2);
-						continue;
-					}
+						string line = rawLine.Trim();
+
+						if (line.StartsWith("#")) //comment
+							continue;
 
-					string[] split = line.Split('='); //actual config line
-					if (split.Length != 2)
-						continue; //empty/invalid line
+						if (line.StartsWith("[") && line.EndsWith("]")) //section
+						{
+							currentSection = line.Substring(1, line.Length - 2);
+							continue;
+						}
 
-					string currentKey = split[0].Trim();
-					string currentValue = split[1].Trim();
+						string[] split = line.Split('='); //actual config line
+						if (split.Length != 2)
+							continue; //empty/invalid line
 
-					var definition = new ConfigDefinition(currentSection, currentKey);
+						string currentKey = split[0].Trim();
+						string currentValue = split[1].Trim();
 
-					Entries.TryGetValue(definition, out ConfigEntry entry);
-					if (entry == null)
-					{
-						entry = new ConfigEntry(this, definition);
-						Entries[definition] = entry;
-					}
+						var definition = new ConfigDefinition(currentSection, currentKey);
+
+						Entries.TryGetValue(definition, out ConfigEntryBase entry);
 
-					entry.SetSerializedValue(currentValue, true, this);
+						if (entry != null)
+							entry.SetSerializedValue(currentValue);
+						else
+							HomelessEntries[definition] = currentValue;
+					}
+				}
+				finally
+				{
+					_disableSaving = false;
 				}
 			}
 
@@ -128,7 +145,7 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				StopWatching();
+				if (_disableSaving) return;
 
 				string directoryName = Path.GetDirectoryName(ConfigFilePath);
 				if (directoryName != null) Directory.CreateDirectory(directoryName);
@@ -159,8 +176,6 @@ namespace BepInEx.Configuration
 						writer.WriteLine();
 					}
 				}
-
-				StartWatching();
 			}
 		}
 
@@ -177,46 +192,55 @@ 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.</param>
-		/// <returns></returns>
 		public ConfigWrapper<T> Wrap<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())}");
+			try
+			{
+				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())}");
 
-			var forceSave = false;
+				lock (_ioLock)
+				{
+					_disableSaving = true;
 
-			Entries.TryGetValue(configDefinition, out var entry);
+					Entries.TryGetValue(configDefinition, out var existingEntry);
 
-			if (entry == null)
-			{
-				entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue);
-				Entries[configDefinition] = entry;
-				forceSave = true;
-			}
-			else
-			{
-				entry.SetTypeAndDefaultValue(typeof(T), defaultValue, !Equals(defaultValue, default(T)));
-			}
+					if (existingEntry != null && !(existingEntry is ConfigEntry<T>))
+						throw new ArgumentException("The defined setting already exists with a different setting type - " + existingEntry.SettingType.Name);
 
-			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.");
+					var entry = (ConfigEntry<T>)existingEntry;
 
-				if (configDescription.AcceptableValues != null)
-				{
-					var genericArguments = configDescription.AcceptableValues.GetType().GetGenericArguments();
-					if (genericArguments.Length > 0 && genericArguments[0] != typeof(T))
-						throw new ArgumentException("AcceptableValues has a different type than the setting type", nameof(configDefinition));
-				}
+					if (entry == null)
+					{
+						entry = new ConfigEntry<T>(this, configDefinition, defaultValue);
+						Entries[configDefinition] = entry;
+					}
 
-				entry.SetDescription(configDescription);
-			}
+					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.");
 
-			if(forceSave)
-				Save();
+						entry.SetDescription(configDescription);
+					}
 
-			return new ConfigWrapper<T>(entry);
+					if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue))
+					{
+						entry.SetSerializedValue(homelessValue);
+						HomelessEntries.Remove(configDefinition);
+					}
+
+					_disableSaving = false;
+					if (SaveOnConfigSet)
+						Save();
+
+					return new ConfigWrapper<T>(entry);
+				}
+			}
+			finally
+			{
+				_disableSaving = false;
+			}
 		}
 
 		/// <summary>
@@ -238,7 +262,6 @@ 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.</param>
-		/// <returns></returns>
 		public ConfigWrapper<T> Wrap<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
 			=> Wrap(new ConfigDefinition(section, key), defaultValue, configDescription);
 
@@ -256,101 +279,36 @@ namespace BepInEx.Configuration
 		/// </summary>
 		public event EventHandler<SettingChangedEventArgs> SettingChanged;
 
-		internal void OnSettingChanged(object sender, ConfigEntry changedEntry)
+		internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase)
 		{
-			if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry));
-
-			if (SettingChanged != null)
-			{
-				var args = new SettingChangedEventArgs(changedEntry);
-
-				foreach (var callback in SettingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>())
-				{
-					try
-					{
-						callback(sender, args);
-					}
-					catch (Exception e)
-					{
-						Logger.Log(LogLevel.Error, e);
-					}
-				}
-			}
+			if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase));
 
-			// Check sender to prevent infinite loops
-			// todo batching / async?
-			if (sender != this && SaveOnConfigSet)
+			if (SaveOnConfigSet)
 				Save();
-		}
 
-		private void OnConfigReloaded()
-		{
-			if (ConfigReloaded != null)
-			{
-				foreach (var callback in ConfigReloaded.GetInvocationList().Cast<EventHandler>())
-				{
-					try
-					{
-						callback(this, EventArgs.Empty);
-					}
-					catch (Exception e)
-					{
-						Logger.Log(LogLevel.Error, e);
-					}
-				}
-			}
-		}
-
-		#endregion
+			var settingChanged = SettingChanged;
+			if (settingChanged == null) return;
 
-		#region File watcher
-
-		private FileSystemWatcher _watcher;
-
-		/// <summary>
-		/// Start watching the config file on disk for changes.
-		/// </summary>
-		public void StartWatching()
-		{
-			lock (_ioLock)
+			var args = new SettingChangedEventArgs(changedEntryBase);
+			foreach (var callback in settingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>())
 			{
-				if (_watcher != null) return;
-
-				_watcher = new FileSystemWatcher
-				{
-					Path = Path.GetDirectoryName(ConfigFilePath) ?? throw new ArgumentException("Invalid config path"),
-					Filter = Path.GetFileName(ConfigFilePath),
-					IncludeSubdirectories = false,
-					NotifyFilter = NotifyFilters.LastWrite,
-					EnableRaisingEvents = true
-				};
-
-				_watcher.Changed += (sender, args) => Reload();
+				try { callback(sender, args); }
+				catch (Exception e) { Logger.Log(LogLevel.Error, e); }
 			}
 		}
 
-		/// <summary>
-		/// Stop watching the config file on disk for changes.
-		/// </summary>
-		public void StopWatching()
+		private void OnConfigReloaded()
 		{
-			lock (_ioLock)
+			var configReloaded = ConfigReloaded;
+			if (configReloaded == null) return;
+
+			foreach (var callback in configReloaded.GetInvocationList().Cast<EventHandler>())
 			{
-				if (_watcher != null)
-				{
-					_watcher.EnableRaisingEvents = false;
-					_watcher.Dispose();
-					_watcher = null;
-				}
+				try { callback(this, EventArgs.Empty); }
+				catch (Exception e) { Logger.Log(LogLevel.Error, e); }
 			}
 		}
 
-		/// <inheritdoc />
-		~ConfigFile()
-		{
-			StopWatching();
-		}
-
 		#endregion
 	}
 }

+ 5 - 5
BepInEx/Configuration/ConfigWrapper.cs

@@ -11,7 +11,7 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Entry of this setting in the <see cref="Configuration.ConfigFile"/>.
 		/// </summary>
-		public ConfigEntry ConfigEntry { get; }
+		public ConfigEntry<T> ConfigEntry { get; }
 
 		/// <summary>
 		/// Unique definition of this setting.
@@ -33,11 +33,11 @@ namespace BepInEx.Configuration
 		/// </summary>
 		public T Value
 		{
-			get => (T)ConfigEntry.Value;
-			set => ConfigEntry.SetValue(value, true, this);
+			get => ConfigEntry.TypedValue;
+			set => ConfigEntry.TypedValue = value;
 		}
 
-		internal ConfigWrapper(ConfigEntry configEntry)
+		internal ConfigWrapper(ConfigEntry<T> configEntry)
 		{
 			ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry));
 
@@ -47,4 +47,4 @@ namespace BepInEx.Configuration
 			};
 		}
 	}
-}
+}

+ 3 - 3
BepInEx/Configuration/SettingChangedEventArgs.cs

@@ -9,7 +9,7 @@ namespace BepInEx.Configuration
 	public sealed class SettingChangedEventArgs : EventArgs
 	{
 		/// <inheritdoc />
-		public SettingChangedEventArgs(ConfigEntry changedSetting)
+		public SettingChangedEventArgs(ConfigEntryBase changedSetting)
 		{
 			ChangedSetting = changedSetting;
 		}
@@ -17,6 +17,6 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Setting that was changed
 		/// </summary>
-		public ConfigEntry ChangedSetting { get; }
+		public ConfigEntryBase ChangedSetting { get; }
 	}
-}
+}

+ 6 - 27
BepInExTests/Configuration/ConfigFileTests.cs

@@ -23,10 +23,7 @@ namespace BepInEx.Configuration.Tests
 		public static void Cleanup()
 		{
 			foreach (var configFile in _toRemove)
-			{
-				configFile.StopWatching();
 				File.Delete(configFile.ConfigFilePath);
-			}
 		}
 
 		private static ConfigFile MakeConfig()
@@ -51,20 +48,20 @@ namespace BepInEx.Configuration.Tests
 
 			var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test"));
 			var lines = File.ReadAllLines(c.ConfigFilePath);
-			Assert.AreEqual(0, lines.Count(x => x.Equals("[Cat]")));
-			Assert.AreEqual(0, lines.Count(x => x.Equals("# Test")));
-			Assert.AreEqual(0, lines.Count(x => x.Equals("Key = 0")));
+			Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
+			Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
+			Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0")));
 
 			c.Save();
 			lines = File.ReadAllLines(c.ConfigFilePath);
 			Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
-			Assert.AreEqual(1, lines.Count(x => x.Equals("# Test")));
+			Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
 			Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0")));
 
 			w.Value = 69;
 			lines = File.ReadAllLines(c.ConfigFilePath);
 			Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
-			Assert.AreEqual(1, lines.Count(x => x.Equals("# Test")));
+			Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
 			Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 69")));
 		}
 
@@ -106,24 +103,7 @@ namespace BepInEx.Configuration.Tests
 			c.Reload();
 			Assert.AreEqual(w.Value, 1);
 		}
-
-		[TestMethod]
-		public void FileWatchTest()
-		{
-			var c = MakeConfig();
-			var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test"));
-			Assert.AreEqual(w.Value, 0);
-
-			var eventFired = new AutoResetEvent(false);
-			w.SettingChanged += (sender, args) => eventFired.Set();
-
-			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n");
-
-			Assert.IsTrue(eventFired.WaitOne(500));
-
-			Assert.AreEqual(w.Value, 1);
-		}
-
+		
 		[TestMethod]
 		public void FileWatchTestNoSelfReload()
 		{
@@ -207,7 +187,6 @@ namespace BepInEx.Configuration.Tests
 		public void ValueRangeLoadTest()
 		{
 			var c = MakeConfig();
-			c.StopWatching();
 
 			File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n");
 			c.Reload();