From 795ab74fe075366c2904d0a6898bd574cc748085 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 11 Aug 2019 14:08:07 +0200 Subject: [PATCH 01/54] Added support for enums to settings --- BepInEx/Configuration/TomlTypeConverter.cs | 102 +++++++++++++-------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index e650e2b..25c4f03 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -6,8 +6,8 @@ namespace BepInEx.Configuration { public class TypeConverter { - public Func ConvertToString { get; set; } - public Func ConvertToObject { get; set; } + public Func ConvertToString { get; set; } + public Func ConvertToObject { get; set; } } internal static class TomlTypeConverter @@ -16,98 +16,126 @@ namespace BepInEx.Configuration { [typeof(string)] = new TypeConverter { - ConvertToString = (obj) => (string)obj, - ConvertToObject = (str) => str, + ConvertToString = (obj, type) => (string)obj, + ConvertToObject = (str, type) => str, }, [typeof(bool)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString().ToLowerInvariant(), - ConvertToObject = (str) => bool.Parse(str), + ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(), + ConvertToObject = (str, type) => bool.Parse(str), }, [typeof(byte)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => byte.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => byte.Parse(str), }, //integral types [typeof(sbyte)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => sbyte.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => sbyte.Parse(str), }, [typeof(byte)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => byte.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => byte.Parse(str), }, [typeof(short)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => short.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => short.Parse(str), }, [typeof(ushort)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => ushort.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => ushort.Parse(str), }, [typeof(int)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => int.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => int.Parse(str), }, [typeof(uint)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => uint.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => uint.Parse(str), }, [typeof(long)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => long.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => long.Parse(str), }, [typeof(ulong)] = new TypeConverter { - ConvertToString = (obj) => obj.ToString(), - ConvertToObject = (str) => ulong.Parse(str), + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => ulong.Parse(str), }, //floating point types [typeof(float)] = new TypeConverter { - ConvertToString = (obj) => ((float)obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str) => float.Parse(str, NumberFormatInfo.InvariantInfo), + ConvertToString = (obj, type) => ((float)obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo), }, [typeof(double)] = new TypeConverter { - ConvertToString = (obj) => ((double)obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str) => double.Parse(str, NumberFormatInfo.InvariantInfo), + ConvertToString = (obj, type) => ((double)obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo), }, [typeof(decimal)] = new TypeConverter { - ConvertToString = (obj) => ((decimal)obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str) => decimal.Parse(str, NumberFormatInfo.InvariantInfo), + ConvertToString = (obj, type) => ((decimal)obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo), + }, + + [typeof(Enum)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => Enum.Parse(type, str, true), }, }; - public static string ConvertToString(object value) + public static string ConvertToString(T value) { - Type valueType = value.GetType(); + var valueType = typeof(T); - if (!TypeConverters.ContainsKey(valueType)) + var conv = GetConverter(valueType); + if (conv == null) throw new InvalidOperationException($"Cannot convert from type {valueType}"); - return TypeConverters[valueType].ConvertToString(value); + return conv.ConvertToString(value, valueType); } public static T ConvertToValue(string value) { - if (!TypeConverters.ContainsKey(typeof(T))) - throw new InvalidOperationException($"Cannot convert to type {typeof(T)}"); + var valueType = typeof(T); - return (T)TypeConverters[typeof(T)].ConvertToObject(value); + var conv = GetConverter(valueType); + if (conv == null) + throw new InvalidOperationException($"Cannot convert to type {valueType}"); + + return (T)conv.ConvertToObject(value, valueType); + } + + private static TypeConverter GetConverter(Type valueType) + { + if (valueType.IsEnum) + return TypeConverters[typeof(Enum)]; + + return TypeConverters[valueType]; + } + + public static bool CanConvert(Type type) + { + return GetConverter(type) != null; + } + + public static IEnumerable GetSupportedTypes() + { + return TypeConverters.Keys; } } } From ad1c9bd7e1bbb89bbc624e4136552c2b2728a355 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 11 Aug 2019 14:08:56 +0200 Subject: [PATCH 02/54] Split ConfigDefinition and ConfigDescription --- BepInEx/Configuration/ConfigDefinition.cs | 46 ++++++++++++++++------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 5fab0e4..304332d 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -1,19 +1,45 @@ -namespace BepInEx.Configuration +using System; + +namespace BepInEx.Configuration { - public class ConfigDefinition + public class ConfigDescription + { + public ConfigDescription(string description, Type settingType, object defaultValue) + { + Description = description ?? throw new ArgumentNullException(nameof(description)); + SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); + DefaultValue = defaultValue; + + if(defaultValue == null && settingType.IsByRef) + 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); + } + + public string Description { get; } + public Type SettingType { get; } + public object DefaultValue { get; } + //todo value range + } + + public class ConfigDefinition : IEquatable { public string Section { get; } public string Key { get; } - public string Description { get; internal set; } - - public ConfigDefinition(string section, string key, string description = null) + public ConfigDefinition(string section, string key) { Key = key; Section = section; + } - Description = description; + public bool Equals(ConfigDefinition other) + { + if (other == null) return false; + return string.Equals(Key, other.Key) + && string.Equals(Section, other.Section); } public override bool Equals(object obj) @@ -22,14 +48,8 @@ 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); + return Equals(obj as ConfigDefinition); } public override int GetHashCode() From 65177c419eafe59ed2f495c3cfea076c313f72e8 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 11 Aug 2019 23:22:46 +0200 Subject: [PATCH 03/54] Large config file upgrade - backend rewrite; config file change monitoring; setting changed events propagate from ConfigFile; added unit tests and Test configuration --- BepInEx.sln | 123 ++++++++++ BepInEx/BepInEx.csproj | 33 +++ BepInEx/Configuration/ConfigDefinition.cs | 28 +-- BepInEx/Configuration/ConfigDescription.cs | 21 ++ BepInEx/Configuration/ConfigEntry.cs | 182 +++++++++++++++ BepInEx/Configuration/ConfigFile.cs | 215 ++++++++++++++---- BepInEx/Configuration/ConfigWrapper.cs | 30 +-- .../Configuration/SettingChangedEventArgs.cs | 14 ++ BepInEx/Configuration/TomlTypeConverter.cs | 11 +- BepInExTests/BepInExTests.csproj | 131 +++++++++++ BepInExTests/Configuration/ConfigFileTests.cs | 172 ++++++++++++++ BepInExTests/Properties/AssemblyInfo.cs | 36 +++ BepInExTests/packages.config | 5 + 13 files changed, 916 insertions(+), 85 deletions(-) create mode 100644 BepInEx/Configuration/ConfigDescription.cs create mode 100644 BepInEx/Configuration/ConfigEntry.cs create mode 100644 BepInEx/Configuration/SettingChangedEventArgs.cs create mode 100644 BepInExTests/BepInExTests.csproj create mode 100644 BepInExTests/Configuration/ConfigFileTests.cs create mode 100644 BepInExTests/Properties/AssemblyInfo.cs create mode 100644 BepInExTests/packages.config diff --git a/BepInEx.sln b/BepInEx.sln index 9d302f8..377c327 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -27,44 +27,167 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.RuntimeDetour", "su EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.Utils", "submodules\MonoMod\MonoMod.Utils\MonoMod.Utils.csproj", "{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInExTests", "BepInExTests\BepInExTests.csproj", "{E7CD429A-D057-48E3-8C51-E5C934E8E07B}" + ProjectSection(ProjectDependencies) = postProject + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9} = {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Test = Debug|Test Legacy|Any CPU = Legacy|Any CPU + Legacy|Test = Legacy|Test + Release|Any CPU = Release|Any CPU + Release|Test = Release|Test v2018|Any CPU = v2018|Any CPU + v2018|Test = v2018|Test EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.ActiveCfg = v2018|x86 + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.Build.0 = v2018|x86 {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.Build.0 = Legacy|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.ActiveCfg = Legacy|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.Build.0 = Legacy|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.ActiveCfg = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.Build.0 = v2018|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.ActiveCfg = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.Build.0 = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.ActiveCfg = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.Build.0 = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.Build.0 = Legacy|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.ActiveCfg = Legacy|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.Build.0 = Legacy|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.ActiveCfg = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.Build.0 = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.ActiveCfg = v2018|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.Build.0 = v2018|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Test.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Test.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Test.ActiveCfg = Debug|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Test.Build.0 = Debug|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Test.ActiveCfg = Debug|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Test.Build.0 = Debug|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.Build.0 = Release|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.ActiveCfg = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.Build.0 = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.Build.0 = Legacy|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.ActiveCfg = Legacy|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.Build.0 = Legacy|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.ActiveCfg = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.Build.0 = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.ActiveCfg = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.Build.0 = v2018|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Test.ActiveCfg = Debug|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Test.Build.0 = Debug|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.Build.0 = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.ActiveCfg = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.Build.0 = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.ActiveCfg = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.Build.0 = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.Build.0 = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.ActiveCfg = Release|Any CPU + {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.Build.0 = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Test.ActiveCfg = Debug|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Test.Build.0 = Debug|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.Build.0 = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.ActiveCfg = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.Build.0 = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.ActiveCfg = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.Build.0 = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.ActiveCfg = Release|Any CPU + {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.ActiveCfg = Debug|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.Build.0 = Debug|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.ActiveCfg = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index 5bf383b..ee37d48 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -33,11 +33,41 @@ AnyCPU prompt MinimumRecommendedRules.ruleset + ..\bin\BepInEx.xml + + + bin\x86\Legacy\ + DEBUG;TRACE + false + x86 + prompt + MinimumRecommendedRules.ruleset + full + true + + + bin\x86\v2018\ + DEBUG;TRACE;UNITY_2018 + false + x86 + prompt + MinimumRecommendedRules.ruleset + full + true ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll + + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll + + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll + ..\lib\UnityEngine.dll @@ -46,8 +76,11 @@ + + + diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 304332d..ff49c71 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -2,27 +2,6 @@ namespace BepInEx.Configuration { - public class ConfigDescription - { - public ConfigDescription(string description, Type settingType, object defaultValue) - { - Description = description ?? throw new ArgumentNullException(nameof(description)); - SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); - DefaultValue = defaultValue; - - if(defaultValue == null && settingType.IsByRef) - 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); - } - - public string Description { get; } - public Type SettingType { get; } - public object DefaultValue { get; } - //todo value range - } - public class ConfigDefinition : IEquatable { public string Section { get; } @@ -39,7 +18,7 @@ namespace BepInEx.Configuration { if (other == null) return false; return string.Equals(Key, other.Key) - && string.Equals(Section, other.Section); + && string.Equals(Section, other.Section); } public override bool Equals(object obj) @@ -67,5 +46,10 @@ namespace BepInEx.Configuration public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right); + + public override string ToString() + { + return Section + " / " + Key; + } } } \ No newline at end of file diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs new file mode 100644 index 0000000..f14216f --- /dev/null +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -0,0 +1,21 @@ +using System; + +namespace BepInEx.Configuration +{ + public class ConfigDescription + { + public ConfigDescription(string description) + { + Description = description ?? throw new ArgumentNullException(nameof(description)); + } + + public string Description { get; } + + //todo value range + + public string ToSerializedString() + { + return $"# {Description.Replace("\n", "\n# ")}"; + } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs new file mode 100644 index 0000000..6ba9900 --- /dev/null +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Linq; +using BepInEx.Logging; + +namespace BepInEx.Configuration +{ + public sealed class ConfigEntry + { + internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) : this(configFile, definition) + { + SetTypeAndDefaultValue(settingType, defaultValue, true); + } + + internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition) + { + 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."); + } + + 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; + DefaultValue = defaultValue; + } + + private object _convertedValue; + private string _serializedValue; + + public ConfigFile ConfigFile { get; } + public ConfigDefinition Definition { get; } + + public ConfigDescription Description { get; internal set; } + + public Type SettingType { get; private set; } + public object DefaultValue { get; private set; } + + /// + /// Is the type of this setting defined, and by extension can of this setting be accessed. + /// Setting is defined when any objects reference it. + /// + public bool IsDefined => SettingType != null; + + /// + /// Can't be used when is false. + /// + public object Value + { + get + { + ProcessSerializedValue(); + + return _convertedValue; + } + set => SetValue(value, true, this); + } + + internal void SetValue(object newValue, bool fireEvent, object sender) + { + bool wasChanged = ProcessSerializedValue(); + wasChanged = wasChanged || !Equals(newValue, _convertedValue); + + if (wasChanged) + { + _convertedValue = newValue; + + if (fireEvent) + OnSettingChanged(sender); + } + } + + public string GetSerializedValue() + { + if (_serializedValue != null) + return _serializedValue; + + if (!IsDefined) + return null; + + return TomlTypeConverter.ConvertToString(Value, SettingType); + } + + public void SetSerializedValue(string newValue, bool fireEvent, object sender) + { + string current = GetSerializedValue(); + if (string.Equals(current, newValue)) return; + + _serializedValue = newValue; + + if (!IsDefined) return; + + if (ProcessSerializedValue()) + { + if (fireEvent) + OnSettingChanged(sender); + } + } + + private bool ProcessSerializedValue() + { + if (!IsDefined) + throw new InvalidOperationException("Can't get the value before the SettingType is specified"); + + if (_serializedValue != null) + { + string value = _serializedValue; + _serializedValue = null; + + if (value != "") + { + try + { + var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); + 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}"); + } + } + } + + if (_convertedValue == null && DefaultValue != null) + { + _convertedValue = DefaultValue; + return true; + } + + return false; + } + + private void OnSettingChanged(object sender) + { + ConfigFile.OnSettingChanged(sender, this); + } + + public void WriteDescription(StreamWriter writer) + { + if (Description != null) + writer.WriteLine(Description.ToSerializedString()); + + if (SettingType != null) + { + writer.WriteLine("# Setting type: " + SettingType.Name); + writer.WriteLine("# Default value: " + DefaultValue); + + // todo acceptable values + + if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) + writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + } + } + } +} diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 7be36a4..85ae3c1 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Text.RegularExpressions; +using System.Text; +using BepInEx.Logging; namespace BepInEx.Configuration { @@ -12,28 +13,30 @@ namespace BepInEx.Configuration /// public class ConfigFile { - private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+"); + // Need to be lazy evaluated to not cause problems for unit tests + private static ConfigFile _coreConfig; + internal static ConfigFile CoreConfig => _coreConfig ?? (_coreConfig = new ConfigFile(Paths.BepInExConfigPath, true)); - internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true); + protected Dictionary Entries { get; } = new Dictionary(); - protected internal Dictionary Cache { get; } = new Dictionary(); + [Obsolete("Use ConfigEntries instead")] + public ReadOnlyCollection ConfigDefinitions => Entries.Keys.ToList().AsReadOnly(); - public ReadOnlyCollection ConfigDefinitions => Cache.Keys.ToList().AsReadOnly(); - - /// - /// An event that is fired every time the config is reloaded. - /// - public event EventHandler ConfigReloaded; + public ReadOnlyCollection ConfigEntries => Entries.Values.ToList().AsReadOnly(); public string ConfigFilePath { get; } /// - /// If enabled, writes the config to disk every time a value is set. + /// If enabled, writes the config to disk every time a value is set. If disabled, you have to manually save or the changes will be lost! /// public bool SaveOnConfigSet { get; set; } = true; public ConfigFile(string configPath, bool saveOnInit) { + if (configPath == null) throw new ArgumentNullException(nameof(configPath)); + + configPath = Path.GetFullPath(configPath); + ConfigFilePath = configPath; if (File.Exists(ConfigFilePath)) @@ -44,9 +47,13 @@ namespace BepInEx.Configuration { Save(); } + + StartWatching(); } - private object _ioLock = new object(); + #region Save/Load + + private readonly object _ioLock = new object(); /// /// Reloads the config from disk. Unsaved changes are lost. @@ -55,9 +62,7 @@ namespace BepInEx.Configuration { lock (_ioLock) { - Dictionary descriptions = Cache.ToDictionary(x => x.Key, x => x.Key.Description); - - string currentSection = ""; + string currentSection = string.Empty; foreach (string rawLine in File.ReadAllLines(ConfigFilePath)) { @@ -81,14 +86,18 @@ namespace BepInEx.Configuration var definition = new ConfigDefinition(currentSection, currentKey); - if (descriptions.ContainsKey(definition)) - definition.Description = descriptions[definition]; + Entries.TryGetValue(definition, out ConfigEntry entry); + if (entry == null) + { + entry = new ConfigEntry(this, definition); + Entries[definition] = entry; + } - Cache[definition] = currentValue; + entry.SetSerializedValue(currentValue, true, this); } - - ConfigReloaded?.Invoke(this, EventArgs.Empty); } + + OnConfigReloaded(); } /// @@ -98,51 +107,177 @@ namespace BepInEx.Configuration { lock (_ioLock) { - if (!Directory.Exists(Paths.ConfigPath)) - Directory.CreateDirectory(Paths.ConfigPath); + StopWatching(); - 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)) + string directoryName = Path.GetDirectoryName(ConfigFilePath); + if (directoryName != null) Directory.CreateDirectory(directoryName); + + using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8)) + { + foreach (var sectionKv in Entries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) { + // Section heading writer.WriteLine($"[{sectionKv.Key}]"); - foreach (var entryKv in sectionKv) + foreach (var configEntry in sectionKv.Select(x => x.Value)) { writer.WriteLine(); - if (!string.IsNullOrEmpty(entryKv.Key.Description)) - writer.WriteLine($"# {entryKv.Key.Description.Replace("\n", "\n# ")}"); + configEntry.WriteDescription(writer); - writer.WriteLine($"{entryKv.Key.Key} = {entryKv.Value}"); + writer.WriteLine($"{configEntry.Definition.Key} = {configEntry.GetSerializedValue()}"); } writer.WriteLine(); } + } + + StartWatching(); } } - public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default(T)) + #endregion + + #region Wraps + + public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) { - if (!Cache.ContainsKey(configDefinition)) + 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())}"); + + Entries.TryGetValue(configDefinition, out var entry); + + if (entry == null) { - Cache.Add(configDefinition, TomlTypeConverter.ConvertToString(defaultValue)); - Save(); + entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue); + Entries[configDefinition] = entry; } else { - var original = Cache.Keys.First(x => x.Equals(configDefinition)); + entry.SetTypeAndDefaultValue(typeof(T), defaultValue, !Equals(defaultValue, default(T))); + } - if (original.Description != configDefinition.Description) + 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."); + + entry.Description = configDescription; + } + + return new ConfigWrapper(entry); + } + + [Obsolete("Use other Wrap overloads instead")] + public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) + => Wrap(new ConfigDefinition(section, key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + + public ConfigWrapper Wrap(string section, string key, T defaultValue, ConfigDescription configDescription = null) + => Wrap(new ConfigDefinition(section, key), defaultValue, configDescription); + + #endregion + + #region Events + + /// + /// An event that is fired every time the config is reloaded. + /// + public event EventHandler ConfigReloaded; + + /// + /// Fired when one of the settings is changed. + /// + public event EventHandler SettingChanged; + + protected internal void OnSettingChanged(object sender, ConfigEntry changedEntry) + { + if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry)); + + if (SettingChanged != null) + { + var args = new SettingChangedEventArgs(changedEntry); + + foreach (var callback in SettingChanged.GetInvocationList().Cast>()) { - original.Description = configDefinition.Description; - Save(); + try + { + callback(sender, args); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e); + } } } - return new ConfigWrapper(this, configDefinition); + // todo better way to prevent write loop? maybe do some caching? + if (sender != this && SaveOnConfigSet) + Save(); } - public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) - => Wrap(new ConfigDefinition(section, key, description), defaultValue); + protected void OnConfigReloaded() + { + if (ConfigReloaded != null) + { + foreach (var callback in ConfigReloaded.GetInvocationList().Cast()) + { + try + { + callback(this, EventArgs.Empty); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e); + } + } + } + } + + #endregion + + #region File watcher + + private FileSystemWatcher _watcher; + + /// + /// Start watching the config file on disk for changes. + /// + public void StartWatching() + { + lock (_ioLock) + { + 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(); + } + } + + /// + /// Stop watching the config file on disk for changes. + /// + public void StopWatching() + { + lock (_ioLock) + { + _watcher?.Dispose(); + _watcher = null; + } + } + + ~ConfigFile() + { + StopWatching(); + } + + #endregion } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/ConfigWrapper.cs b/BepInEx/Configuration/ConfigWrapper.cs index e3c1145..8f919f6 100644 --- a/BepInEx/Configuration/ConfigWrapper.cs +++ b/BepInEx/Configuration/ConfigWrapper.cs @@ -2,11 +2,12 @@ namespace BepInEx.Configuration { - public class ConfigWrapper + public sealed class ConfigWrapper { - public ConfigDefinition Definition { get; protected set; } + public ConfigEntry ConfigEntry { get; } - public ConfigFile ConfigFile { get; protected set; } + public ConfigDefinition Definition => ConfigEntry.Definition; + public ConfigFile ConfigFile => ConfigEntry.ConfigFile; /// /// Fired when the setting is changed. Does not detect changes made outside from this object. @@ -15,25 +16,18 @@ namespace BepInEx.Configuration public T Value { - get => TomlTypeConverter.ConvertToValue(ConfigFile.Cache[Definition]); - set - { - ConfigFile.Cache[Definition] = TomlTypeConverter.ConvertToString(value); - - if (ConfigFile.SaveOnConfigSet) - ConfigFile.Save(); - - SettingChanged?.Invoke(this, EventArgs.Empty); - } + get => (T)ConfigEntry.Value; + set => ConfigEntry.SetValue(value, true, this); } - public ConfigWrapper(ConfigFile configFile, ConfigDefinition definition) + internal ConfigWrapper(ConfigEntry configEntry) { - if (!TomlTypeConverter.TypeConverters.ContainsKey(typeof(T))) - throw new ArgumentException("Unsupported config wrapper type"); + ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry)); - ConfigFile = configFile; - Definition = definition; + configEntry.ConfigFile.SettingChanged += (sender, args) => + { + if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args); + }; } } } \ No newline at end of file diff --git a/BepInEx/Configuration/SettingChangedEventArgs.cs b/BepInEx/Configuration/SettingChangedEventArgs.cs new file mode 100644 index 0000000..3767e9b --- /dev/null +++ b/BepInEx/Configuration/SettingChangedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace BepInEx.Configuration +{ + public sealed class SettingChangedEventArgs : EventArgs + { + public SettingChangedEventArgs(ConfigEntry changedSetting) + { + ChangedSetting = changedSetting; + } + + public ConfigEntry ChangedSetting { get; } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 25c4f03..8a836a3 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -98,10 +98,8 @@ namespace BepInEx.Configuration }, }; - public static string ConvertToString(T value) + public static string ConvertToString(object value, Type valueType) { - var valueType = typeof(T); - var conv = GetConverter(valueType); if (conv == null) throw new InvalidOperationException($"Cannot convert from type {valueType}"); @@ -111,13 +109,16 @@ namespace BepInEx.Configuration public static T ConvertToValue(string value) { - var valueType = typeof(T); + return (T)ConvertToValue(value, typeof(T)); + } + public static object ConvertToValue(string value, Type valueType) + { var conv = GetConverter(valueType); if (conv == null) throw new InvalidOperationException($"Cannot convert to type {valueType}"); - return (T)conv.ConvertToObject(value, valueType); + return conv.ConvertToObject(value, valueType); } private static TypeConverter GetConverter(Type valueType) diff --git a/BepInExTests/BepInExTests.csproj b/BepInExTests/BepInExTests.csproj new file mode 100644 index 0000000..b9ebf21 --- /dev/null +++ b/BepInExTests/BepInExTests.csproj @@ -0,0 +1,131 @@ + + + + + Debug + AnyCPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B} + Library + Properties + BepInExTests + BepInExTests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + + + 15.0 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + + + + + + {4ffba620-f5ed-47f9-b90c-dad1316fd9b9} + BepInEx + + + + + + + False + + + False + + + False + + + False + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs new file mode 100644 index 0000000..3fde291 --- /dev/null +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using System.Linq; +using System.Threading; + +namespace BepInEx.Configuration.Tests +{ + [TestClass] + public class ConfigFileTests + { + private static ConcurrentBag _toRemove; + + [ClassInitialize] + public static void Init(TestContext context) + { + _toRemove = new ConcurrentBag(); + } + + [ClassCleanup] + public static void Cleanup() + { + foreach (var configFile in _toRemove) + { + configFile.StopWatching(); + File.Delete(configFile.ConfigFilePath); + } + } + + private static ConfigFile MakeConfig() + { + string configPath = Path.GetTempFileName(); + if (configPath == null) throw new InvalidOperationException("Wtf"); + var config = new ConfigFile(configPath, true); + _toRemove.Add(config); + return config; + } + + [TestMethod] + public void SaveTest() + { + MakeConfig().Save(); + } + + [TestMethod] + public void SaveTestValueChange() + { + var c = MakeConfig(); + + 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"))); + + 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("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("Key = 69"))); + } + + [TestMethod] + public void AutoSaveTest() + { + var c = MakeConfig(); + c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + + var eventFired = new AutoResetEvent(false); + c.ConfigReloaded += (sender, args) => eventFired.Set(); + + c.Save(); + + Assert.IsFalse(eventFired.WaitOne(200)); + } + + [TestMethod] + public void ReadTest() + { + var c = MakeConfig(); + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + c.Reload(); + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + Assert.AreEqual(w.Value, 1); + var w2 = c.Wrap("Cat", "Key2", 0, new ConfigDescription("Test")); + Assert.AreEqual(w2.Value, 0); + } + + [TestMethod] + public void ReadTest2() + { + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + Assert.AreEqual(w.Value, 0); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n"); + + 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() + { + var c = MakeConfig(); + + var eventFired = new AutoResetEvent(false); + c.ConfigReloaded += (sender, args) => eventFired.Set(); + + c.Save(); + + Assert.IsFalse(eventFired.WaitOne(200)); + } + + [TestMethod] + public void EventTestWrapper() + { + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + + var eventFired = false; + w.SettingChanged += (sender, args) => eventFired = true; + + c.Reload(); + + Assert.IsTrue(eventFired); + } + + [TestMethod] + public void EventTestReload() + { + var c = MakeConfig(); + var eventFired = false; + + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + w.SettingChanged += (sender, args) => eventFired = true; + + Assert.IsFalse(eventFired); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + c.Reload(); + + Assert.IsTrue(eventFired); + } + } +} \ No newline at end of file diff --git a/BepInExTests/Properties/AssemblyInfo.cs b/BepInExTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1487903 --- /dev/null +++ b/BepInExTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BepInExTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("BepInExTests")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e7cd429a-d057-48e3-8c51-e5c934e8e07b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/BepInExTests/packages.config b/BepInExTests/packages.config new file mode 100644 index 0000000..d2da0eb --- /dev/null +++ b/BepInExTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 0e4b05316598470779a1daa23f71009f047099fc Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 16:52:04 +0200 Subject: [PATCH 04/54] Fixed solution configurations --- BepInEx.Preloader/BepInEx.Preloader.csproj | 17 ++++ BepInEx.sln | 99 +++------------------- BepInEx/BepInEx.csproj | 16 +--- BepInExTests/BepInExTests.csproj | 19 ----- 4 files changed, 32 insertions(+), 119 deletions(-) diff --git a/BepInEx.Preloader/BepInEx.Preloader.csproj b/BepInEx.Preloader/BepInEx.Preloader.csproj index ac123d9..1be0603 100644 --- a/BepInEx.Preloader/BepInEx.Preloader.csproj +++ b/BepInEx.Preloader/BepInEx.Preloader.csproj @@ -31,10 +31,27 @@ prompt MinimumRecommendedRules.ruleset + + bin\Debug\ + TRACE;UNITY_2018 + true + AnyCPU + prompt + MinimumRecommendedRules.ruleset + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll + + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll + + + ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll + diff --git a/BepInEx.sln b/BepInEx.sln index 377c327..2538f19 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -35,159 +35,82 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|Test = Debug|Test Legacy|Any CPU = Legacy|Any CPU - Legacy|Test = Legacy|Test Release|Any CPU = Release|Any CPU - Release|Test = Release|Test v2018|Any CPU = v2018|Any CPU - v2018|Test = v2018|Test EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.ActiveCfg = v2018|x86 - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.Build.0 = v2018|x86 + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.ActiveCfg = Legacy|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.Build.0 = Legacy|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = v2018|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.ActiveCfg = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.Build.0 = v2018|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.ActiveCfg = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.Build.0 = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.Build.0 = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.ActiveCfg = Legacy|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.Build.0 = Legacy|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.Build.0 = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.Build.0 = v2018|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Test.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Test.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Test.ActiveCfg = Debug|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Test.Build.0 = Debug|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.Build.0 = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.ActiveCfg = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.ActiveCfg = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.ActiveCfg = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Test.ActiveCfg = Debug|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Test.Build.0 = Debug|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.Build.0 = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.ActiveCfg = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.ActiveCfg = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.ActiveCfg = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.Build.0 = Release|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.Build.0 = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.ActiveCfg = Legacy|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.Build.0 = Legacy|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.Build.0 = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.Build.0 = v2018|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Test.ActiveCfg = Debug|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Test.Build.0 = Debug|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.Build.0 = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.ActiveCfg = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.Build.0 = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.ActiveCfg = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.Build.0 = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.Build.0 = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.ActiveCfg = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Test.ActiveCfg = Debug|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Test.Build.0 = Debug|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.Build.0 = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.ActiveCfg = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.ActiveCfg = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.Build.0 = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.ActiveCfg = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.ActiveCfg = Debug|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.Build.0 = Debug|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.Build.0 = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.ActiveCfg = Release|Any CPU - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.Build.0 = Release|Any CPU + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.ActiveCfg = Release|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.Build.0 = Release|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.ActiveCfg = Release|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index ee37d48..c6055d0 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -35,19 +35,11 @@ MinimumRecommendedRules.ruleset ..\bin\BepInEx.xml - - bin\x86\Legacy\ - DEBUG;TRACE - false - x86 - prompt - MinimumRecommendedRules.ruleset - full - true - - - bin\x86\v2018\ + + bin\Debug\ DEBUG;TRACE;UNITY_2018 + + false x86 prompt diff --git a/BepInExTests/BepInExTests.csproj b/BepInExTests/BepInExTests.csproj index b9ebf21..d172ea5 100644 --- a/BepInExTests/BepInExTests.csproj +++ b/BepInExTests/BepInExTests.csproj @@ -26,25 +26,6 @@ 15.0 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - true bin\x86\Debug\ From 1357478c3d145bd0710df902d58d375311b4bd92 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 17:11:02 +0200 Subject: [PATCH 05/54] Initialize Paths in unit tests to avoid having to do hack arounds --- BepInEx/Configuration/ConfigFile.cs | 4 +--- BepInEx/Properties/AssemblyInfo.cs | 1 + BepInExTests/AssemblyInit.cs | 29 +++++++++++++++++++++++++++++ BepInExTests/BepInExTests.csproj | 1 + 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 BepInExTests/AssemblyInit.cs diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 85ae3c1..390d427 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -13,9 +13,7 @@ namespace BepInEx.Configuration /// public class ConfigFile { - // Need to be lazy evaluated to not cause problems for unit tests - private static ConfigFile _coreConfig; - internal static ConfigFile CoreConfig => _coreConfig ?? (_coreConfig = new ConfigFile(Paths.BepInExConfigPath, true)); + internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true); protected Dictionary Entries { get; } = new Dictionary(); diff --git a/BepInEx/Properties/AssemblyInfo.cs b/BepInEx/Properties/AssemblyInfo.cs index 0b6cf69..1da00e3 100644 --- a/BepInEx/Properties/AssemblyInfo.cs +++ b/BepInEx/Properties/AssemblyInfo.cs @@ -25,6 +25,7 @@ using BepInEx; [assembly: Guid("4ffba620-f5ed-47f9-b90c-dad1316fd9b9")] [assembly: InternalsVisibleTo("BepInEx.Preloader")] +[assembly: InternalsVisibleTo("BepInExTests")] // Version information for an assembly consists of the following four values: // diff --git a/BepInExTests/AssemblyInit.cs b/BepInExTests/AssemblyInit.cs new file mode 100644 index 0000000..4c4fc15 --- /dev/null +++ b/BepInExTests/AssemblyInit.cs @@ -0,0 +1,29 @@ +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BepInEx.Tests +{ + [TestClass] + public class AssemblyInit + { + private static string _testPath; + + [AssemblyInitialize] + public static void InitAss(TestContext context) + { + _testPath = Path.Combine(Path.GetTempPath(), "BepinexTestDir"); + Directory.CreateDirectory(_testPath); + + string exePath = Path.Combine(_testPath, "Text.exe"); + File.WriteAllBytes(exePath, new byte[] { }); + + Paths.SetExecutablePath(_testPath); + } + + [AssemblyCleanup] + public static void CleanupAss() + { + Directory.Delete(_testPath, true); + } + } +} \ No newline at end of file diff --git a/BepInExTests/BepInExTests.csproj b/BepInExTests/BepInExTests.csproj index d172ea5..43e94f1 100644 --- a/BepInExTests/BepInExTests.csproj +++ b/BepInExTests/BepInExTests.csproj @@ -62,6 +62,7 @@ + From 970438883c44aadbc69168c1effa9c47d2471aea Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 18:24:54 +0200 Subject: [PATCH 06/54] Added documentation, misc accessibility changes --- BepInEx/Configuration/ConfigDefinition.cs | 31 +++++++++ BepInEx/Configuration/ConfigDescription.cs | 16 ++++- BepInEx/Configuration/ConfigEntry.cs | 35 +++++++++- BepInEx/Configuration/ConfigFile.cs | 68 ++++++++++++++++--- BepInEx/Configuration/ConfigWrapper.cs | 17 +++++ .../Configuration/SettingChangedEventArgs.cs | 8 +++ BepInEx/Configuration/TomlTypeConverter.cs | 2 +- 7 files changed, 165 insertions(+), 12 deletions(-) diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index ff49c71..628b400 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -2,18 +2,38 @@ namespace BepInEx.Configuration { + /// + /// Section and key of a setting. Used as a unique key for identification within a . + /// The same definition can be used in multiple config files, it will point to different settings then. + /// + /// public class ConfigDefinition : IEquatable { + /// + /// Group of the setting. All settings within a config file are grouped by this. + /// public string Section { get; } + /// + /// Name of the setting. + /// public string Key { get; } + /// + /// Create a new definition. Definitions with same section and key are equal. + /// + /// Group of the setting, case sensitive. + /// Name of the setting, case sensitive. public ConfigDefinition(string section, string key) { Key = key; Section = section; } + /// + /// Check if the definitions are the same. + /// + /// public bool Equals(ConfigDefinition other) { if (other == null) return false; @@ -21,6 +41,9 @@ namespace BepInEx.Configuration && string.Equals(Section, other.Section); } + /// + /// Check if the definitions are the same. + /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) @@ -31,6 +54,7 @@ namespace BepInEx.Configuration return Equals(obj as ConfigDefinition); } + /// public override int GetHashCode() { unchecked @@ -41,12 +65,19 @@ namespace BepInEx.Configuration } } + /// + /// Check if the definitions are the same. + /// public static bool operator ==(ConfigDefinition left, ConfigDefinition right) => Equals(left, right); + /// + /// Check if the definitions are the same. + /// public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right); + /// public override string ToString() { return Section + " / " + Key; diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs index f14216f..2b0df1f 100644 --- a/BepInEx/Configuration/ConfigDescription.cs +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -2,17 +2,29 @@ namespace BepInEx.Configuration { + //todo value range + /// + /// Metadata of a . + /// public class ConfigDescription { + /// + /// Create a new description. + /// + /// Text describing the function of the setting and any notes or warnings. public ConfigDescription(string description) { Description = description ?? throw new ArgumentNullException(nameof(description)); } + /// + /// Text describing the function of the setting and any notes or warnings. + /// public string Description { get; } - //todo value range - + /// + /// Convert the description object into a form suitable for writing into a config file. + /// public string ToSerializedString() { return $"# {Description.Replace("\n", "\n# ")}"; diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index 6ba9900..51c7339 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -5,6 +5,10 @@ using BepInEx.Logging; namespace BepInEx.Configuration { + /// + /// Container for a single setting of a . + /// Each config entry is linked to one config file. + /// public sealed class ConfigEntry { internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) : this(configFile, definition) @@ -49,12 +53,29 @@ namespace BepInEx.Configuration private object _convertedValue; private string _serializedValue; + /// + /// Config file this entry is a part of. + /// public ConfigFile ConfigFile { get; } + + /// + /// Category and name of this setting. Used as a unique key for identification within a . + /// public ConfigDefinition Definition { get; } + /// + /// Description / metadata of this setting. + /// public ConfigDescription Description { get; internal set; } + /// + /// Type of the that this setting holds. + /// public Type SettingType { get; private set; } + + /// + /// Default value of this setting (set only if the setting was not changed before). + /// public object DefaultValue { get; private set; } /// @@ -64,6 +85,7 @@ namespace BepInEx.Configuration public bool IsDefined => SettingType != null; /// + /// Get or set the value of the setting. /// Can't be used when is false. /// public object Value @@ -91,6 +113,9 @@ namespace BepInEx.Configuration } } + /// + /// Get the serialized representation of the value. + /// public string GetSerializedValue() { if (_serializedValue != null) @@ -102,7 +127,12 @@ namespace BepInEx.Configuration return TomlTypeConverter.ConvertToString(Value, SettingType); } - public void SetSerializedValue(string newValue, bool fireEvent, object sender) + /// + /// Set the value by using its serialized form. + /// + public void SetSerializedValue(string newValue) => SetSerializedValue(newValue, true, this); + + internal void SetSerializedValue(string newValue, bool fireEvent, object sender) { string current = GetSerializedValue(); if (string.Equals(current, newValue)) return; @@ -162,6 +192,9 @@ namespace BepInEx.Configuration ConfigFile.OnSettingChanged(sender, this); } + /// + /// Write a description of this setting using all available metadata. + /// public void WriteDescription(StreamWriter writer) { if (Description != null) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 390d427..228c415 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -15,20 +15,40 @@ namespace BepInEx.Configuration { internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true); + /// + /// All config entries inside + /// protected Dictionary Entries { get; } = new Dictionary(); - [Obsolete("Use ConfigEntries instead")] + /// + /// Create a list with all config entries inside of this config file. + /// + [Obsolete("Use GetConfigEntries instead")] public ReadOnlyCollection ConfigDefinitions => Entries.Keys.ToList().AsReadOnly(); - public ReadOnlyCollection ConfigEntries => Entries.Values.ToList().AsReadOnly(); + /// + /// 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 + /// instead with no description. + /// + public ConfigEntry[] GetConfigEntries() => Entries.Values.ToArray(); + /// + /// Full path to the config file. The file might not exist until a setting is added and changed, or is called. + /// public string ConfigFilePath { get; } /// - /// If enabled, writes the config to disk every time a value is set. If disabled, you have to manually save or the changes will be lost! + /// If enabled, writes the config to disk every time a value is set. + /// If disabled, you have to manually use or the changes will be lost! /// public bool SaveOnConfigSet { get; set; } = true; + /// + /// Create a new config file at the specified config path. + /// + /// Full path to a file that contains settings. The file will be created as needed. + /// If the config file/directory doesn't exist, create it immediately. public ConfigFile(string configPath, bool saveOnInit) { if (configPath == null) throw new ArgumentNullException(nameof(configPath)); @@ -138,6 +158,16 @@ namespace BepInEx.Configuration #region Wraps + /// + /// 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. + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user. + /// public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) { if (!TomlTypeConverter.CanConvert(typeof(T))) @@ -166,10 +196,26 @@ namespace BepInEx.Configuration return new ConfigWrapper(entry); } + /// + /// 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. + /// [Obsolete("Use other Wrap overloads instead")] public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) => Wrap(new ConfigDefinition(section, key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + /// + /// 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. + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user. + /// public ConfigWrapper Wrap(string section, string key, T defaultValue, ConfigDescription configDescription = null) => Wrap(new ConfigDefinition(section, key), defaultValue, configDescription); @@ -187,7 +233,7 @@ namespace BepInEx.Configuration /// public event EventHandler SettingChanged; - protected internal void OnSettingChanged(object sender, ConfigEntry changedEntry) + internal void OnSettingChanged(object sender, ConfigEntry changedEntry) { if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry)); @@ -208,12 +254,13 @@ namespace BepInEx.Configuration } } - // todo better way to prevent write loop? maybe do some caching? + // Check sender to prevent infinite loops + // todo batching / async? if (sender != this && SaveOnConfigSet) Save(); } - protected void OnConfigReloaded() + private void OnConfigReloaded() { if (ConfigReloaded != null) { @@ -266,11 +313,16 @@ namespace BepInEx.Configuration { lock (_ioLock) { - _watcher?.Dispose(); - _watcher = null; + if (_watcher != null) + { + _watcher.EnableRaisingEvents = false; + _watcher.Dispose(); + _watcher = null; + } } } + /// ~ConfigFile() { StopWatching(); diff --git a/BepInEx/Configuration/ConfigWrapper.cs b/BepInEx/Configuration/ConfigWrapper.cs index 8f919f6..1e1f0eb 100644 --- a/BepInEx/Configuration/ConfigWrapper.cs +++ b/BepInEx/Configuration/ConfigWrapper.cs @@ -2,11 +2,25 @@ namespace BepInEx.Configuration { + /// + /// Provides access to a single setting inside of a . + /// + /// Type of the setting. public sealed class ConfigWrapper { + /// + /// Entry of this setting in the . + /// public ConfigEntry ConfigEntry { get; } + /// + /// Unique definition of this setting. + /// public ConfigDefinition Definition => ConfigEntry.Definition; + + /// + /// Config file this setting is inside of. + /// public ConfigFile ConfigFile => ConfigEntry.ConfigFile; /// @@ -14,6 +28,9 @@ namespace BepInEx.Configuration /// public event EventHandler SettingChanged; + /// + /// Value of this setting. + /// public T Value { get => (T)ConfigEntry.Value; diff --git a/BepInEx/Configuration/SettingChangedEventArgs.cs b/BepInEx/Configuration/SettingChangedEventArgs.cs index 3767e9b..53d81b2 100644 --- a/BepInEx/Configuration/SettingChangedEventArgs.cs +++ b/BepInEx/Configuration/SettingChangedEventArgs.cs @@ -2,13 +2,21 @@ namespace BepInEx.Configuration { + /// + /// Arguments for events concerning a change of a setting. + /// + /// public sealed class SettingChangedEventArgs : EventArgs { + /// public SettingChangedEventArgs(ConfigEntry changedSetting) { ChangedSetting = changedSetting; } + /// + /// Setting that was changed + /// public ConfigEntry ChangedSetting { get; } } } \ No newline at end of file diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 8a836a3..aff8bde 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -4,7 +4,7 @@ using System.Globalization; namespace BepInEx.Configuration { - public class TypeConverter + internal class TypeConverter { public Func ConvertToString { get; set; } public Func ConvertToObject { get; set; } From ab3509ed7b5ff86dba073c5cccd29c6d7e7327f8 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 18:37:00 +0200 Subject: [PATCH 07/54] Added BaseUnityPlugin.Metadata and some xmldocs --- BepInEx/Contract/Attributes.cs | 2 +- BepInEx/Contract/BaseUnityPlugin.cs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/BepInEx/Contract/Attributes.cs b/BepInEx/Contract/Attributes.cs index b19f19b..9ef80be 100644 --- a/BepInEx/Contract/Attributes.cs +++ b/BepInEx/Contract/Attributes.cs @@ -116,7 +116,7 @@ namespace BepInEx /// /// Retrieves the BepInPlugin metadata from a plugin type. /// - /// The plugin type. + /// The plugin type. /// The BepInPlugin metadata of the plugin type. public static BepInPlugin GetMetadata(Type pluginType) { diff --git a/BepInEx/Contract/BaseUnityPlugin.cs b/BepInEx/Contract/BaseUnityPlugin.cs index 23296b4..7781a46 100644 --- a/BepInEx/Contract/BaseUnityPlugin.cs +++ b/BepInEx/Contract/BaseUnityPlugin.cs @@ -9,17 +9,32 @@ namespace BepInEx /// public abstract class BaseUnityPlugin : MonoBehaviour { + /// + /// Information about this plugin as it was loaded. + /// + public BepInPlugin Metadata { get; } + + /// + /// Logger instance tied to this plugin. + /// protected ManualLogSource Logger { get; } + /// + /// Default config file tied to this plugin. The config file will not be created until + /// any settings are added and changed, or is called. + /// protected ConfigFile Config { get; } + /// + /// Create a new instance of a plugin and all of its tied in objects. + /// protected BaseUnityPlugin() { - var metadata = MetadataHelper.GetMetadata(this); + Metadata = MetadataHelper.GetMetadata(this); - Logger = Logging.Logger.CreateLogSource(metadata.Name); + Logger = Logging.Logger.CreateLogSource(Metadata.Name); - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false); + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Metadata.GUID + ".cfg"), false, this); } } } \ No newline at end of file From e6e8041bf821b2dcfb2439b7c3089ca282f2308d Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 18:41:43 +0200 Subject: [PATCH 08/54] Added information of owner plugin to config files --- BepInEx/Configuration/ConfigFile.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 228c415..040a4d6 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -13,6 +13,8 @@ namespace BepInEx.Configuration /// public class ConfigFile { + private readonly BepInPlugin _ownerMetadata; + internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true); /// @@ -49,12 +51,13 @@ namespace BepInEx.Configuration /// /// Full path to a file that contains settings. The file will be created as needed. /// If the config file/directory doesn't exist, create it immediately. - public ConfigFile(string configPath, bool saveOnInit) + /// The plugin that owns this setting. + public ConfigFile(string configPath, bool saveOnInit, BaseUnityPlugin owner = null) { + _ownerMetadata = owner?.Metadata; + if (configPath == null) throw new ArgumentNullException(nameof(configPath)); - configPath = Path.GetFullPath(configPath); - ConfigFilePath = configPath; if (File.Exists(ConfigFilePath)) @@ -132,6 +135,13 @@ namespace BepInEx.Configuration using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8)) { + if (_ownerMetadata != null) + { + writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}"); + writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}"); + writer.WriteLine(); + } + foreach (var sectionKv in Entries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) { // Section heading From ea42a9932a2b751eabe58905c0a76e0a1a88bf88 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 18:58:16 +0200 Subject: [PATCH 09/54] Clean up parameter names --- BepInEx/Configuration/ConfigEntry.cs | 2 +- BepInEx/Contract/Attributes.cs | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index 51c7339..8924592 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -37,7 +37,7 @@ namespace BepInEx.Configuration { 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."); + "Type for all Wrappers of a single setting."); } if (defaultValue == null && settingType.IsValueType) diff --git a/BepInEx/Contract/Attributes.cs b/BepInEx/Contract/Attributes.cs index 9ef80be..3e2eddf 100644 --- a/BepInEx/Contract/Attributes.cs +++ b/BepInEx/Contract/Attributes.cs @@ -29,21 +29,21 @@ namespace BepInEx /// public Version Version { get; protected set; } - /// The unique identifier of the plugin. Should not change between plugin versions. - /// The user friendly name of the plugin. Is able to be changed between versions. - /// The specfic version of the plugin. - public BepInPlugin(string GUID, string Name, string Version) + /// The unique identifier of the plugin. Should not change between plugin versions. + /// The user friendly name of the plugin. Is able to be changed between versions. + /// The specfic version of the plugin. + public BepInPlugin(string guid, string name, string version) { - this.GUID = GUID; - this.Name = Name; + GUID = guid; + Name = name; try { - this.Version = new Version(Version); + Version = new Version(version); } catch { - this.Version = null; + Version = null; } } } @@ -140,7 +140,7 @@ namespace BepInEx /// Gets the specified attributes of a type, if they exist. /// /// The attribute type to retrieve. - /// The plugin type. + /// The plugin type. /// The attributes of the type, if existing. public static T[] GetAttributes(Type pluginType) where T : Attribute { @@ -159,18 +159,19 @@ namespace BepInEx /// /// Retrieves the dependencies of the specified plugin type. /// - /// The plugin type. - /// All currently loaded plugin types. + /// The plugin type. + /// All currently loaded plugin types. /// A list of all plugin types that the specified plugin type depends upon. - public static IEnumerable GetDependencies(Type Plugin, IEnumerable AllPlugins) + public static IEnumerable GetDependencies(Type plugin, IEnumerable allPlugins) { - return Plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast(); + return plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast(); } } /// /// An exception which is thrown when a plugin's dependencies cannot be found. /// + [Obsolete] public class MissingDependencyException : Exception { public MissingDependencyException(string message) : base(message) { } From d9bf3ba22518b3a78298c12a2721928c23236ede Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 22:36:58 +0200 Subject: [PATCH 10/54] Added AcceptableValue stuff --- BepInEx/BepInEx.csproj | 3 + BepInEx/Configuration/AcceptableValueBase.cs | 23 +++++++ BepInEx/Configuration/AcceptableValueList.cs | 46 +++++++++++++ BepInEx/Configuration/AcceptableValueRange.cs | 59 ++++++++++++++++ BepInEx/Configuration/ConfigDescription.cs | 19 ++++- BepInEx/Configuration/ConfigEntry.cs | 52 +++++++++++--- BepInEx/Configuration/ConfigFile.cs | 9 ++- BepInExTests/BepInExTests.csproj | 3 + BepInExTests/Configuration/ConfigFileTests.cs | 69 ++++++++++++++++++- 9 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 BepInEx/Configuration/AcceptableValueBase.cs create mode 100644 BepInEx/Configuration/AcceptableValueList.cs create mode 100644 BepInEx/Configuration/AcceptableValueRange.cs diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index c6055d0..01f0a6f 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -67,6 +67,9 @@ + + + diff --git a/BepInEx/Configuration/AcceptableValueBase.cs b/BepInEx/Configuration/AcceptableValueBase.cs new file mode 100644 index 0000000..bae8416 --- /dev/null +++ b/BepInEx/Configuration/AcceptableValueBase.cs @@ -0,0 +1,23 @@ +namespace BepInEx.Configuration +{ + /// + /// Base type of all classes represeting and enforcing acceptable values of config settings. + /// + public abstract class AcceptableValueBase + { + /// + /// Change the value to be acceptable, if it's not already. + /// + public abstract object Clamp(object value); + + /// + /// Check if the value is an acceptable value. + /// + public abstract bool IsValid(object value); + + /// + /// Get the string for use in config files. + /// + public abstract string ToSerializedString(); + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs new file mode 100644 index 0000000..9e0c6cd --- /dev/null +++ b/BepInEx/Configuration/AcceptableValueList.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; + +namespace BepInEx.Configuration +{ + /// + /// Specify the list of acceptable values for a setting. + /// + public sealed class AcceptableValueList : AcceptableValueBase where T : IEquatable + { + private readonly T[] _acceptableValues; + + /// + /// 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. + /// + public AcceptableValueList(params T[] acceptableValues) + { + if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues)); + if (acceptableValues.Length == 0) throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues)); + + _acceptableValues = acceptableValues; + } + + /// + public override object Clamp(object value) + { + if (value is T v && _acceptableValues.Any(x => x.Equals(v))) + return value; + + return _acceptableValues[0]; + } + + /// + public override bool IsValid(object value) + { + return value is T v && _acceptableValues.Any(x => x.Equals(v)); + } + + /// + public override string ToSerializedString() + { + return "# Acceptable values: " + string.Join(", ", _acceptableValues.Select(x => x.ToString()).ToArray()); + } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/AcceptableValueRange.cs b/BepInEx/Configuration/AcceptableValueRange.cs new file mode 100644 index 0000000..bab318a --- /dev/null +++ b/BepInEx/Configuration/AcceptableValueRange.cs @@ -0,0 +1,59 @@ +using System; + +namespace BepInEx.Configuration +{ + /// + /// Specify the range of acceptable values for a setting. + /// + public sealed class AcceptableValueRange : AcceptableValueBase where T : IComparable + { + /// Lowest acceptable value + /// Highest acceptable value + public AcceptableValueRange(T minValue, T maxValue) + { + if (maxValue == null) + throw new ArgumentNullException(nameof(maxValue)); + if (minValue == null) + throw new ArgumentNullException(nameof(minValue)); + if (minValue.CompareTo(maxValue) >= 0) + throw new ArgumentException($"{nameof(minValue)} has to be lower than {nameof(maxValue)}"); + + MinValue = minValue; + MaxValue = maxValue; + } + + /// + /// Lowest acceptable value + /// + public T MinValue { get; } + + /// + /// Highest acceptable value + /// + public T MaxValue { get; } + + /// + public override object Clamp(object value) + { + if (MinValue.CompareTo(value) > 0) + return MinValue; + + if (MaxValue.CompareTo(value) < 0) + return MaxValue; + + return value; + } + + /// + public override bool IsValid(object value) + { + return MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0; + } + + /// + public override string ToSerializedString() + { + return $"# Acceptable value range: From {MinValue} to {MaxValue}"; + } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs index 2b0df1f..856bbd8 100644 --- a/BepInEx/Configuration/ConfigDescription.cs +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -2,7 +2,6 @@ namespace BepInEx.Configuration { - //todo value range /// /// Metadata of a . /// @@ -12,8 +11,12 @@ namespace BepInEx.Configuration /// Create a new description. /// /// Text describing the function of the setting and any notes or warnings. - public ConfigDescription(string description) + /// Range of values that this setting can take. The setting's value will be automatically clamped. + /// Objects that can be used by user-made classes to add functionality. + public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags) { + AcceptableValues = acceptableValues; + Tags = tags; Description = description ?? throw new ArgumentNullException(nameof(description)); } @@ -22,6 +25,16 @@ namespace BepInEx.Configuration /// public string Description { get; } + /// + /// Range of acceptable values for a setting. + /// + public AcceptableValueBase AcceptableValues { get; } + + /// + /// Objects that can be used by user-made classes to add functionality. + /// + public object[] Tags { get; } + /// /// Convert the description object into a form suitable for writing into a config file. /// @@ -30,4 +43,4 @@ namespace BepInEx.Configuration return $"# {Description.Replace("\n", "\n# ")}"; } } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index 8924592..b423c08 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -36,8 +36,8 @@ namespace BepInEx.Configuration 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."); + $"while it was already defined as type {SettingType.Name}. Use the same " + + "Type for all Wrappers of a single setting."); } if (defaultValue == null && settingType.IsValueType) @@ -66,7 +66,7 @@ namespace BepInEx.Configuration /// /// Description / metadata of this setting. /// - public ConfigDescription Description { get; internal set; } + public ConfigDescription Description { get; private set; } /// /// Type of the that this setting holds. @@ -102,6 +102,7 @@ namespace BepInEx.Configuration internal void SetValue(object newValue, bool fireEvent, object sender) { bool wasChanged = ProcessSerializedValue(); + newValue = ClampValue(newValue); wasChanged = wasChanged || !Equals(newValue, _convertedValue); if (wasChanged) @@ -140,7 +141,7 @@ namespace BepInEx.Configuration _serializedValue = newValue; if (!IsDefined) return; - + if (ProcessSerializedValue()) { if (fireEvent) @@ -163,6 +164,7 @@ namespace BepInEx.Configuration try { var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); + newValue = ClampValue(newValue); if (!Equals(newValue, _convertedValue)) { _convertedValue = newValue; @@ -173,7 +175,7 @@ namespace BepInEx.Configuration 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}"); + $"parsed and will be ignored. Reason: {e.Message}; Value: {value}"); } } } @@ -187,6 +189,19 @@ namespace BepInEx.Configuration return false; } + internal void SetDescription(ConfigDescription configDescription) + { + Description = configDescription; + SetValue(ClampValue(Value), true, this); + } + + private object ClampValue(object value) + { + if (Description?.AcceptableValues != null) + return Description.AcceptableValues.Clamp(value); + return value; + } + private void OnSettingChanged(object sender) { ConfigFile.OnSettingChanged(sender, this); @@ -197,18 +212,35 @@ namespace BepInEx.Configuration /// public void WriteDescription(StreamWriter writer) { - if (Description != null) + bool hasDescription = Description != null; + bool hasType = SettingType != null; + + if (hasDescription) writer.WriteLine(Description.ToSerializedString()); - if (SettingType != null) + if (hasType) { writer.WriteLine("# Setting type: " + SettingType.Name); - writer.WriteLine("# Default value: " + DefaultValue); - - // todo acceptable values if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + + writer.WriteLine("# Default value: " + DefaultValue); + } + + if (hasDescription) + { + 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))); + } } } } diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 040a4d6..32b2478 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -200,7 +200,14 @@ namespace BepInEx.Configuration 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."); - entry.Description = configDescription; + 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)); + } + + entry.SetDescription(configDescription); } return new ConfigWrapper(entry); diff --git a/BepInExTests/BepInExTests.csproj b/BepInExTests/BepInExTests.csproj index 43e94f1..a7c1be5 100644 --- a/BepInExTests/BepInExTests.csproj +++ b/BepInExTests/BepInExTests.csproj @@ -52,6 +52,9 @@ ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + ..\lib\UnityEngine.dll + diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index 3fde291..a65a125 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -168,5 +168,72 @@ namespace BepInEx.Configuration.Tests Assert.IsTrue(eventFired); } + + [TestMethod] + public void ValueRangeTest() + { + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + + Assert.AreEqual(0, w.Value); + w.Value = 2; + Assert.AreEqual(2, w.Value); + w.Value = -2; + Assert.AreEqual(0, w.Value); + w.Value = 4; + Assert.AreEqual(2, w.Value); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ValueRangeBadTypeTest() + { + var c = MakeConfig(); + c.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + Assert.Fail(); + } + + [TestMethod] + public void ValueRangeDefaultTest() + { + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + + Assert.AreEqual(w.Value, 1); + } + + [TestMethod] + public void ValueRangeLoadTest() + { + var c = MakeConfig(); + c.StopWatching(); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n"); + c.Reload(); + + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + + Assert.AreEqual(w.Value, 1); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 5\n"); + c.Reload(); + + Assert.AreEqual(w.Value, 2); + } + + [TestMethod] + public void ValueListTest() + { + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList("lel", "kek", "wew", "why"))); + + Assert.AreEqual("kek", w.Value); + w.Value = "wew"; + Assert.AreEqual("wew", w.Value); + w.Value = "no"; + Assert.AreEqual("lel", w.Value); + w.Value = null; + Assert.AreEqual("lel", w.Value); + } } -} \ No newline at end of file +} From 841cdd5b77b879845289f15c7c905980cc0cbf3b Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 12 Aug 2019 23:48:18 +0200 Subject: [PATCH 11/54] Added KeyboardShortcut --- BepInEx/BepInEx.csproj | 2 + BepInEx/Configuration/KeyboardShortcut.cs | 179 ++++++++++++++++++ BepInEx/Configuration/TomlTypeConverter.cs | 33 ++-- BepInEx/Configuration/TypeConverter.cs | 22 +++ BepInExTests/Configuration/ConfigFileTests.cs | 18 ++ 5 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 BepInEx/Configuration/KeyboardShortcut.cs create mode 100644 BepInEx/Configuration/TypeConverter.cs diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index 01f0a6f..fdb963c 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -75,8 +75,10 @@ + + diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs new file mode 100644 index 0000000..79b2aba --- /dev/null +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BepInEx.Logging; +using UnityEngine; +using Logger = BepInEx.Logging.Logger; + +namespace BepInEx.Configuration +{ + /// + /// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only + /// triggered when the user presses the exact combination. For example, F + LeftCtrl 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 + /// to allow user to change this shortcut and have the changes saved. + /// + /// How to use: Use in this class instead of in the Update loop. + /// + public class KeyboardShortcut + { + static KeyboardShortcut() + { + TomlTypeConverter.AddConverter( + typeof(KeyboardShortcut), + new TypeConverter + { + ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize(), + ConvertToObject = (s, type) => Deserialize(s) + }); + } + + /// + /// All KeyCode values that can be used in a keyboard shortcut. + /// + public static readonly IEnumerable AllKeyCodes = (KeyCode[])Enum.GetValues(typeof(KeyCode)); + + private readonly KeyCode[] _allKeys; + private readonly HashSet _allKeysLookup; + + /// + /// Create a new keyboard shortcut. + /// + /// Main key to press + /// Keys that should be held down before main key is registered + public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers).ToArray()) + { + if (mainKey == KeyCode.None && modifiers.Any()) + throw new ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}"); + } + + private KeyboardShortcut(KeyCode[] keys) + { + _allKeys = SanitizeKeys(keys); + _allKeysLookup = new HashSet(_allKeys); + } + + public KeyboardShortcut() + { + _allKeys = SanitizeKeys(); + _allKeysLookup = new HashSet(_allKeys); + } + + private static KeyCode[] SanitizeKeys(params KeyCode[] keys) + { + if (keys.Length == 0 || keys[0] == KeyCode.None) + return new[] { KeyCode.None }; + + return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int)x)).ToArray(); + } + + /// + /// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered. + /// If the combination is empty, is returned. + /// + public KeyCode MainKey => _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; + + /// + /// Modifiers of the key combination, if any. + /// + public IEnumerable Modifiers => _allKeys.Skip(1); + + /// + /// Attempt to deserialize key combination from the string. + /// + public static KeyboardShortcut Deserialize(string str) + { + try + { + var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => (KeyCode)Enum.Parse(typeof(KeyCode), x)).ToArray(); + return new KeyboardShortcut(parts); + } + catch (SystemException ex) + { + Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message); + return new KeyboardShortcut(); + } + } + + /// + /// Serialize the key combination into a user readable string. + /// + public string Serialize() + { + return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray()); + } + + /// + /// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed + /// + public bool IsDown() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyDown(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed + /// + public bool IsPressed() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKey(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed. + /// + public bool IsUp() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyUp(mainKey) && ModifierKeyTest(); + } + + private bool ModifierKeyTest() + { + return AllKeyCodes.All(c => + { + if (_allKeysLookup.Contains(c)) + { + if (_allKeys[0] == c) + return true; + return Input.GetKey(c); + } + return !Input.GetKey(c); + }); + } + + /// + public override string ToString() + { + if (MainKey == KeyCode.None) return "Not set"; + + return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray()); + } + + /// + public override bool Equals(object obj) + { + return obj is KeyboardShortcut shortcut && _allKeys.SequenceEqual(shortcut._allKeys); + } + + /// + public override int GetHashCode() + { + var hc = _allKeys.Length; + for (var i = 0; i < _allKeys.Length; i++) + hc = unchecked(hc * 31 + (int)_allKeys[i]); + return hc; + } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index aff8bde..2376653 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -4,15 +4,12 @@ using System.Globalization; namespace BepInEx.Configuration { - internal class TypeConverter + /// + /// Serializer/deserializer used by the config system. + /// + public static class TomlTypeConverter { - public Func ConvertToString { get; set; } - public Func ConvertToObject { get; set; } - } - - internal static class TomlTypeConverter - { - public static Dictionary TypeConverters { get; } = new Dictionary + private static Dictionary TypeConverters { get; } = new Dictionary { [typeof(string)] = new TypeConverter { @@ -91,6 +88,8 @@ namespace BepInEx.Configuration ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo), }, + //enums are special + [typeof(Enum)] = new TypeConverter { ConvertToString = (obj, type) => obj.ToString(), @@ -116,17 +115,29 @@ namespace BepInEx.Configuration { var conv = GetConverter(valueType); if (conv == null) - throw new InvalidOperationException($"Cannot convert to type {valueType}"); + throw new InvalidOperationException($"Cannot convert to type {valueType.Name}"); return conv.ConvertToObject(value, valueType); } - private static TypeConverter GetConverter(Type valueType) + public static TypeConverter GetConverter(Type valueType) { + if (valueType == null) throw new ArgumentNullException(nameof(valueType)); + if (valueType.IsEnum) return TypeConverters[typeof(Enum)]; - return TypeConverters[valueType]; + TypeConverters.TryGetValue(valueType, out var result); + return result; + } + + public static void AddConverter(Type type, TypeConverter converter) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (converter == null) throw new ArgumentNullException(nameof(converter)); + if (CanConvert(type)) throw new ArgumentException("The specified type already has a converter assigned to it", nameof(type)); + + TypeConverters.Add(type, converter); } public static bool CanConvert(Type type) diff --git a/BepInEx/Configuration/TypeConverter.cs b/BepInEx/Configuration/TypeConverter.cs new file mode 100644 index 0000000..1f1b2db --- /dev/null +++ b/BepInEx/Configuration/TypeConverter.cs @@ -0,0 +1,22 @@ +using System; + +namespace BepInEx.Configuration +{ + /// + /// A serializer/deserializer combo for some type(s). Used by the config system. + /// + public class TypeConverter + { + /// + /// Used to serialize the type into a (hopefully) human-readable string. + /// Object is the instance to serialize, Type is the object's type. + /// + public Func ConvertToString { get; set; } + + /// + /// Used to deserialize the type from a string. + /// String is the data to deserialize, Type is the object's type, should return instance to an object of Type. + /// + public Func ConvertToObject { get; set; } + } +} \ No newline at end of file diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index a65a125..96f8617 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; using System.Linq; using System.Threading; +using UnityEngine; namespace BepInEx.Configuration.Tests { @@ -235,5 +236,22 @@ namespace BepInEx.Configuration.Tests w.Value = null; Assert.AreEqual("lel", w.Value); } + + [TestMethod] + public void KeyShortcutTest() + { + var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y); + var s = shortcut.Serialize(); + var d = KeyboardShortcut.Deserialize(s); + Assert.AreEqual(shortcut, d); + + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); + Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value); + + w.Value = shortcut; + c.Reload(); + Assert.AreEqual(shortcut, w.Value); + } } } From e7028d2fee286560d364b36f942d438b90ebf888 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 17 Aug 2019 01:02:16 +0200 Subject: [PATCH 12/54] Move PluginInfo to correct namespace --- BepInEx/Contract/PluginInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx/Contract/PluginInfo.cs b/BepInEx/Contract/PluginInfo.cs index 5cf8506..fdd2cbb 100644 --- a/BepInEx/Contract/PluginInfo.cs +++ b/BepInEx/Contract/PluginInfo.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using BepInEx.Bootstrap; -namespace BepInEx.Contract +namespace BepInEx { public class PluginInfo : ICacheable { @@ -60,4 +60,4 @@ namespace BepInEx.Contract Dependencies = depList; } } -} \ No newline at end of file +} From 4711b1e5fa35bb847c4252286f666d70370b440c Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 17 Aug 2019 01:03:16 +0200 Subject: [PATCH 13/54] Change BepInEx settings to use new wrap overloads --- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 21 ++++---- BepInEx.Preloader/Preloader.cs | 49 ++++++++----------- BepInEx/Bootstrap/Chainloader.cs | 12 +++-- BepInEx/Bootstrap/TypeLoader.cs | 9 ++-- BepInEx/ConsoleUtil/ConsoleWindow.cs | 16 +++--- BepInEx/Contract/BaseUnityPlugin.cs | 1 - BepInEx/Logging/ConsoleLogListener.cs | 13 ++--- BepInEx/Logging/DiskLogListener.cs | 30 +++++------- 8 files changed, 66 insertions(+), 85 deletions(-) diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index e01aa5f..2bf55c1 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -278,22 +278,19 @@ namespace BepInEx.Preloader.Patching #region Config private static readonly ConfigWrapper 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); + "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 ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Wrap( - "Preloader", - "LoadDumpedAssemblies", - "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.", - false); + "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 ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Wrap( - "Preloader", - "BreakBeforeLoadAssemblies", - "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.", - false); + "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.")); #endregion } diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index fd90677..77ca461 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -234,42 +234,35 @@ namespace BepInEx.Preloader #region Config - private static readonly ConfigWrapper ConfigEntrypointAssembly = ConfigFile.CoreConfig.Wrap( - "Preloader.Entrypoint", - "Assembly", - "The local filename of the assembly to target.", - IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll" - ); + private static readonly ConfigWrapper ConfigEntrypointAssembly = ConfigFile.CoreConfig.Wrap( + "Preloader.Entrypoint", "Assembly", + IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll", + new ConfigDescription("The local filename of the assembly to target.")); - private static readonly ConfigWrapper ConfigEntrypointType = ConfigFile.CoreConfig.Wrap( - "Preloader.Entrypoint", - "Type", - "The name of the type in the entrypoint assembly to search for the entrypoint method.", - "Application"); + private static readonly ConfigWrapper ConfigEntrypointType = ConfigFile.CoreConfig.Wrap( + "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 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"); + private static readonly ConfigWrapper ConfigEntrypointMethod = ConfigFile.CoreConfig.Wrap( + "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 ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Wrap( - "Preloader", - "ApplyRuntimePatches", - "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.", - true); + "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 ConfigShimHarmony = ConfigFile.CoreConfig.Wrap( - "Preloader", - "ShimHarmonySupport", - "If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.", - !Utility.CLRSupportsDynamicAssemblies); + "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 ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap( - "Logging", - "PreloaderConsoleOutRedirection", - "Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.", - true); + "Logging", "PreloaderConsoleOutRedirection", + true, + new ConfigDescription("Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.")); #endregion } diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index d877cc1..73e532e 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -2,13 +2,11 @@ using BepInEx.Logging; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using BepInEx.Contract; using Mono.Cecil; using UnityEngine; using UnityInjector.ConsoleUtil; @@ -304,9 +302,15 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap("Paths", "PluginsDirectory", "The relative directory to the BepInEx folder where plugins are loaded.", "plugins"); + private static readonly ConfigWrapper ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap( + "Paths", "PluginsDirectory", + "plugins", + new ConfigDescription("The relative directory to the BepInEx folder where plugins are loaded.")); - private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.Wrap("Logging", "UnityLogListening", "Enables showing unity log messages in the BepInEx logging system.", true); + private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.Wrap( + "Logging", "UnityLogListening", + true, + new ConfigDescription("Enables showing unity log messages in the BepInEx logging system.")); #endregion } diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index 6bec362..214cd64 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -257,11 +257,10 @@ namespace BepInEx.Bootstrap #region Config - private static ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.Wrap( - "Caching", - "EnableAssemblyCache", - "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.", - true); + private static readonly ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.Wrap( + "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.")); #endregion } diff --git a/BepInEx/ConsoleUtil/ConsoleWindow.cs b/BepInEx/ConsoleUtil/ConsoleWindow.cs index 9c9e9b6..f583006 100644 --- a/BepInEx/ConsoleUtil/ConsoleWindow.cs +++ b/BepInEx/ConsoleUtil/ConsoleWindow.cs @@ -14,16 +14,14 @@ namespace UnityInjector.ConsoleUtil internal class ConsoleWindow { public static readonly ConfigWrapper ConfigConsoleEnabled = ConfigFile.CoreConfig.Wrap( - "Logging.Console", - "Enabled", - "Enables showing a console for log output.", - false); + "Logging.Console", "Enabled", + false, + new ConfigDescription("Enables showing a console for log output.")); public static readonly ConfigWrapper ConfigConsoleShiftJis = ConfigFile.CoreConfig.Wrap( - "Logging.Console", - "ShiftJisEncoding", - "If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.", - false); + "Logging.Console", "ShiftJisEncoding", + false, + new ConfigDescription("If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.")); public static bool IsAttached { get; private set; } private static IntPtr _cOut; @@ -106,7 +104,7 @@ namespace UnityInjector.ConsoleUtil [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - static extern bool SetForegroundWindow(IntPtr hWnd); + private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AllocConsole(); diff --git a/BepInEx/Contract/BaseUnityPlugin.cs b/BepInEx/Contract/BaseUnityPlugin.cs index 7a74859..7fd51e9 100644 --- a/BepInEx/Contract/BaseUnityPlugin.cs +++ b/BepInEx/Contract/BaseUnityPlugin.cs @@ -1,6 +1,5 @@ using BepInEx.Bootstrap; using BepInEx.Configuration; -using BepInEx.Contract; using BepInEx.Logging; using UnityEngine; diff --git a/BepInEx/Logging/ConsoleLogListener.cs b/BepInEx/Logging/ConsoleLogListener.cs index ee9d9c2..50d292e 100644 --- a/BepInEx/Logging/ConsoleLogListener.cs +++ b/BepInEx/Logging/ConsoleLogListener.cs @@ -9,11 +9,9 @@ namespace BepInEx.Logging /// 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) + if (eventArgs.Level.GetHighestLevel() > ConfigConsoleDisplayedLevel.Value) return; string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n"; @@ -25,10 +23,9 @@ namespace BepInEx.Logging public void Dispose() { } - private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( - "Logging.Console", - "DisplayedLogLevel", - "Only displays the specified log level and above in the console output.", - "Info"); + private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( + "Logging.Console","DisplayedLogLevel", + LogLevel.Info, + new ConfigDescription("Only displays the specified log level and above in the console output.")); } } \ No newline at end of file diff --git a/BepInEx/Logging/DiskLogListener.cs b/BepInEx/Logging/DiskLogListener.cs index e2b94cd..f57cab4 100644 --- a/BepInEx/Logging/DiskLogListener.cs +++ b/BepInEx/Logging/DiskLogListener.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Text; using System.Threading; using BepInEx.Configuration; @@ -11,8 +10,6 @@ namespace BepInEx.Logging /// public class DiskLogListener : ILogListener { - protected LogLevel DisplayedLogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), ConfigConsoleDisplayedLevel.Value, true); - protected TextWriter LogWriter { get; set; } protected Timer FlushTimer { get; set; } @@ -49,7 +46,7 @@ namespace BepInEx.Logging if (!ConfigWriteUnityLog.Value && eventArgs.Source is UnityLogSource) return; - if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel) + if (eventArgs.Level.GetHighestLevel() > ConfigConsoleDisplayedLevel.Value) return; LogWriter.WriteLine($"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}"); @@ -67,22 +64,19 @@ namespace BepInEx.Logging Dispose(); } - private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( - "Logging.Disk", - "DisplayedLogLevel", - "Only displays the specified log level and above in the console output.", - "Info"); + private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( + "Logging.Disk", "DisplayedLogLevel", + LogLevel.Info, + new ConfigDescription("Only displays the specified log level and above in the console output.")); private static readonly ConfigWrapper ConfigWriteUnityLog = ConfigFile.CoreConfig.Wrap( - "Logging.Disk", - "WriteUnityLog", - "Include unity log messages in log file output.", - false); + "Logging.Disk", "WriteUnityLog", + false, + new ConfigDescription("Include unity log messages in log file output.")); private static readonly ConfigWrapper ConfigAppendLog = ConfigFile.CoreConfig.Wrap( - "Logging.Disk", - "AppendLog", - "Appends to the log file instead of overwriting, on game startup.", - false); + "Logging.Disk", "AppendLog", + false, + new ConfigDescription("Appends to the log file instead of overwriting, on game startup.")); } } \ No newline at end of file From 095bdbece1c01745242de09389d8595086414699 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 17 Aug 2019 01:38:28 +0200 Subject: [PATCH 14/54] Fixed newly added settings not getting written until they are changed --- BepInEx/Configuration/ConfigFile.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 6c4f33d..fa32c09 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -183,12 +183,15 @@ namespace BepInEx.Configuration 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; + Entries.TryGetValue(configDefinition, out var entry); if (entry == null) { entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue); Entries[configDefinition] = entry; + forceSave = true; } else { @@ -210,6 +213,9 @@ namespace BepInEx.Configuration entry.SetDescription(configDescription); } + if(forceSave) + Save(); + return new ConfigWrapper(entry); } From 2d8148367067d56b74cd862894b9e15631a965c5 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 17 Aug 2019 01:43:58 +0200 Subject: [PATCH 15/54] Tweaks to cfg comment formatting --- BepInEx/Configuration/ConfigDescription.cs | 2 +- BepInEx/Configuration/ConfigEntry.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs index 856bbd8..07b05d3 100644 --- a/BepInEx/Configuration/ConfigDescription.cs +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -40,7 +40,7 @@ namespace BepInEx.Configuration /// public string ToSerializedString() { - return $"# {Description.Replace("\n", "\n# ")}"; + return $"## {Description.Replace("\n", "\n## ")}"; } } } diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index b423c08..a1c43e8 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -220,11 +220,11 @@ namespace BepInEx.Configuration if (hasType) { - writer.WriteLine("# Setting type: " + SettingType.Name); - if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + writer.WriteLine("# Setting type: " + SettingType.Name); + writer.WriteLine("# Default value: " + DefaultValue); } @@ -236,9 +236,9 @@ namespace BepInEx.Configuration } else if (hasType) { - if (SettingType == typeof(bool)) + /*if (SettingType == typeof(bool)) writer.WriteLine("# Acceptable values: True, False"); - else if (SettingType.IsEnum) + else*/ if (SettingType.IsEnum) writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType))); } } From cf73569a0a0164924cfa05a100ffe966e6ce4074 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 18 Aug 2019 01:29:15 +0200 Subject: [PATCH 16/54] Restore unnecessary changes --- BepInEx/BepInEx.csproj | 18 ------------------ BepInEx/Contract/Attributes.cs | 16 ++++++++-------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index 0c61db8..d1ccbe0 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -26,15 +26,6 @@ - - ..\bin\ - TRACE;UNITY_2018 - true - AnyCPU - prompt - MinimumRecommendedRules.ruleset - ..\bin\BepInEx.xml - bin\Debug\ TRACE;DEBUG @@ -51,15 +42,6 @@ ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll - ..\lib\UnityEngine.dll diff --git a/BepInEx/Contract/Attributes.cs b/BepInEx/Contract/Attributes.cs index eff3e92..38e62c8 100644 --- a/BepInEx/Contract/Attributes.cs +++ b/BepInEx/Contract/Attributes.cs @@ -33,21 +33,21 @@ namespace BepInEx /// public Version Version { get; protected set; } - /// The unique identifier of the plugin. Should not change between plugin versions. - /// The user friendly name of the plugin. Is able to be changed between versions. - /// The specfic version of the plugin. - public BepInPlugin(string guid, string name, string version) + /// The unique identifier of the plugin. Should not change between plugin versions. + /// The user friendly name of the plugin. Is able to be changed between versions. + /// The specfic version of the plugin. + public BepInPlugin(string GUID, string Name, string Version) { - GUID = guid; - Name = name; + this.GUID = GUID; + this.Name = Name; try { - Version = new Version(version); + this.Version = new Version(Version); } catch { - Version = null; + this.Version = null; } } From 0cc23c76a052e48e7fa64c9a1547bc8a8787d98a Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 16:50:50 +0200 Subject: [PATCH 17/54] Use IsValid --- BepInEx/Configuration/AcceptableValueList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs index 9e0c6cd..c2db766 100644 --- a/BepInEx/Configuration/AcceptableValueList.cs +++ b/BepInEx/Configuration/AcceptableValueList.cs @@ -25,7 +25,7 @@ namespace BepInEx.Configuration /// public override object Clamp(object value) { - if (value is T v && _acceptableValues.Any(x => x.Equals(v))) + if (IsValid(value)) return value; return _acceptableValues[0]; From 5391af10e27dceb8237580e51d18df81b930aa90 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 20:24:58 +0200 Subject: [PATCH 18/54] Added ThreadingHelper --- BepInEx/Bootstrap/Chainloader.cs | 2 + BepInEx/ThreadingHelper.cs | 178 +++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 BepInEx/ThreadingHelper.cs diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index 73e532e..d60521c 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -43,6 +43,8 @@ namespace BepInEx.Bootstrap if (_initialized) return; + ThreadingHelper.Initialize(); + // Set vitals if (gameExePath != null) { diff --git a/BepInEx/ThreadingHelper.cs b/BepInEx/ThreadingHelper.cs new file mode 100644 index 0000000..ce6c97d --- /dev/null +++ b/BepInEx/ThreadingHelper.cs @@ -0,0 +1,178 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using BepInEx.Logging; +using UnityEngine; + +namespace BepInEx +{ + /// + /// Provides methods for running code on other threads and synchronizing with the main thread. + /// + public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke + { + private readonly object _invokeLock = new object(); + private Action _invokeList; + private Thread _mainThread; + + /// + /// Current instance of the helper. + /// + public static ThreadingHelper Instance { get; private set; } + + /// + /// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously. + /// Can be used in many built-in framework types, for example + /// and to make their events fire on the main unity thread. + /// + public static ISynchronizeInvoke SynchronizingObject => Instance; + + internal static void Initialize() + { + var go = new GameObject("BepInEx_ThreadingHelper"); + DontDestroyOnLoad(go); + Instance = go.AddComponent(); + } + + /// + /// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads. + /// + public void StartSyncInvoke(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + lock (_invokeLock) _invokeList += action; + } + + private void Update() + { + if (_mainThread == null) + _mainThread = Thread.CurrentThread; + + // Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay + if (_invokeList == null) return; + + Action toRun; + lock (_invokeLock) + { + toRun = _invokeList; + _invokeList = null; + } + + // Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock + // The invocation would also block any threads that call Invoke + foreach (var action in toRun.GetInvocationList().Cast()) + { + try + { + action(); + } + catch (Exception ex) + { + LogInvocationException(ex); + } + } + } + + /// + /// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game. + /// NOTE: Most of Unity API can not be accessed while running on another thread! + /// + /// + /// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread. + /// You can use this action to return results of your work safely. Return null if this is not needed. + /// + public void StartAsyncInvoke(Func action) + { + void DoWork(object _) + { + try + { + var result = action(); + + if (result != null) + StartSyncInvoke(result); + } + catch (Exception ex) + { + LogInvocationException(ex); + } + } + + if (!ThreadPool.QueueUserWorkItem(DoWork)) + throw new NotSupportedException("Failed to queue the action on ThreadPool"); + } + + private static void LogInvocationException(Exception ex) + { + Logging.Logger.Log(LogLevel.Error, ex); + if (ex.InnerException != null) Logging.Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException); + } + + #region ISynchronizeInvoke + + IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args) + { + object Invoke() + { + try { return method.DynamicInvoke(args); } + catch (Exception ex) { return ex; } + } + + var result = new InvokeResult(); + + if (!InvokeRequired) + result.Finish(Invoke(), true); + else + StartSyncInvoke(() => result.Finish(Invoke(), false)); + + return result; + } + + object ISynchronizeInvoke.EndInvoke(IAsyncResult result) + { + result.AsyncWaitHandle.WaitOne(); + + if (result.AsyncState is Exception ex) + throw ex; + return result.AsyncState; + } + + object ISynchronizeInvoke.Invoke(Delegate method, object[] args) + { + var invokeResult = ((ISynchronizeInvoke)this).BeginInvoke(method, args); + return ((ISynchronizeInvoke)this).EndInvoke(invokeResult); + } + + /// + /// False if current code is executing on the main unity thread, otherwise True. + /// Warning: Will return false before the first frame finishes (i.e. inside plugin Awake and Start methods). + /// + /// + public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread; + + private sealed class InvokeResult : IAsyncResult + { + public InvokeResult() + { + AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + } + + public void Finish(object result, bool completedSynchronously) + { + AsyncState = result; + CompletedSynchronously = completedSynchronously; + IsCompleted = true; + ((EventWaitHandle)AsyncWaitHandle).Set(); + } + + public bool IsCompleted { get; private set; } + public WaitHandle AsyncWaitHandle { get; } + public object AsyncState { get; private set; } + public bool CompletedSynchronously { get; private set; } + } + + #endregion + } +} \ No newline at end of file From 5aa47a1ba1e3c023e2166159a372ae2d476355b2 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 20:31:59 +0200 Subject: [PATCH 19/54] Refactor ConfigEntry to be generic; Remove file watching --- BepInEx/Configuration/AcceptableValueBase.cs | 18 +- BepInEx/Configuration/AcceptableValueList.cs | 4 +- BepInEx/Configuration/AcceptableValueRange.cs | 4 +- BepInEx/Configuration/ConfigDescription.cs | 2 +- BepInEx/Configuration/ConfigEntry.cs | 218 +++++---------- BepInEx/Configuration/ConfigFile.cs | 264 ++++++++---------- BepInEx/Configuration/ConfigWrapper.cs | 10 +- .../Configuration/SettingChangedEventArgs.cs | 6 +- BepInExTests/Configuration/ConfigFileTests.cs | 33 +-- 9 files changed, 220 insertions(+), 339 deletions(-) diff --git a/BepInEx/Configuration/AcceptableValueBase.cs b/BepInEx/Configuration/AcceptableValueBase.cs index bae8416..00a227b 100644 --- a/BepInEx/Configuration/AcceptableValueBase.cs +++ b/BepInEx/Configuration/AcceptableValueBase.cs @@ -1,10 +1,17 @@ -namespace BepInEx.Configuration +using System; + +namespace BepInEx.Configuration { /// - /// 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. /// public abstract class AcceptableValueBase { + protected AcceptableValueBase(Type valueType) + { + ValueType = valueType; + } + /// /// Change the value to be acceptable, if it's not already. /// @@ -15,9 +22,14 @@ /// public abstract bool IsValid(object value); + /// + /// Type of the supported values. + /// + public Type ValueType { get; } + /// /// Get the string for use in config files. /// public abstract string ToSerializedString(); } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs index c2db766..7371b1a 100644 --- a/BepInEx/Configuration/AcceptableValueList.cs +++ b/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. /// - 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()); } } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/AcceptableValueRange.cs b/BepInEx/Configuration/AcceptableValueRange.cs index bab318a..6cd3f6c 100644 --- a/BepInEx/Configuration/AcceptableValueRange.cs +++ b/BepInEx/Configuration/AcceptableValueRange.cs @@ -9,7 +9,7 @@ namespace BepInEx.Configuration { /// Lowest acceptable value /// Highest acceptable value - 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}"; } } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs index 07b05d3..fd85d70 100644 --- a/BepInEx/Configuration/ConfigDescription.cs +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -3,7 +3,7 @@ namespace BepInEx.Configuration { /// - /// Metadata of a . + /// Metadata of a . /// public class ConfigDescription { diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index a1c43e8..a2499ab 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -9,50 +9,22 @@ namespace BepInEx.Configuration /// Container for a single setting of a . /// Each config entry is linked to one config file. /// - 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) + /// + /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. + /// + internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) { ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile)); Definition = definition ?? throw new ArgumentNullException(nameof(definition)); - } + SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); - 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."); - } - - 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; - /// /// Config file this entry is a part of. /// @@ -71,138 +43,69 @@ namespace BepInEx.Configuration /// /// Type of the that this setting holds. /// - public Type SettingType { get; private set; } + public Type SettingType { get; } /// /// Default value of this setting (set only if the setting was not changed before). /// - public object DefaultValue { get; private set; } - - /// - /// Is the type of this setting defined, and by extension can of this setting be accessed. - /// Setting is defined when any objects reference it. - /// - public bool IsDefined => SettingType != null; + public object DefaultValue { get; } /// /// Get or set the value of the setting. - /// Can't be used when is false. /// - 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; } /// /// Get the serialized representation of the value. /// public string GetSerializedValue() { - if (_serializedValue != null) - return _serializedValue; - - if (!IsDefined) - return null; - return TomlTypeConverter.ConvertToString(Value, SettingType); } /// /// Set the value by using its serialized form. /// - 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) + /// + /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. + /// + protected T ClampValue(T value) { if (Description?.AcceptableValues != null) - return Description.AcceptableValues.Clamp(value); + return (T)Description.AcceptableValues.Clamp(value); return value; } - private void OnSettingChanged(object sender) + /// + /// Trigger setting changed event. + /// + 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) { - 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))); - } + 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))); } } } + + /// + public class ConfigEntry : ConfigEntryBase + { + private T _typedValue; + internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue) : base(configFile, definition, typeof(T), defaultValue) { } + + /// + /// Get or set the value of the setting without boxing. + /// + public T TypedValue + { + get => _typedValue; + set + { + value = ClampValue(value); + if (Equals(_typedValue, value)) + return; + + _typedValue = value; + OnSettingChanged(this); + } + } + + /// + public override object Value + { + get => TypedValue; + set => TypedValue = (T)value; + } + } } diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index fa32c09..cb4adcc 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -20,20 +20,31 @@ namespace BepInEx.Configuration /// /// All config entries inside /// - protected Dictionary Entries { get; } = new Dictionary(); + protected Dictionary Entries { get; } = new Dictionary(); + + private Dictionary HomelessEntries { get; } = new Dictionary(); /// /// Create a list with all config entries inside of this config file. /// [Obsolete("Use GetConfigEntries instead")] - public ReadOnlyCollection ConfigDefinitions => Entries.Keys.ToList().AsReadOnly(); + public ReadOnlyCollection ConfigDefinitions + { + get + { + lock (_ioLock) return Entries.Keys.ToList().AsReadOnly(); + } + } /// /// 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 /// instead with no description. /// - public ConfigEntry[] GetConfigEntries() => Entries.Values.ToArray(); + public ConfigEntryBase[] GetConfigEntries() + { + lock (_ioLock) return Entries.Values.ToArray(); + } /// /// Full path to the config file. The file might not exist until a setting is added and changed, or is called. @@ -68,13 +79,12 @@ namespace BepInEx.Configuration { Save(); } - - StartWatching(); } #region Save/Load private readonly object _ioLock = new object(); + private bool _disableSaving; /// /// 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; + + 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); + + Entries.TryGetValue(definition, out ConfigEntryBase entry); + + if (entry != null) + entry.SetSerializedValue(currentValue); + else + HomelessEntries[definition] = currentValue; } - - 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); - - Entries.TryGetValue(definition, out ConfigEntry entry); - if (entry == null) - { - entry = new ConfigEntry(this, definition); - Entries[definition] = entry; - } - - entry.SetSerializedValue(currentValue, true, this); + } + 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 /// Section and Key of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - /// public ConfigWrapper Wrap(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())}"); - - var forceSave = false; - - Entries.TryGetValue(configDefinition, out var entry); - - if (entry == null) + try { - entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue); - Entries[configDefinition] = entry; - forceSave = true; - } - else - { - entry.SetTypeAndDefaultValue(typeof(T), defaultValue, !Equals(defaultValue, default(T))); - } + 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())}"); - 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 (configDescription.AcceptableValues != null) + lock (_ioLock) { - 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)); + _disableSaving = true; + + Entries.TryGetValue(configDefinition, out var existingEntry); + + if (existingEntry != null && !(existingEntry is ConfigEntry)) + throw new ArgumentException("The defined setting already exists with a different setting type - " + existingEntry.SettingType.Name); + + var entry = (ConfigEntry)existingEntry; + + if (entry == null) + { + entry = new ConfigEntry(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."); + + entry.SetDescription(configDescription); + } + + if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue)) + { + entry.SetSerializedValue(homelessValue); + HomelessEntries.Remove(configDefinition); + } + + _disableSaving = false; + if (SaveOnConfigSet) + Save(); + + return new ConfigWrapper(entry); } - - entry.SetDescription(configDescription); } - - if(forceSave) - Save(); - - return new ConfigWrapper(entry); + finally + { + _disableSaving = false; + } } /// @@ -238,7 +262,6 @@ namespace BepInEx.Configuration /// Name of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - /// public ConfigWrapper Wrap(string section, string key, T defaultValue, ConfigDescription configDescription = null) => Wrap(new ConfigDefinition(section, key), defaultValue, configDescription); @@ -256,101 +279,36 @@ namespace BepInEx.Configuration /// public event EventHandler SettingChanged; - internal void OnSettingChanged(object sender, ConfigEntry changedEntry) + internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) { - if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry)); + if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); - if (SettingChanged != null) - { - var args = new SettingChangedEventArgs(changedEntry); - - foreach (var callback in SettingChanged.GetInvocationList().Cast>()) - { - try - { - callback(sender, args); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, e); - } - } - } - - // Check sender to prevent infinite loops - // todo batching / async? - if (sender != this && SaveOnConfigSet) + if (SaveOnConfigSet) Save(); + + var settingChanged = SettingChanged; + if (settingChanged == null) return; + + var args = new SettingChangedEventArgs(changedEntryBase); + foreach (var callback in settingChanged.GetInvocationList().Cast>()) + { + try { callback(sender, args); } + catch (Exception e) { Logger.Log(LogLevel.Error, e); } + } } private void OnConfigReloaded() { - if (ConfigReloaded != null) + var configReloaded = ConfigReloaded; + if (configReloaded == null) return; + + foreach (var callback in configReloaded.GetInvocationList().Cast()) { - foreach (var callback in ConfigReloaded.GetInvocationList().Cast()) - { - try - { - callback(this, EventArgs.Empty); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, e); - } - } + try { callback(this, EventArgs.Empty); } + catch (Exception e) { Logger.Log(LogLevel.Error, e); } } } #endregion - - #region File watcher - - private FileSystemWatcher _watcher; - - /// - /// Start watching the config file on disk for changes. - /// - public void StartWatching() - { - lock (_ioLock) - { - 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(); - } - } - - /// - /// Stop watching the config file on disk for changes. - /// - public void StopWatching() - { - lock (_ioLock) - { - if (_watcher != null) - { - _watcher.EnableRaisingEvents = false; - _watcher.Dispose(); - _watcher = null; - } - } - } - - /// - ~ConfigFile() - { - StopWatching(); - } - - #endregion } } diff --git a/BepInEx/Configuration/ConfigWrapper.cs b/BepInEx/Configuration/ConfigWrapper.cs index 1e1f0eb..660df46 100644 --- a/BepInEx/Configuration/ConfigWrapper.cs +++ b/BepInEx/Configuration/ConfigWrapper.cs @@ -11,7 +11,7 @@ namespace BepInEx.Configuration /// /// Entry of this setting in the . /// - public ConfigEntry ConfigEntry { get; } + public ConfigEntry ConfigEntry { get; } /// /// Unique definition of this setting. @@ -33,11 +33,11 @@ namespace BepInEx.Configuration /// 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 configEntry) { ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry)); @@ -47,4 +47,4 @@ namespace BepInEx.Configuration }; } } -} \ No newline at end of file +} diff --git a/BepInEx/Configuration/SettingChangedEventArgs.cs b/BepInEx/Configuration/SettingChangedEventArgs.cs index 53d81b2..917af04 100644 --- a/BepInEx/Configuration/SettingChangedEventArgs.cs +++ b/BepInEx/Configuration/SettingChangedEventArgs.cs @@ -9,7 +9,7 @@ namespace BepInEx.Configuration public sealed class SettingChangedEventArgs : EventArgs { /// - public SettingChangedEventArgs(ConfigEntry changedSetting) + public SettingChangedEventArgs(ConfigEntryBase changedSetting) { ChangedSetting = changedSetting; } @@ -17,6 +17,6 @@ namespace BepInEx.Configuration /// /// Setting that was changed /// - public ConfigEntry ChangedSetting { get; } + public ConfigEntryBase ChangedSetting { get; } } -} \ No newline at end of file +} diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index 96f8617..ad8b2f0 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/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(); From c85cfabee1b2b3a4bd792442c49ffdcff4c3208b Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 20:32:34 +0200 Subject: [PATCH 20/54] Split into separate classes --- BepInEx/BepInEx.csproj | 2 + BepInEx/Configuration/ConfigEntry.cs | 148 +---------------------- BepInEx/Configuration/ConfigEntryBase.cs | 147 ++++++++++++++++++++++ 3 files changed, 150 insertions(+), 147 deletions(-) create mode 100644 BepInEx/Configuration/ConfigEntryBase.cs diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index d1ccbe0..e798dc1 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -52,6 +52,7 @@ + @@ -85,6 +86,7 @@ + diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs index a2499ab..0ff907d 100644 --- a/BepInEx/Configuration/ConfigEntry.cs +++ b/BepInEx/Configuration/ConfigEntry.cs @@ -1,151 +1,5 @@ -using System; -using System.IO; -using System.Linq; -using BepInEx.Logging; - -namespace BepInEx.Configuration +namespace BepInEx.Configuration { - /// - /// Container for a single setting of a . - /// Each config entry is linked to one config file. - /// - public abstract class ConfigEntryBase - { - /// - /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. - /// - internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) - { - ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile)); - Definition = definition ?? throw new ArgumentNullException(nameof(definition)); - SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); - - // Free type check - Value = defaultValue; - DefaultValue = defaultValue; - } - - /// - /// Config file this entry is a part of. - /// - public ConfigFile ConfigFile { get; } - - /// - /// Category and name of this setting. Used as a unique key for identification within a . - /// - public ConfigDefinition Definition { get; } - - /// - /// Description / metadata of this setting. - /// - public ConfigDescription Description { get; private set; } - - /// - /// Type of the that this setting holds. - /// - public Type SettingType { get; } - - /// - /// Default value of this setting (set only if the setting was not changed before). - /// - public object DefaultValue { get; } - - /// - /// Get or set the value of the setting. - /// - public abstract object Value { get; set; } - - /// - /// Get the serialized representation of the value. - /// - public string GetSerializedValue() - { - return TomlTypeConverter.ConvertToString(Value, SettingType); - } - - /// - /// Set the value by using its serialized form. - /// - public void SetSerializedValue(string value) - { - try - { - var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); - Value = newValue; - } - 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}"); - } - } - - 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 - Value = Value; - } - - /// - /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. - /// - protected T ClampValue(T value) - { - if (Description?.AcceptableValues != null) - return (T)Description.AcceptableValues.Clamp(value); - return value; - } - - /// - /// Trigger setting changed event. - /// - protected void OnSettingChanged(object sender) - { - ConfigFile.OnSettingChanged(sender, this); - } - - /// - /// Write a description of this setting using all available metadata. - /// - public void WriteDescription(StreamWriter writer) - { - bool hasDescription = Description != null; - bool hasType = SettingType != null; - - if (hasDescription) - writer.WriteLine(Description.ToSerializedString()); - - if (hasType) - { - if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) - writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); - - writer.WriteLine("# Setting type: " + SettingType.Name); - - writer.WriteLine("# Default value: " + DefaultValue); - } - - if (hasDescription && 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))); - } - } - } - /// public class ConfigEntry : ConfigEntryBase { diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs new file mode 100644 index 0000000..aed022c --- /dev/null +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Linq; +using BepInEx.Logging; + +namespace BepInEx.Configuration { + /// + /// Container for a single setting of a . + /// Each config entry is linked to one config file. + /// + public abstract class ConfigEntryBase + { + /// + /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. + /// + internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) + { + ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile)); + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); + + // Free type check + Value = defaultValue; + DefaultValue = defaultValue; + } + + /// + /// Config file this entry is a part of. + /// + public ConfigFile ConfigFile { get; } + + /// + /// Category and name of this setting. Used as a unique key for identification within a . + /// + public ConfigDefinition Definition { get; } + + /// + /// Description / metadata of this setting. + /// + public ConfigDescription Description { get; private set; } + + /// + /// Type of the that this setting holds. + /// + public Type SettingType { get; } + + /// + /// Default value of this setting (set only if the setting was not changed before). + /// + public object DefaultValue { get; } + + /// + /// Get or set the value of the setting. + /// + public abstract object Value { get; set; } + + /// + /// Get the serialized representation of the value. + /// + public string GetSerializedValue() + { + return TomlTypeConverter.ConvertToString(Value, SettingType); + } + + /// + /// Set the value by using its serialized form. + /// + public void SetSerializedValue(string value) + { + try + { + var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); + Value = newValue; + } + 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}"); + } + } + + 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 + Value = Value; + } + + /// + /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. + /// + protected T ClampValue(T value) + { + if (Description?.AcceptableValues != null) + return (T)Description.AcceptableValues.Clamp(value); + return value; + } + + /// + /// Trigger setting changed event. + /// + protected void OnSettingChanged(object sender) + { + ConfigFile.OnSettingChanged(sender, this); + } + + /// + /// Write a description of this setting using all available metadata. + /// + public void WriteDescription(StreamWriter writer) + { + bool hasDescription = Description != null; + bool hasType = SettingType != null; + + if (hasDescription) + writer.WriteLine(Description.ToSerializedString()); + + if (hasType) + { + if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) + writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + + writer.WriteLine("# Setting type: " + SettingType.Name); + + writer.WriteLine("# Default value: " + DefaultValue); + } + + if (hasDescription && 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))); + } + } + } +} From b730c16443113c625a3f0f27ee90bfd905ac3c72 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 21:56:27 +0200 Subject: [PATCH 21/54] Fixed homeless entries not getting saved --- BepInEx/Configuration/ConfigEntryBase.cs | 7 ++++--- BepInEx/Configuration/ConfigFile.cs | 13 +++++++++---- BepInExTests/Configuration/ConfigFileTests.cs | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs index aed022c..f6435d2 100644 --- a/BepInEx/Configuration/ConfigEntryBase.cs +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -3,7 +3,8 @@ using System.IO; using System.Linq; using BepInEx.Logging; -namespace BepInEx.Configuration { +namespace BepInEx.Configuration +{ /// /// Container for a single setting of a . /// Each config entry is linked to one config file. @@ -75,7 +76,7 @@ namespace BepInEx.Configuration { 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}"); + $"parsed and will be ignored. Reason: {e.Message}; Value: {value}"); } } @@ -86,7 +87,7 @@ namespace BepInEx.Configuration { 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 Value = Value; } diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index cb4adcc..aa858d0 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -93,6 +93,8 @@ namespace BepInEx.Configuration { lock (_ioLock) { + HomelessEntries.Clear(); + try { _disableSaving = true; @@ -159,18 +161,21 @@ namespace BepInEx.Configuration writer.WriteLine(); } - foreach (var sectionKv in Entries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) + var allConfigEntries = Entries.Select(x => new { x.Key, entry = x.Value, value = x.Value.GetSerializedValue() }) + .Concat(HomelessEntries.Select(x => new { x.Key, entry = (ConfigEntryBase)null, value = x.Value })); + + foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) { // Section heading writer.WriteLine($"[{sectionKv.Key}]"); - foreach (var configEntry in sectionKv.Select(x => x.Value)) + foreach (var configEntry in sectionKv) { writer.WriteLine(); - configEntry.WriteDescription(writer); + configEntry.entry?.WriteDescription(writer); - writer.WriteLine($"{configEntry.Definition.Key} = {configEntry.GetSerializedValue()}"); + writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}"); } writer.WriteLine(); diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index ad8b2f0..31c9c4e 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -103,7 +103,7 @@ namespace BepInEx.Configuration.Tests c.Reload(); Assert.AreEqual(w.Value, 1); } - + [TestMethod] public void FileWatchTestNoSelfReload() { @@ -133,6 +133,21 @@ namespace BepInEx.Configuration.Tests Assert.IsTrue(eventFired); } + [TestMethod] + public void PersistHomeless() + { + var c = MakeConfig(); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\nHomeless=0"); + c.Reload(); + + var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + + c.Save(); + + Assert.IsTrue(File.ReadAllLines(c.ConfigFilePath).Single(x => x.StartsWith("Homeless") && x.EndsWith("0")) != null); + } + [TestMethod] public void EventTestReload() { From a0d0d363db6a043d505868cfe4f48629013748b3 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 22:42:43 +0200 Subject: [PATCH 22/54] Fixed ConfigFile breaking preloader patcher --- BepInEx/Configuration/ConfigFile.cs | 6 +++--- BepInEx/Contract/BaseUnityPlugin.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index aa858d0..ef48b24 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -62,10 +62,10 @@ namespace BepInEx.Configuration /// /// Full path to a file that contains settings. The file will be created as needed. /// If the config file/directory doesn't exist, create it immediately. - /// The plugin that owns this setting. - public ConfigFile(string configPath, bool saveOnInit, BaseUnityPlugin owner = null) + /// Information about the plugin that owns this setting file. + public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata = null) { - _ownerMetadata = owner?.Info.Metadata; + _ownerMetadata = ownerMetadata; if (configPath == null) throw new ArgumentNullException(nameof(configPath)); configPath = Path.GetFullPath(configPath); diff --git a/BepInEx/Contract/BaseUnityPlugin.cs b/BepInEx/Contract/BaseUnityPlugin.cs index 7fd51e9..9470836 100644 --- a/BepInEx/Contract/BaseUnityPlugin.cs +++ b/BepInEx/Contract/BaseUnityPlugin.cs @@ -48,7 +48,7 @@ namespace BepInEx Logger = Logging.Logger.CreateLogSource(metadata.Name); - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, this); + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } } } \ No newline at end of file From 87edfafce0a154b7279893e3794d0228be275c29 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 22:50:26 +0200 Subject: [PATCH 23/54] Fixed null sections acting weird --- BepInEx/Configuration/ConfigDefinition.cs | 4 ++-- BepInEx/Configuration/ConfigFile.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 628b400..18eaf16 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -26,8 +26,8 @@ namespace BepInEx.Configuration /// Name of the setting, case sensitive. public ConfigDefinition(string section, string key) { - Key = key; - Section = section; + Key = key ?? throw new ArgumentNullException(nameof(key)); + Section = section ?? throw new ArgumentNullException(nameof(section)); } /// diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index ef48b24..b37e529 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -255,7 +255,7 @@ namespace BepInEx.Configuration /// [Obsolete("Use other Wrap overloads instead")] public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) - => Wrap(new ConfigDefinition(section, key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + => Wrap(new ConfigDefinition(section ?? "", key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); /// /// Create a new setting or access one of the existing ones. The setting is saved to drive and loaded automatically. From 5d76d36037e476459574dda8b8b87ca8147eeab3 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 23:06:38 +0200 Subject: [PATCH 24/54] Added missing docs --- BepInEx/Configuration/AcceptableValueBase.cs | 1 + BepInEx/Configuration/KeyboardShortcut.cs | 3 +++ BepInEx/Configuration/TomlTypeConverter.cs | 21 ++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/BepInEx/Configuration/AcceptableValueBase.cs b/BepInEx/Configuration/AcceptableValueBase.cs index 00a227b..f5118c2 100644 --- a/BepInEx/Configuration/AcceptableValueBase.cs +++ b/BepInEx/Configuration/AcceptableValueBase.cs @@ -7,6 +7,7 @@ namespace BepInEx.Configuration /// public abstract class AcceptableValueBase { + /// Type of values that this class can Clamp. protected AcceptableValueBase(Type valueType) { ValueType = valueType; diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index 79b2aba..2eb0860 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -55,6 +55,9 @@ namespace BepInEx.Configuration _allKeysLookup = new HashSet(_allKeys); } + /// + /// Create a new empty shortcut. + /// public KeyboardShortcut() { _allKeys = SanitizeKeys(); diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 9f41b06..4b24a93 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -97,6 +97,9 @@ namespace BepInEx.Configuration }, }; + /// + /// Convert object of a given type to a string using available converters. + /// public static string ConvertToString(object value, Type valueType) { var conv = GetConverter(valueType); @@ -106,11 +109,17 @@ namespace BepInEx.Configuration return conv.ConvertToString(value, valueType); } + /// + /// Convert string to an object of a given type using available converters. + /// public static T ConvertToValue(string value) { return (T)ConvertToValue(value, typeof(T)); } + /// + /// Convert string to an object of a given type using available converters. + /// public static object ConvertToValue(string value, Type valueType) { var conv = GetConverter(valueType); @@ -120,6 +129,9 @@ namespace BepInEx.Configuration return conv.ConvertToObject(value, valueType); } + /// + /// Get a converter for a given type if there is any. + /// public static TypeConverter GetConverter(Type valueType) { if (valueType == null) throw new ArgumentNullException(nameof(valueType)); @@ -131,6 +143,9 @@ namespace BepInEx.Configuration return result; } + /// + /// Add a new type converter for a given type. + /// public static void AddConverter(Type type, TypeConverter converter) { if (type == null) throw new ArgumentNullException(nameof(type)); @@ -140,11 +155,17 @@ namespace BepInEx.Configuration TypeConverters.Add(type, converter); } + /// + /// Check if a given type can be converted to and from string. + /// public static bool CanConvert(Type type) { return GetConverter(type) != null; } + /// + /// Give a list of types with registered converters. + /// public static IEnumerable GetSupportedTypes() { return TypeConverters.Keys; From 1c66732b417be6bc730e2c2bdb7f59174d3fa50e Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 24 Aug 2019 23:40:02 +0200 Subject: [PATCH 25/54] Split the constructor for backwards compatibility --- BepInEx/Configuration/ConfigFile.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index b37e529..600d380 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -57,13 +57,16 @@ namespace BepInEx.Configuration /// public bool SaveOnConfigSet { get; set; } = true; + /// + public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { } + /// /// Create a new config file at the specified config path. /// /// Full path to a file that contains settings. The file will be created as needed. /// If the config file/directory doesn't exist, create it immediately. /// Information about the plugin that owns this setting file. - public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata = null) + public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata) { _ownerMetadata = ownerMetadata; From e01dd44786b7bc9a55bb0570f943e606a41b9a88 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 25 Aug 2019 13:03:44 +0200 Subject: [PATCH 26/54] Added support for newlines and special chars in string settings --- BepInEx/Configuration/TomlTypeConverter.cs | 11 ++++- BepInExTests/Configuration/ConfigFileTests.cs | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 4b24a93..baddc4a 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Text.RegularExpressions; namespace BepInEx.Configuration { @@ -13,8 +14,14 @@ namespace BepInEx.Configuration { [typeof(string)] = new TypeConverter { - ConvertToString = (obj, type) => (string)obj, - ConvertToObject = (str, type) => str, + ConvertToString = (obj, type) => Regex.Escape((string)obj), + ConvertToObject = (str, type) => + { + // Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test) + if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)")) + return str; + return Regex.Unescape(str); + }, }, [typeof(bool)] = new TypeConverter { diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index 31c9c4e..cf2d8ff 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -247,5 +247,50 @@ namespace BepInEx.Configuration.Tests c.Reload(); Assert.AreEqual(shortcut, w.Value); } + + [TestMethod] + public void StringEscapeChars() + { + const string testVal = "new line\n test \t\0"; + + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", testVal, new ConfigDescription("Test")); + + Assert.AreEqual(testVal, w.Value); + Assert.IsFalse(w.ConfigEntry.GetSerializedValue().Any(x => x == '\n')); + + w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + Assert.AreEqual(testVal, w.Value); + + c.Save(); + c.Reload(); + + Assert.AreEqual(testVal, w.Value); + } + + [TestMethod] + public void UnescapedPathString() + { + var c = MakeConfig(); + + var unescaped = @"D:\test\p ath"; + foreach (string testVal in new[]{ unescaped , @"D:\\test\\p ath" }) + { + File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); + c.Reload(); + + var w = c.Wrap("Cat", "Key", "", new ConfigDescription("Test")); + + Assert.AreEqual(unescaped, w.Value); + + w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + Assert.AreEqual(unescaped, w.Value); + + c.Save(); + c.Reload(); + + Assert.AreEqual(unescaped, w.Value); + } + } } } From 0378ba54e3d393581a37710d92a2ccfe6dfa3d94 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 25 Aug 2019 20:25:56 +0200 Subject: [PATCH 27/54] Rename Wrap to GetSetting and add some missing Obsolete overloads --- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 6 ++-- BepInEx.Preloader/Preloader.cs | 12 +++---- BepInEx/Bootstrap/Chainloader.cs | 10 +++--- BepInEx/Bootstrap/TypeLoader.cs | 2 +- BepInEx/Configuration/ConfigDefinition.cs | 4 +++ BepInEx/Configuration/ConfigFile.cs | 33 ++++++++++--------- BepInEx/Configuration/KeyboardShortcut.cs | 4 +-- BepInEx/ConsoleUtil/ConsoleWindow.cs | 4 +-- BepInEx/Logging/ConsoleLogListener.cs | 2 +- BepInExTests/Configuration/ConfigFileTests.cs | 32 +++++++++--------- 10 files changed, 57 insertions(+), 52 deletions(-) diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 2bf55c1..1949bad 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -277,17 +277,17 @@ namespace BepInEx.Preloader.Patching #region Config - private static readonly ConfigWrapper ConfigDumpAssemblies = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigDumpAssemblies = ConfigFile.CoreConfig.GetSetting( "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 ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.GetSetting( "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 ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.GetSetting( "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.")); diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index c437b83..6005087 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -237,32 +237,32 @@ namespace BepInEx.Preloader #region Config - private static readonly ConfigWrapper ConfigEntrypointAssembly = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigEntrypointAssembly = ConfigFile.CoreConfig.GetSetting( "Preloader.Entrypoint", "Assembly", IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll", new ConfigDescription("The local filename of the assembly to target.")); - private static readonly ConfigWrapper ConfigEntrypointType = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigEntrypointType = ConfigFile.CoreConfig.GetSetting( "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 ConfigEntrypointMethod = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigEntrypointMethod = ConfigFile.CoreConfig.GetSetting( "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 ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigApplyRuntimePatches = ConfigFile.CoreConfig.GetSetting( "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 ConfigShimHarmony = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigShimHarmony = ConfigFile.CoreConfig.GetSetting( "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 ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.GetSetting( "Logging", "PreloaderConsoleOutRedirection", true, new ConfigDescription("Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.")); diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index a489d57..0073e74 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -307,27 +307,27 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.GetSetting( "Logging", "UnityLogListening", true, new ConfigDescription("Enables showing unity log messages in the BepInEx logging system.")); - private static readonly ConfigWrapper ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.GetSetting( "Logging.Disk", "WriteUnityLog", false, new ConfigDescription("Include unity log messages in log file output.")); - private static readonly ConfigWrapper ConfigDiskAppend = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigDiskAppend = ConfigFile.CoreConfig.GetSetting( "Logging.Disk", "AppendLog", false, new ConfigDescription("Appends to the log file instead of overwriting, on game startup.")); - private static readonly ConfigWrapper ConfigDiskLogging = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigDiskLogging = ConfigFile.CoreConfig.GetSetting( "Logging.Disk", "Enabled", true, new ConfigDescription("Enables writing log messages to disk.")); - private static readonly ConfigWrapper ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting( "Logging.Disk", "DisplayedLogLevel", LogLevel.Info, new ConfigDescription("Only displays the specified log level and above in the console output.")); diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index 214cd64..1fbea2a 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -257,7 +257,7 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.GetSetting( "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.")); diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 18eaf16..99b2334 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -30,6 +30,10 @@ namespace BepInEx.Configuration Section = section ?? throw new ArgumentNullException(nameof(section)); } + /// + [Obsolete("description argument is no longer used, put it in a ConfigDescription instead")] + public ConfigDefinition(string section, string key, string description) : this(section, key) { } + /// /// Check if the definitions are the same. /// diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 600d380..076ef5e 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -38,7 +38,7 @@ namespace BepInEx.Configuration /// /// 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 + /// If you want to access and modify an existing setting then use /// instead with no description. /// public ConfigEntryBase[] GetConfigEntries() @@ -200,13 +200,13 @@ namespace BepInEx.Configuration /// Section and Key of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) + public ConfigWrapper GetSetting(ConfigDefinition configDefinition, T defaultValue = default(T), 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())}"); - lock (_ioLock) { _disableSaving = true; @@ -251,15 +251,6 @@ namespace BepInEx.Configuration } } - /// - /// 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. - /// - [Obsolete("Use other Wrap overloads instead")] - public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) - => Wrap(new ConfigDefinition(section ?? "", key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); - /// /// 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. @@ -270,8 +261,18 @@ namespace BepInEx.Configuration /// Name of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper Wrap(string section, string key, T defaultValue, ConfigDescription configDescription = null) - => Wrap(new ConfigDefinition(section, key), defaultValue, configDescription); + public ConfigWrapper GetSetting(string section, string key, T defaultValue = default(T), ConfigDescription configDescription = null) + => GetSetting(new ConfigDefinition(section, key), defaultValue, configDescription); + + /// + [Obsolete("Use GetSetting instead")] + public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) + => GetSetting(new ConfigDefinition(section ?? "", key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + + /// + [Obsolete("Use GetSetting instead")] + public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default(T)) + => GetSetting(configDefinition, defaultValue); #endregion diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index 2eb0860..af1b81f 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -12,7 +12,7 @@ namespace BepInEx.Configuration /// triggered when the user presses the exact combination. For example, F + LeftCtrl 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 + /// Can be used as a value of a setting in /// to allow user to change this shortcut and have the changes saved. /// /// How to use: Use in this class instead of in the Update loop. @@ -25,7 +25,7 @@ namespace BepInEx.Configuration typeof(KeyboardShortcut), new TypeConverter { - ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize(), + ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize() ?? "", ConvertToObject = (s, type) => Deserialize(s) }); } diff --git a/BepInEx/ConsoleUtil/ConsoleWindow.cs b/BepInEx/ConsoleUtil/ConsoleWindow.cs index f583006..46ede11 100644 --- a/BepInEx/ConsoleUtil/ConsoleWindow.cs +++ b/BepInEx/ConsoleUtil/ConsoleWindow.cs @@ -13,12 +13,12 @@ namespace UnityInjector.ConsoleUtil { internal class ConsoleWindow { - public static readonly ConfigWrapper ConfigConsoleEnabled = ConfigFile.CoreConfig.Wrap( + public static readonly ConfigWrapper ConfigConsoleEnabled = ConfigFile.CoreConfig.GetSetting( "Logging.Console", "Enabled", false, new ConfigDescription("Enables showing a console for log output.")); - public static readonly ConfigWrapper ConfigConsoleShiftJis = ConfigFile.CoreConfig.Wrap( + public static readonly ConfigWrapper ConfigConsoleShiftJis = ConfigFile.CoreConfig.GetSetting( "Logging.Console", "ShiftJisEncoding", false, new ConfigDescription("If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.")); diff --git a/BepInEx/Logging/ConsoleLogListener.cs b/BepInEx/Logging/ConsoleLogListener.cs index 50d292e..fbe116a 100644 --- a/BepInEx/Logging/ConsoleLogListener.cs +++ b/BepInEx/Logging/ConsoleLogListener.cs @@ -23,7 +23,7 @@ namespace BepInEx.Logging public void Dispose() { } - private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap( + private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting( "Logging.Console","DisplayedLogLevel", LogLevel.Info, new ConfigDescription("Only displays the specified log level and above in the console output.")); diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index cf2d8ff..6cad1e0 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -46,7 +46,7 @@ namespace BepInEx.Configuration.Tests { var c = MakeConfig(); - var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test")); Assert.AreEqual(w.Value, 1); - var w2 = c.Wrap("Cat", "Key2", 0, new ConfigDescription("Test")); + var w2 = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.GetSetting("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + var w = c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); Assert.Fail(); } @@ -193,7 +193,7 @@ namespace BepInEx.Configuration.Tests public void ValueRangeDefaultTest() { var c = MakeConfig(); - var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + var w = c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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.Wrap("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + var w = c.GetSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); Assert.AreEqual(w.Value, 1); @@ -220,7 +220,7 @@ namespace BepInEx.Configuration.Tests public void ValueListTest() { var c = MakeConfig(); - var w = c.Wrap("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList("lel", "kek", "wew", "why"))); + var w = c.GetSetting("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList("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.Wrap("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); + var w = c.GetSetting("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value); w.Value = shortcut; @@ -254,7 +254,7 @@ namespace BepInEx.Configuration.Tests const string testVal = "new line\n test \t\0"; var c = MakeConfig(); - var w = c.Wrap("Cat", "Key", testVal, new ConfigDescription("Test")); + var w = c.GetSetting("Cat", "Key", testVal, new ConfigDescription("Test")); Assert.AreEqual(testVal, w.Value); Assert.IsFalse(w.ConfigEntry.GetSerializedValue().Any(x => x == '\n')); @@ -279,7 +279,7 @@ namespace BepInEx.Configuration.Tests File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); c.Reload(); - var w = c.Wrap("Cat", "Key", "", new ConfigDescription("Test")); + var w = c.GetSetting("Cat", "Key", "", new ConfigDescription("Test")); Assert.AreEqual(unescaped, w.Value); From 091b2fa788f63a5590f554148d764ac2b21ca1d0 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 25 Aug 2019 21:12:27 +0200 Subject: [PATCH 28/54] Convert KeyboardShortcut into a struct --- BepInEx/Configuration/KeyboardShortcut.cs | 35 ++++++++++--------- BepInExTests/Configuration/ConfigFileTests.cs | 32 +++++++++++++++-- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index af1b81f..fb493a2 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -17,7 +17,7 @@ namespace BepInEx.Configuration /// /// How to use: Use in this class instead of in the Update loop. /// - public class KeyboardShortcut + public struct KeyboardShortcut { static KeyboardShortcut() { @@ -25,11 +25,16 @@ namespace BepInEx.Configuration typeof(KeyboardShortcut), new TypeConverter { - ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize() ?? "", + ConvertToString = (o, type) => ((KeyboardShortcut)o).Serialize(), ConvertToObject = (s, type) => Deserialize(s) }); } + /// + /// Shortcut that never triggers. + /// + public static readonly KeyboardShortcut Empty = new KeyboardShortcut(); + /// /// All KeyCode values that can be used in a keyboard shortcut. /// @@ -55,15 +60,6 @@ namespace BepInEx.Configuration _allKeysLookup = new HashSet(_allKeys); } - /// - /// Create a new empty shortcut. - /// - public KeyboardShortcut() - { - _allKeys = SanitizeKeys(); - _allKeysLookup = new HashSet(_allKeys); - } - private static KeyCode[] SanitizeKeys(params KeyCode[] keys) { if (keys.Length == 0 || keys[0] == KeyCode.None) @@ -76,12 +72,12 @@ namespace BepInEx.Configuration /// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered. /// If the combination is empty, is returned. /// - public KeyCode MainKey => _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; + public KeyCode MainKey => _allKeys != null && _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; /// /// Modifiers of the key combination, if any. /// - public IEnumerable Modifiers => _allKeys.Skip(1); + public IEnumerable Modifiers => _allKeys?.Skip(1) ?? Enumerable.Empty(); /// /// Attempt to deserialize key combination from the string. @@ -97,7 +93,7 @@ namespace BepInEx.Configuration catch (SystemException ex) { Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message); - return new KeyboardShortcut(); + return Empty; } } @@ -106,6 +102,7 @@ namespace BepInEx.Configuration /// public string Serialize() { + if (_allKeys == null) return string.Empty; return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray()); } @@ -144,11 +141,13 @@ namespace BepInEx.Configuration private bool ModifierKeyTest() { + var lookup = _allKeysLookup; + var mainKey = MainKey; return AllKeyCodes.All(c => { - if (_allKeysLookup.Contains(c)) + if (lookup.Contains(c)) { - if (_allKeys[0] == c) + if (mainKey == c) return true; return Input.GetKey(c); } @@ -167,12 +166,14 @@ namespace BepInEx.Configuration /// public override bool Equals(object obj) { - return obj is KeyboardShortcut shortcut && _allKeys.SequenceEqual(shortcut._allKeys); + return obj is KeyboardShortcut shortcut && MainKey == shortcut.MainKey && Modifiers.SequenceEqual(shortcut.Modifiers); } /// public override int GetHashCode() { + if (_allKeys == null || _allKeys.Length == 0) return 0; + var hc = _allKeys.Length; for (var i = 0; i < _allKeys.Length; i++) hc = unchecked(hc * 31 + (int)_allKeys[i]); diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index 6cad1e0..12938b6 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -232,7 +232,7 @@ namespace BepInEx.Configuration.Tests } [TestMethod] - public void KeyShortcutTest() + public void KeyboardShortcutTest() { var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y); var s = shortcut.Serialize(); @@ -247,7 +247,33 @@ namespace BepInEx.Configuration.Tests c.Reload(); Assert.AreEqual(shortcut, w.Value); } - + + [TestMethod] + public void KeyboardShortcutTest2() + { + Assert.AreEqual(KeyboardShortcut.Empty, new KeyboardShortcut()); + + var c = MakeConfig(); + + var w = c.GetSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test")); + + Assert.AreEqual("", w.ConfigEntry.GetSerializedValue()); + + w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + Assert.AreEqual(KeyboardShortcut.Empty, w.Value); + + var testShortcut = new KeyboardShortcut(KeyCode.A, KeyCode.B, KeyCode.C); + w.Value = testShortcut; + + w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + Assert.AreEqual(testShortcut, w.Value); + + c.Save(); + c.Reload(); + + Assert.AreEqual(testShortcut, w.Value); + } + [TestMethod] public void StringEscapeChars() { @@ -274,7 +300,7 @@ namespace BepInEx.Configuration.Tests var c = MakeConfig(); var unescaped = @"D:\test\p ath"; - foreach (string testVal in new[]{ unescaped , @"D:\\test\\p ath" }) + foreach (string testVal in new[] { unescaped, @"D:\\test\\p ath" }) { File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); c.Reload(); From b60442ed5ef4fe88a81a41db9a6ce73635844f6e Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 25 Aug 2019 22:39:53 +0200 Subject: [PATCH 29/54] Fixed hashcode for empty hotkeys --- BepInEx/Configuration/KeyboardShortcut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index fb493a2..b37ce2d 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -172,11 +172,11 @@ namespace BepInEx.Configuration /// public override int GetHashCode() { - if (_allKeys == null || _allKeys.Length == 0) return 0; + if (MainKey == KeyCode.None) return 0; var hc = _allKeys.Length; for (var i = 0; i < _allKeys.Length; i++) - hc = unchecked(hc * 31 + (int)_allKeys[i]); + hc = unchecked(hc * 31 + (int)i); return hc; } } From 6abda092cb9eb5a7456280607d461080b8a4e391 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sun, 25 Aug 2019 23:10:38 +0200 Subject: [PATCH 30/54] Expose AcceptableValueList.AcceptableValues --- BepInEx/Configuration/AcceptableValueList.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs index 7371b1a..1c36983 100644 --- a/BepInEx/Configuration/AcceptableValueList.cs +++ b/BepInEx/Configuration/AcceptableValueList.cs @@ -8,7 +8,10 @@ namespace BepInEx.Configuration /// public sealed class AcceptableValueList : AcceptableValueBase where T : IEquatable { - private readonly T[] _acceptableValues; + /// + /// List of values that a setting can take. + /// + public T[] AcceptableValues { get; } /// /// Specify the list of acceptable values for a setting. @@ -19,7 +22,7 @@ namespace BepInEx.Configuration if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues)); if (acceptableValues.Length == 0) throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues)); - _acceptableValues = acceptableValues; + AcceptableValues = acceptableValues; } /// @@ -28,19 +31,19 @@ namespace BepInEx.Configuration if (IsValid(value)) return value; - return _acceptableValues[0]; + return AcceptableValues[0]; } /// public override bool IsValid(object value) { - return value is T v && _acceptableValues.Any(x => x.Equals(v)); + return value is T v && AcceptableValues.Any(x => x.Equals(v)); } /// public override string ToSerializedString() { - return "# Acceptable values: " + string.Join(", ", _acceptableValues.Select(x => x.ToString()).ToArray()); + return "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray()); } } } From 5f0e229e5243cb25cf4be29ec091c4510b344c6b Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Tue, 27 Aug 2019 02:04:40 +0200 Subject: [PATCH 31/54] Hash tweak --- BepInEx/Configuration/KeyboardShortcut.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index b37ce2d..31f3002 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -174,10 +174,7 @@ namespace BepInEx.Configuration { if (MainKey == KeyCode.None) return 0; - var hc = _allKeys.Length; - for (var i = 0; i < _allKeys.Length; i++) - hc = unchecked(hc * 31 + (int)i); - return hc; + return _allKeys.Aggregate(_allKeys.Length, (current, item) => unchecked(current * 31 + (int)item)); } } } \ No newline at end of file From 138c5037fda3727508a1af669d34b0db9f164b6f Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Tue, 27 Aug 2019 03:34:12 +0200 Subject: [PATCH 32/54] Added converter for Color, tweaks to config comment generation --- BepInEx/Configuration/ConfigEntryBase.cs | 29 +++++++++++----------- BepInEx/Configuration/TomlTypeConverter.cs | 16 ++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs index f6435d2..ffc2ab7 100644 --- a/BepInEx/Configuration/ConfigEntryBase.cs +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using BepInEx.Logging; +using UnityEngine; namespace BepInEx.Configuration { @@ -75,8 +76,7 @@ namespace BepInEx.Configuration } 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}"); + Logging.Logger.Log(LogLevel.Warning, $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}"); } } @@ -116,32 +116,31 @@ namespace BepInEx.Configuration public void WriteDescription(StreamWriter writer) { bool hasDescription = Description != null; - bool hasType = SettingType != null; if (hasDescription) writer.WriteLine(Description.ToSerializedString()); - if (hasType) - { - if (SettingType.IsEnum && SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) - writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + writer.WriteLine("# Setting type: " + SettingType.Name); - writer.WriteLine("# Setting type: " + SettingType.Name); - - writer.WriteLine("# Default value: " + DefaultValue); - } + writer.WriteLine("# Default value: " + DefaultValue); if (hasDescription && Description.AcceptableValues != null) { writer.WriteLine(Description.AcceptableValues.ToSerializedString()); } - else if (hasType) + else { - /*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.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) + writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); + } + else if (SettingType == typeof(bool)) + writer.WriteLine("# Acceptable values: True, False"); + else if (SettingType == typeof(Color)) + writer.WriteLine("# Acceptable values: Hex HTML color codes"); } } } diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index baddc4a..2f912b6 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; +using UnityEngine; namespace BepInEx.Configuration { @@ -102,6 +103,21 @@ namespace BepInEx.Configuration ConvertToString = (obj, type) => obj.ToString(), ConvertToObject = (str, type) => Enum.Parse(type, str, true), }, + + //unity types + + [typeof(Color)] = new TypeConverter + { + ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color)obj), + ConvertToObject = (str, type) => + { + if (string.IsNullOrEmpty(str)) return Color.clear; + Color c; + if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out c)) + throw new FormatException("Invalid color string, expected hex #RRGGBBAA"); + return c; + }, + }, }; /// From ee782a00c49689e904992bf655136305a502b3aa Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Tue, 27 Aug 2019 04:03:19 +0200 Subject: [PATCH 33/54] Roll back Color converter --- BepInEx/Configuration/ConfigEntryBase.cs | 16 ++++------------ BepInEx/Configuration/TomlTypeConverter.cs | 20 +++----------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs index ffc2ab7..13da296 100644 --- a/BepInEx/Configuration/ConfigEntryBase.cs +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using BepInEx.Logging; -using UnityEngine; namespace BepInEx.Configuration { @@ -128,19 +127,12 @@ namespace BepInEx.Configuration { writer.WriteLine(Description.AcceptableValues.ToSerializedString()); } - else + else if (SettingType.IsEnum) { - if (SettingType.IsEnum) - { - writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType))); + writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType))); - if (SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) - writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); - } - else if (SettingType == typeof(bool)) - writer.WriteLine("# Acceptable values: True, False"); - else if (SettingType == typeof(Color)) - writer.WriteLine("# Acceptable values: Hex HTML color codes"); + if (SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) + writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); } } } diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 2f912b6..9b9d53f 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; -using UnityEngine; namespace BepInEx.Configuration { @@ -11,6 +10,7 @@ namespace BepInEx.Configuration /// public static class TomlTypeConverter { + // Don't put anything from UnityEngine here or it will break preloader (loads the assembly before it's patched) private static Dictionary TypeConverters { get; } = new Dictionary { [typeof(string)] = new TypeConverter @@ -103,21 +103,6 @@ namespace BepInEx.Configuration ConvertToString = (obj, type) => obj.ToString(), ConvertToObject = (str, type) => Enum.Parse(type, str, true), }, - - //unity types - - [typeof(Color)] = new TypeConverter - { - ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color)obj), - ConvertToObject = (str, type) => - { - if (string.IsNullOrEmpty(str)) return Color.clear; - Color c; - if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out c)) - throw new FormatException("Invalid color string, expected hex #RRGGBBAA"); - return c; - }, - }, }; /// @@ -167,7 +152,8 @@ namespace BepInEx.Configuration } /// - /// Add a new type converter for a given type. + /// Add a new type converter for a given type. + /// If a different converter is already added, an ArgumentException is thrown. /// public static void AddConverter(Type type, TypeConverter converter) { From bc1c51c137a58c07ef438340a910c0d9b336aeab Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Tue, 27 Aug 2019 04:05:42 +0200 Subject: [PATCH 34/54] Don't crash AddConverter on duplicates --- BepInEx/Configuration/TomlTypeConverter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 9b9d53f..8514f87 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -153,15 +153,16 @@ namespace BepInEx.Configuration /// /// Add a new type converter for a given type. - /// If a different converter is already added, an ArgumentException is thrown. + /// If a different converter is already added, this call is ignored and false is returned. /// - public static void AddConverter(Type type, TypeConverter converter) + public static bool AddConverter(Type type, TypeConverter converter) { if (type == null) throw new ArgumentNullException(nameof(type)); if (converter == null) throw new ArgumentNullException(nameof(converter)); - if (CanConvert(type)) throw new ArgumentException("The specified type already has a converter assigned to it", nameof(type)); + if (CanConvert(type)) return false; TypeConverters.Add(type, converter); + return true; } /// From 0a9825f278bc4159ccf549c06028dd4dae58a7e5 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Tue, 27 Aug 2019 14:11:03 +0200 Subject: [PATCH 35/54] Added lazy-loaded converters for UnityEngine types --- BepInEx/Configuration/TomlTypeConverter.cs | 59 ++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 8514f87..46313bc 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; +using UnityEngine; +using Logger = BepInEx.Logging.Logger; namespace BepInEx.Configuration { @@ -10,7 +13,7 @@ namespace BepInEx.Configuration /// public static class TomlTypeConverter { - // Don't put anything from UnityEngine here or it will break preloader (loads the assembly before it's patched) + // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead private static Dictionary TypeConverters { get; } = new Dictionary { [typeof(string)] = new TypeConverter @@ -147,7 +150,12 @@ namespace BepInEx.Configuration if (valueType.IsEnum) return TypeConverters[typeof(Enum)]; - TypeConverters.TryGetValue(valueType, out var result); + if (!TypeConverters.TryGetValue(valueType, out var result) && !_lazyLoadedConverters) + { + _lazyLoadedConverters = true; + LazyLoadConverters(); + TypeConverters.TryGetValue(valueType, out result); + } return result; } @@ -159,7 +167,11 @@ namespace BepInEx.Configuration { if (type == null) throw new ArgumentNullException(nameof(type)); if (converter == null) throw new ArgumentNullException(nameof(converter)); - if (CanConvert(type)) return false; + if (CanConvert(type)) + { + Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName); + return false; + } TypeConverters.Add(type, converter); return true; @@ -180,5 +192,46 @@ namespace BepInEx.Configuration { return TypeConverters.Keys; } + + private static bool _lazyLoadedConverters; + private static void LazyLoadConverters() + { + try { LazyTomlConverterLoader.AddUnityEngineConverters(); } + catch (Exception ex) { Logger.LogWarning("Failed to load UnityEngine Toml converters - " + ex.Message); } + } + } + + /// + /// For types that are in assemblies that can't get loaded before preloader runs (or it won't work on these assemblies) + /// + internal static class LazyTomlConverterLoader + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static void AddUnityEngineConverters() + { + var colorConverter = new TypeConverter + { + ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color)obj), + ConvertToObject = (str, type) => + { + if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out var c)) + throw new FormatException("Invalid color string, expected hex #RRGGBBAA"); + return c; + }, + }; + + TomlTypeConverter.AddConverter(typeof(Color), colorConverter); + + var jsonConverter = new TypeConverter + { + ConvertToString = (obj, type) => JsonUtility.ToJson(obj), + ConvertToObject = (str, type) => JsonUtility.FromJson(type: type, json: str), + }; + + TomlTypeConverter.AddConverter(typeof(Vector2), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter); + } } } \ No newline at end of file From 23878e9df817bdb6b343e620d431664b388e6373 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Thu, 29 Aug 2019 19:36:33 +0200 Subject: [PATCH 36/54] Make defaultValue parameter mandatory --- BepInEx/Configuration/ConfigFile.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 076ef5e..3853b14 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -200,7 +200,7 @@ namespace BepInEx.Configuration /// Section and Key of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper GetSetting(ConfigDefinition configDefinition, T defaultValue = default(T), ConfigDescription configDescription = null) + public ConfigWrapper GetSetting(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())}"); @@ -261,7 +261,7 @@ namespace BepInEx.Configuration /// Name of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper GetSetting(string section, string key, T defaultValue = default(T), ConfigDescription configDescription = null) + public ConfigWrapper GetSetting(string section, string key, T defaultValue, ConfigDescription configDescription = null) => GetSetting(new ConfigDefinition(section, key), defaultValue, configDescription); /// From 3e33169d511a3815b0bb8a290da12324092fa061 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Thu, 29 Aug 2019 19:42:16 +0200 Subject: [PATCH 37/54] Use the first ConfigDescription in case multiple are provided --- BepInEx/Configuration/ConfigFile.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 3853b14..ee25acf 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -228,8 +228,8 @@ namespace BepInEx.Configuration { 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."); - - entry.SetDescription(configDescription); + else + entry.SetDescription(configDescription); } if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue)) From 930c6afd8f376cf96e4c6e9255cf972a5922278d Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Fri, 30 Aug 2019 01:30:21 +0200 Subject: [PATCH 38/54] Make ValueRange classes inheritable --- BepInEx/Configuration/AcceptableValueList.cs | 4 ++-- BepInEx/Configuration/AcceptableValueRange.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs index 1c36983..a9a1565 100644 --- a/BepInEx/Configuration/AcceptableValueList.cs +++ b/BepInEx/Configuration/AcceptableValueList.cs @@ -6,12 +6,12 @@ namespace BepInEx.Configuration /// /// Specify the list of acceptable values for a setting. /// - public sealed class AcceptableValueList : AcceptableValueBase where T : IEquatable + public class AcceptableValueList : AcceptableValueBase where T : IEquatable { /// /// List of values that a setting can take. /// - public T[] AcceptableValues { get; } + public virtual T[] AcceptableValues { get; } /// /// Specify the list of acceptable values for a setting. diff --git a/BepInEx/Configuration/AcceptableValueRange.cs b/BepInEx/Configuration/AcceptableValueRange.cs index 6cd3f6c..c4408dc 100644 --- a/BepInEx/Configuration/AcceptableValueRange.cs +++ b/BepInEx/Configuration/AcceptableValueRange.cs @@ -5,7 +5,7 @@ namespace BepInEx.Configuration /// /// Specify the range of acceptable values for a setting. /// - public sealed class AcceptableValueRange : AcceptableValueBase where T : IComparable + public class AcceptableValueRange : AcceptableValueBase where T : IComparable { /// Lowest acceptable value /// Highest acceptable value @@ -25,12 +25,12 @@ namespace BepInEx.Configuration /// /// Lowest acceptable value /// - public T MinValue { get; } + public virtual T MinValue { get; } /// /// Highest acceptable value /// - public T MaxValue { get; } + public virtual T MaxValue { get; } /// public override object Clamp(object value) From 04be8b17ded871aa8d9305513616fac9b3f4cdbb Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Fri, 30 Aug 2019 20:01:20 +0200 Subject: [PATCH 39/54] Simplify the config class structure --- BepInEx/BepInEx.csproj | 1 - BepInEx/Configuration/ConfigEntry.cs | 33 ------------------ BepInEx/Configuration/ConfigEntryBase.cs | 12 +++---- BepInEx/Configuration/ConfigFile.cs | 8 ++--- BepInEx/Configuration/ConfigWrapper.cs | 44 ++++++++++++------------ 5 files changed, 32 insertions(+), 66 deletions(-) delete mode 100644 BepInEx/Configuration/ConfigEntry.cs diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index e798dc1..b8c480b 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -56,7 +56,6 @@ - diff --git a/BepInEx/Configuration/ConfigEntry.cs b/BepInEx/Configuration/ConfigEntry.cs deleted file mode 100644 index 0ff907d..0000000 --- a/BepInEx/Configuration/ConfigEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace BepInEx.Configuration -{ - /// - public class ConfigEntry : ConfigEntryBase - { - private T _typedValue; - internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue) : base(configFile, definition, typeof(T), defaultValue) { } - - /// - /// Get or set the value of the setting without boxing. - /// - public T TypedValue - { - get => _typedValue; - set - { - value = ClampValue(value); - if (Equals(_typedValue, value)) - return; - - _typedValue = value; - OnSettingChanged(this); - } - } - - /// - public override object Value - { - get => TypedValue; - set => TypedValue = (T)value; - } - } -} diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs index 13da296..1d15368 100644 --- a/BepInEx/Configuration/ConfigEntryBase.cs +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -21,7 +21,7 @@ namespace BepInEx.Configuration SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); // Free type check - Value = defaultValue; + BoxedValue = defaultValue; DefaultValue = defaultValue; } @@ -41,7 +41,7 @@ namespace BepInEx.Configuration public ConfigDescription Description { get; private set; } /// - /// Type of the that this setting holds. + /// Type of the that this setting holds. /// public Type SettingType { get; } @@ -53,14 +53,14 @@ namespace BepInEx.Configuration /// /// Get or set the value of the setting. /// - public abstract object Value { get; set; } + public abstract object BoxedValue { get; set; } /// /// Get the serialized representation of the value. /// public string GetSerializedValue() { - return TomlTypeConverter.ConvertToString(Value, SettingType); + return TomlTypeConverter.ConvertToString(BoxedValue, SettingType); } /// @@ -71,7 +71,7 @@ namespace BepInEx.Configuration try { var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); - Value = newValue; + BoxedValue = newValue; } catch (Exception e) { @@ -88,7 +88,7 @@ namespace BepInEx.Configuration Description = configDescription; // Automatically calls ClampValue in case it changed - Value = Value; + BoxedValue = BoxedValue; } /// diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index ee25acf..61b8392 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -213,14 +213,14 @@ namespace BepInEx.Configuration Entries.TryGetValue(configDefinition, out var existingEntry); - if (existingEntry != null && !(existingEntry is ConfigEntry)) + if (existingEntry != null && !(existingEntry is ConfigWrapper)) throw new ArgumentException("The defined setting already exists with a different setting type - " + existingEntry.SettingType.Name); - var entry = (ConfigEntry)existingEntry; + var entry = (ConfigWrapper)existingEntry; if (entry == null) { - entry = new ConfigEntry(this, configDefinition, defaultValue); + entry = new ConfigWrapper(this, configDefinition, defaultValue); Entries[configDefinition] = entry; } @@ -242,7 +242,7 @@ namespace BepInEx.Configuration if (SaveOnConfigSet) Save(); - return new ConfigWrapper(entry); + return entry; } } finally diff --git a/BepInEx/Configuration/ConfigWrapper.cs b/BepInEx/Configuration/ConfigWrapper.cs index 660df46..74f70f6 100644 --- a/BepInEx/Configuration/ConfigWrapper.cs +++ b/BepInEx/Configuration/ConfigWrapper.cs @@ -6,44 +6,44 @@ namespace BepInEx.Configuration /// Provides access to a single setting inside of a . /// /// Type of the setting. - public sealed class ConfigWrapper + public sealed class ConfigWrapper : ConfigEntryBase { - /// - /// Entry of this setting in the . - /// - public ConfigEntry ConfigEntry { get; } - - /// - /// Unique definition of this setting. - /// - public ConfigDefinition Definition => ConfigEntry.Definition; - - /// - /// Config file this setting is inside of. - /// - public ConfigFile ConfigFile => ConfigEntry.ConfigFile; - /// /// Fired when the setting is changed. Does not detect changes made outside from this object. /// public event EventHandler SettingChanged; + private T _typedValue; + /// /// Value of this setting. /// public T Value { - get => ConfigEntry.TypedValue; - set => ConfigEntry.TypedValue = value; + get => _typedValue; + set + { + value = ClampValue(value); + if (Equals(_typedValue, value)) + return; + + _typedValue = value; + OnSettingChanged(this); + } } - internal ConfigWrapper(ConfigEntry configEntry) + /// + public override object BoxedValue { - ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry)); + get => Value; + set => Value = (T)value; + } - configEntry.ConfigFile.SettingChanged += (sender, args) => + internal ConfigWrapper(ConfigFile configFile, ConfigDefinition definition, T defaultValue) : base(configFile, definition, typeof(T), defaultValue) + { + configFile.SettingChanged += (sender, args) => { - if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args); + if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args); }; } } From d0b529166d3d122459c09c561dcd2f2b3ceef653 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Fri, 30 Aug 2019 20:01:44 +0200 Subject: [PATCH 40/54] Fixed possible race condition --- BepInEx/Configuration/ConfigFile.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 61b8392..3f74782 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -205,9 +205,9 @@ namespace BepInEx.Configuration 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 + lock (_ioLock) { - lock (_ioLock) + try { _disableSaving = true; @@ -244,10 +244,10 @@ namespace BepInEx.Configuration return entry; } - } - finally - { - _disableSaving = false; + finally + { + _disableSaving = false; + } } } From 1523ad3eaab086e23c8f0ea5315c69da26246af1 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Fri, 30 Aug 2019 20:08:21 +0200 Subject: [PATCH 41/54] Match Toml spec --- BepInEx/Configuration/ConfigDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 99b2334..3a0899f 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -84,7 +84,7 @@ namespace BepInEx.Configuration /// public override string ToString() { - return Section + " / " + Key; + return Section + "." + Key; } } } \ No newline at end of file From 72fc21fc1c3a3ba9dd23e1faba2d092f9c2cc60f Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Fri, 30 Aug 2019 20:09:08 +0200 Subject: [PATCH 42/54] Remove MissingDependencyException --- BepInEx/Contract/Attributes.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/BepInEx/Contract/Attributes.cs b/BepInEx/Contract/Attributes.cs index 0421c41..ca65584 100644 --- a/BepInEx/Contract/Attributes.cs +++ b/BepInEx/Contract/Attributes.cs @@ -292,15 +292,6 @@ namespace BepInEx } } - /// - /// An exception which is thrown when a plugin's dependencies cannot be found. - /// - [Obsolete] - public class MissingDependencyException : Exception - { - public MissingDependencyException(string message) : base(message) { } - } - #endregion #region Build configuration From 47a248e62311e5760d0b77887efff894d2af2b80 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Sat, 31 Aug 2019 12:49:30 +0200 Subject: [PATCH 43/54] Less aggressive string unescaping --- BepInEx/Configuration/TomlTypeConverter.cs | 111 ++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 46313bc..676a18d 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; +using System.Text; using System.Text.RegularExpressions; using UnityEngine; using Logger = BepInEx.Logging.Logger; @@ -18,13 +19,13 @@ namespace BepInEx.Configuration { [typeof(string)] = new TypeConverter { - ConvertToString = (obj, type) => Regex.Escape((string)obj), + ConvertToString = (obj, type) => Escape((string)obj), ConvertToObject = (str, type) => { // Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test) if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)")) return str; - return Regex.Unescape(str); + return Unescape(str); }, }, [typeof(bool)] = new TypeConverter @@ -199,6 +200,112 @@ namespace BepInEx.Configuration try { LazyTomlConverterLoader.AddUnityEngineConverters(); } catch (Exception ex) { Logger.LogWarning("Failed to load UnityEngine Toml converters - " + ex.Message); } } + + private static string Escape(this string txt) + { + var stringBuilder = new StringBuilder(txt.Length + 2); + foreach (char c in txt) + switch (c) + { + case '\0': + stringBuilder.Append(@"\0"); + break; + case '\a': + stringBuilder.Append(@"\a"); + break; + case '\b': + stringBuilder.Append(@"\b"); + break; + case '\t': + stringBuilder.Append(@"\t"); + break; + case '\n': + stringBuilder.Append(@"\n"); + break; + case '\v': + stringBuilder.Append(@"\v"); + break; + case '\f': + stringBuilder.Append(@"\f"); + break; + case '\r': + stringBuilder.Append(@"\r"); + break; + case '\'': + stringBuilder.Append(@"\'"); + break; + case '\\': + stringBuilder.Append(@"\"); + break; + case '\"': + stringBuilder.Append(@"\"""); + break; + default: + stringBuilder.Append(c); + break; + } + return stringBuilder.ToString(); + } + + private static string Unescape(this string txt) + { + if (string.IsNullOrEmpty(txt)) + return txt; + var stringBuilder = new StringBuilder(txt.Length); + for (int i = 0; i < txt.Length;) + { + int num = txt.IndexOf('\\', i); + if (num < 0 || num == txt.Length - 1) + num = txt.Length; + stringBuilder.Append(txt, i, num - i); + if (num >= txt.Length) + break; + char c = txt[num + 1]; + switch (c) + { + case '0': + stringBuilder.Append('\0'); + break; + case 'a': + stringBuilder.Append('\a'); + break; + case 'b': + stringBuilder.Append('\b'); + break; + case 't': + stringBuilder.Append('\t'); + break; + case 'n': + stringBuilder.Append('\n'); + break; + case 'v': + stringBuilder.Append('\v'); + break; + case 'f': + stringBuilder.Append('\f'); + break; + case 'r': + stringBuilder.Append('\r'); + break; + case '\'': + stringBuilder.Append('\''); + break; + case '\"': + stringBuilder.Append('\"'); + break; + case '\\': + stringBuilder.Append('\\'); + break; + default: + stringBuilder.Append('\\').Append(c); + break; + } + + i = num + 2; + } + + return stringBuilder.ToString(); + } } /// From f9eee41b330ac0c836f187269cebd5d91a7d0baa Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 16 Sep 2019 18:25:16 +0200 Subject: [PATCH 44/54] Split GetSetting and AddSetting; Roll back to separate ConfigWrapper and ConfigEntry for better compatibility and naming --- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 6 +- BepInEx.Preloader/Preloader.cs | 12 +-- BepInEx.sln.DotSettings | 4 +- BepInEx/Bootstrap/Chainloader.cs | 10 +-- BepInEx/Bootstrap/TypeLoader.cs | 2 +- BepInEx/Configuration/AcceptableValueBase.cs | 2 +- BepInEx/Configuration/AcceptableValueList.cs | 2 +- BepInEx/Configuration/AcceptableValueRange.cs | 2 +- BepInEx/Configuration/ConfigDescription.cs | 7 +- BepInEx/Configuration/ConfigEntryBase.cs | 85 ++++++++++++------ BepInEx/Configuration/ConfigFile.cs | 86 +++++++++++-------- BepInEx/Configuration/ConfigWrapper.cs | 45 +++++----- BepInEx/Configuration/KeyboardShortcut.cs | 2 +- BepInEx/ConsoleUtil/ConsoleWindow.cs | 4 +- BepInEx/Logging/ConsoleLogListener.cs | 2 +- BepInExTests/Configuration/ConfigFileTests.cs | 34 ++++---- submodules/MonoMod | 2 +- 17 files changed, 177 insertions(+), 130 deletions(-) diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 1949bad..0e2828e 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -277,17 +277,17 @@ namespace BepInEx.Preloader.Patching #region Config - private static readonly ConfigWrapper ConfigDumpAssemblies = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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.")); diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index 6005087..2ccce75 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -237,32 +237,32 @@ namespace BepInEx.Preloader #region Config - private static readonly ConfigWrapper ConfigEntrypointAssembly = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigEntrypointType = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigEntrypointMethod = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigApplyRuntimePatches = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigShimHarmony = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.AddSetting( "Logging", "PreloaderConsoleOutRedirection", true, new ConfigDescription("Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.")); diff --git a/BepInEx.sln.DotSettings b/BepInEx.sln.DotSettings index 2590452..dcdeee3 100644 --- a/BepInEx.sln.DotSettings +++ b/BepInEx.sln.DotSettings @@ -211,5 +211,7 @@ True True True + True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index 2263ce9..c12b312 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -334,27 +334,27 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.AddSetting( "Logging", "UnityLogListening", true, new ConfigDescription("Enables showing unity log messages in the BepInEx logging system.")); - private static readonly ConfigWrapper ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "WriteUnityLog", false, new ConfigDescription("Include unity log messages in log file output.")); - private static readonly ConfigWrapper ConfigDiskAppend = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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 ConfigDiskLogging = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "Enabled", true, new ConfigDescription("Enables writing log messages to disk.")); - private static readonly ConfigWrapper ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "DisplayedLogLevel", LogLevel.Info, new ConfigDescription("Only displays the specified log level and above in the console output.")); diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index 1fbea2a..f8ace67 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -257,7 +257,7 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry 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.")); diff --git a/BepInEx/Configuration/AcceptableValueBase.cs b/BepInEx/Configuration/AcceptableValueBase.cs index f5118c2..f066e17 100644 --- a/BepInEx/Configuration/AcceptableValueBase.cs +++ b/BepInEx/Configuration/AcceptableValueBase.cs @@ -31,6 +31,6 @@ namespace BepInEx.Configuration /// /// Get the string for use in config files. /// - public abstract string ToSerializedString(); + public abstract string ToDescriptionString(); } } diff --git a/BepInEx/Configuration/AcceptableValueList.cs b/BepInEx/Configuration/AcceptableValueList.cs index a9a1565..340aa39 100644 --- a/BepInEx/Configuration/AcceptableValueList.cs +++ b/BepInEx/Configuration/AcceptableValueList.cs @@ -41,7 +41,7 @@ namespace BepInEx.Configuration } /// - public override string ToSerializedString() + public override string ToDescriptionString() { return "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray()); } diff --git a/BepInEx/Configuration/AcceptableValueRange.cs b/BepInEx/Configuration/AcceptableValueRange.cs index c4408dc..e94eb30 100644 --- a/BepInEx/Configuration/AcceptableValueRange.cs +++ b/BepInEx/Configuration/AcceptableValueRange.cs @@ -51,7 +51,7 @@ namespace BepInEx.Configuration } /// - public override string ToSerializedString() + public override string ToDescriptionString() { return $"# Acceptable value range: From {MinValue} to {MaxValue}"; } diff --git a/BepInEx/Configuration/ConfigDescription.cs b/BepInEx/Configuration/ConfigDescription.cs index fd85d70..2ed734e 100644 --- a/BepInEx/Configuration/ConfigDescription.cs +++ b/BepInEx/Configuration/ConfigDescription.cs @@ -36,11 +36,8 @@ namespace BepInEx.Configuration public object[] Tags { get; } /// - /// Convert the description object into a form suitable for writing into a config file. + /// An empty description. /// - public string ToSerializedString() - { - return $"## {Description.Replace("\n", "\n## ")}"; - } + public static ConfigDescription Empty { get; } = new ConfigDescription(""); } } diff --git a/BepInEx/Configuration/ConfigEntryBase.cs b/BepInEx/Configuration/ConfigEntryBase.cs index 1d15368..113e3e3 100644 --- a/BepInEx/Configuration/ConfigEntryBase.cs +++ b/BepInEx/Configuration/ConfigEntryBase.cs @@ -5,6 +5,52 @@ using BepInEx.Logging; namespace BepInEx.Configuration { + /// + /// Provides access to a single setting inside of a . + /// + /// Type of the setting. + public sealed class ConfigEntry : ConfigEntryBase + { + /// + /// Fired when the setting is changed. Does not detect changes made outside from this object. + /// + public event EventHandler SettingChanged; + + private T _typedValue; + + /// + /// Value of this setting. + /// + public T Value + { + get => _typedValue; + set + { + value = ClampValue(value); + if (Equals(_typedValue, value)) + return; + + _typedValue = value; + OnSettingChanged(this); + } + } + + /// + 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); + }; + } + } + /// /// Container for a single setting of a . /// Each config entry is linked to one config file. @@ -14,15 +60,20 @@ namespace BepInEx.Configuration /// /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. /// - 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; } /// @@ -38,7 +89,7 @@ namespace BepInEx.Configuration /// /// Description / metadata of this setting. /// - public ConfigDescription Description { get; private set; } + public ConfigDescription Description { get; } /// /// Type of the 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; - } - /// /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. /// protected T ClampValue(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 /// 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) { diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 3f74782..89adc44 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -9,7 +9,7 @@ using BepInEx.Logging; namespace BepInEx.Configuration { /// - /// A helper class to handle persistent data. + /// A helper class to handle persistent data. All public methods are thread-safe. /// public class ConfigFile { @@ -38,7 +38,7 @@ namespace BepInEx.Configuration /// /// 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 + /// If you want to access and modify an existing setting then use /// instead with no description. /// public ConfigEntryBase[] GetConfigEntries() @@ -192,45 +192,46 @@ namespace BepInEx.Configuration #region Wraps /// - /// 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 . + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + public ConfigEntry GetSetting(string section, string key) + { + lock (_ioLock) + { + Entries.TryGetValue(new ConfigDefinition(section, key), out var entry); + return (ConfigEntry)entry; + } + } + + /// + /// 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. /// /// Type of the value contained in this setting. /// Section and Key of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper GetSetting(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) + public ConfigEntry AddSetting(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)) - throw new ArgumentException("The defined setting already exists with a different setting type - " + existingEntry.SettingType.Name); - - var entry = (ConfigWrapper)existingEntry; - - if (entry == null) - { - entry = new ConfigWrapper(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(this, configDefinition, defaultValue, configDescription); + Entries[configDefinition] = entry; if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue)) { @@ -252,27 +253,36 @@ namespace BepInEx.Configuration } /// - /// 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. /// /// Type of the value contained in this setting. /// Section/category/group of the setting. Settings are grouped by this. /// Name of the setting. /// Value of the setting if the setting was not created yet. /// Description of the setting shown to the user. - public ConfigWrapper GetSetting(string section, string key, T defaultValue, ConfigDescription configDescription = null) - => GetSetting(new ConfigDefinition(section, key), defaultValue, configDescription); + public ConfigEntry AddSetting(string section, string key, T defaultValue, ConfigDescription configDescription = null) + => AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription); - /// - [Obsolete("Use GetSetting instead")] + /// + /// Access a setting. Use AddSetting and GetSetting instead. + /// + [Obsolete("Use AddSetting and GetSetting instead")] public ConfigWrapper Wrap(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(section, key) ?? AddSetting(section, key, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + return new ConfigWrapper(setting); + } + } - /// - [Obsolete("Use GetSetting instead")] + /// + /// Access a setting. Use AddSetting and GetSetting instead. + /// + [Obsolete("Use AddSetting and GetSetting instead")] public ConfigWrapper Wrap(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 SettingChanged; internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) - { + {ThreadingHelper.SynchronizingObject.InvokeRequired if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); if (SaveOnConfigSet) diff --git a/BepInEx/Configuration/ConfigWrapper.cs b/BepInEx/Configuration/ConfigWrapper.cs index 74f70f6..a216e15 100644 --- a/BepInEx/Configuration/ConfigWrapper.cs +++ b/BepInEx/Configuration/ConfigWrapper.cs @@ -6,44 +6,45 @@ namespace BepInEx.Configuration /// Provides access to a single setting inside of a . /// /// Type of the setting. - public sealed class ConfigWrapper : ConfigEntryBase + [Obsolete("Use ConfigFile from new AddSetting overloads instead")] + public sealed class ConfigWrapper { + /// + /// Entry of this setting in the . + /// + public ConfigEntry ConfigEntry { get; } + + /// + /// Unique definition of this setting. + /// + public ConfigDefinition Definition => ConfigEntry.Definition; + + /// + /// Config file this setting is inside of. + /// + public ConfigFile ConfigFile => ConfigEntry.ConfigFile; + /// /// Fired when the setting is changed. Does not detect changes made outside from this object. /// public event EventHandler SettingChanged; - private T _typedValue; - /// /// Value of this setting. /// 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; } - /// - public override object BoxedValue + internal ConfigWrapper(ConfigEntry 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); }; } } diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs index 31f3002..383f74e 100644 --- a/BepInEx/Configuration/KeyboardShortcut.cs +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -12,7 +12,7 @@ namespace BepInEx.Configuration /// triggered when the user presses the exact combination. For example, F + LeftCtrl 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 + /// Can be used as a value of a setting in /// to allow user to change this shortcut and have the changes saved. /// /// How to use: Use in this class instead of in the Update loop. diff --git a/BepInEx/ConsoleUtil/ConsoleWindow.cs b/BepInEx/ConsoleUtil/ConsoleWindow.cs index 46ede11..eeb67e0 100644 --- a/BepInEx/ConsoleUtil/ConsoleWindow.cs +++ b/BepInEx/ConsoleUtil/ConsoleWindow.cs @@ -13,12 +13,12 @@ namespace UnityInjector.ConsoleUtil { internal class ConsoleWindow { - public static readonly ConfigWrapper ConfigConsoleEnabled = ConfigFile.CoreConfig.GetSetting( + public static readonly ConfigEntry ConfigConsoleEnabled = ConfigFile.CoreConfig.AddSetting( "Logging.Console", "Enabled", false, new ConfigDescription("Enables showing a console for log output.")); - public static readonly ConfigWrapper ConfigConsoleShiftJis = ConfigFile.CoreConfig.GetSetting( + public static readonly ConfigEntry ConfigConsoleShiftJis = ConfigFile.CoreConfig.AddSetting( "Logging.Console", "ShiftJisEncoding", false, new ConfigDescription("If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.")); diff --git a/BepInEx/Logging/ConsoleLogListener.cs b/BepInEx/Logging/ConsoleLogListener.cs index fbe116a..1bc9d59 100644 --- a/BepInEx/Logging/ConsoleLogListener.cs +++ b/BepInEx/Logging/ConsoleLogListener.cs @@ -23,7 +23,7 @@ namespace BepInEx.Logging public void Dispose() { } - private static readonly ConfigWrapper ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.GetSetting( + private static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting( "Logging.Console","DisplayedLogLevel", LogLevel.Info, new ConfigDescription("Only displays the specified log level and above in the console output.")); diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index 12938b6..ade6e19 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/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(0, 2))); + var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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(1, 2))); + c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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(1, 2))); + var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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(0, 2))); + var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(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("lel", "kek", "wew", "why"))); + var w = c.AddSetting("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList("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); diff --git a/submodules/MonoMod b/submodules/MonoMod index 9462a0f..a70072c 160000 --- a/submodules/MonoMod +++ b/submodules/MonoMod @@ -1 +1 @@ -Subproject commit 9462a0f75b669606b59a4648a3461338d2be32c2 +Subproject commit a70072cdf759ac0cfa80991fcd2cca67d3eec130 From 503e39675fad2c9f5317066c96211ac14c67403f Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 16 Sep 2019 18:40:59 +0200 Subject: [PATCH 45/54] Make ThreadingHelper run before other scripts --- BepInEx/ThreadingHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BepInEx/ThreadingHelper.cs b/BepInEx/ThreadingHelper.cs index ce6c97d..316b07f 100644 --- a/BepInEx/ThreadingHelper.cs +++ b/BepInEx/ThreadingHelper.cs @@ -10,6 +10,7 @@ namespace BepInEx /// /// Provides methods for running code on other threads and synchronizing with the main thread. /// + [DefaultExecutionOrder(int.MinValue)] public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke { private readonly object _invokeLock = new object(); @@ -47,6 +48,7 @@ namespace BepInEx private void Update() { + // The CurrentThread can change between Awake and later methods, it's safest to get it here. if (_mainThread == null) _mainThread = Thread.CurrentThread; From 50ea73bfbcf955e2ece32a787863c08de440350a Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 16 Sep 2019 18:42:19 +0200 Subject: [PATCH 46/54] Remove my mistakes --- BepInEx/Configuration/ConfigFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 89adc44..1b53ae9 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -299,7 +299,7 @@ namespace BepInEx.Configuration public event EventHandler SettingChanged; internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) - {ThreadingHelper.SynchronizingObject.InvokeRequired + { if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); if (SaveOnConfigSet) From 80453026bb32d88c1c66fe053203c334795628c6 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 16 Sep 2019 18:45:04 +0200 Subject: [PATCH 47/54] Roll back VS varsion in sln --- BepInEx.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx.sln b/BepInEx.sln index 3c6a5ae..044fbfe 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.757 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28922.388 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx", "BepInEx\BepInEx.csproj", "{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}" EndProject From d2bab3824eab7d7a0177030b66e11dff58c706b5 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Mon, 16 Sep 2019 18:55:32 +0200 Subject: [PATCH 48/54] Made BaseUnityPlugin.Config public to make access by other plugins easier --- BepInEx/Contract/BaseUnityPlugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx/Contract/BaseUnityPlugin.cs b/BepInEx/Contract/BaseUnityPlugin.cs index 9470836..0ecd8a5 100644 --- a/BepInEx/Contract/BaseUnityPlugin.cs +++ b/BepInEx/Contract/BaseUnityPlugin.cs @@ -23,7 +23,7 @@ namespace BepInEx /// Default config file tied to this plugin. The config file will not be created until /// any settings are added and changed, or is called. /// - protected ConfigFile Config { get; } + public ConfigFile Config { get; } /// /// Create a new instance of a plugin and all of its tied in objects. From 4c20c05b7348778d186cdfab5ff1e8270f0e6db6 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Wed, 18 Sep 2019 17:59:12 +0200 Subject: [PATCH 49/54] Added simpler AddSetting overload --- BepInEx/Configuration/ConfigFile.cs | 36 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 1b53ae9..464523c 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -208,15 +208,15 @@ namespace BepInEx.Configuration } } - /// - /// 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. - /// - /// Type of the value contained in this setting. - /// Section and Key of the setting. - /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user. - public ConfigEntry AddSetting(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) + /// + /// 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. + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user and other metadata. + public ConfigEntry AddSetting(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())}"); @@ -260,14 +260,26 @@ namespace BepInEx.Configuration /// Section/category/group of the setting. Settings are grouped by this. /// Name of the setting. /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user. + /// Description of the setting shown to the user and other metadata. public ConfigEntry AddSetting(string section, string key, T defaultValue, ConfigDescription configDescription = null) => AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription); /// - /// Access a setting. Use AddSetting and GetSetting instead. + /// 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. /// - [Obsolete("Use AddSetting and GetSetting instead")] + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Simple description of the setting shown to the user. + public ConfigEntry AddSetting(string section, string key, T defaultValue, string description) + => AddSetting(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); + + /// + /// Access a setting. Use AddSetting and GetSetting instead. + /// + [Obsolete("Use AddSetting and GetSetting instead")] public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T)) { lock (_ioLock) From 13358f7237b3132b56dc7dcd1d89bb5c11140f34 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Wed, 18 Sep 2019 17:59:34 +0200 Subject: [PATCH 50/54] Fix tests for latest builds --- BepInExTests/Configuration/ConfigFileTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index ade6e19..e474130 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -85,7 +85,7 @@ namespace BepInEx.Configuration.Tests var c = MakeConfig(); File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); c.Reload(); - var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test")); + var w = c.AddSetting("Cat", "Key", 0, "Test"); Assert.AreEqual(w.Value, 1); var w2 = c.AddSetting("Cat", "Key2", 0, new ConfigDescription("Test")); Assert.AreEqual(w2.Value, 0); @@ -257,15 +257,15 @@ namespace BepInEx.Configuration.Tests var w = c.AddSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test")); - Assert.AreEqual("", w.ConfigEntry.GetSerializedValue()); + Assert.AreEqual("", w.GetSerializedValue()); - w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + w.SetSerializedValue(w.GetSerializedValue()); Assert.AreEqual(KeyboardShortcut.Empty, w.Value); var testShortcut = new KeyboardShortcut(KeyCode.A, KeyCode.B, KeyCode.C); w.Value = testShortcut; - w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + w.SetSerializedValue(w.GetSerializedValue()); Assert.AreEqual(testShortcut, w.Value); c.Save(); @@ -283,9 +283,9 @@ namespace BepInEx.Configuration.Tests var w = c.AddSetting("Cat", "Key", testVal, new ConfigDescription("Test")); Assert.AreEqual(testVal, w.Value); - Assert.IsFalse(w.ConfigEntry.GetSerializedValue().Any(x => x == '\n')); + Assert.IsFalse(w.GetSerializedValue().Any(x => x == '\n')); - w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + w.SetSerializedValue(w.GetSerializedValue()); Assert.AreEqual(testVal, w.Value); c.Save(); @@ -309,7 +309,7 @@ namespace BepInEx.Configuration.Tests Assert.AreEqual(unescaped, w.Value); - w.ConfigEntry.SetSerializedValue(w.ConfigEntry.GetSerializedValue()); + w.SetSerializedValue(w.GetSerializedValue()); Assert.AreEqual(unescaped, w.Value); c.Save(); From 8098324fec99840e17960677ca21e33f9b0d4d0d Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Wed, 18 Sep 2019 18:00:05 +0200 Subject: [PATCH 51/54] Change AddSetting calls to new shorter overload --- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 6 +++--- BepInEx.Preloader/Preloader.cs | 12 ++++++------ BepInEx/Bootstrap/Chainloader.cs | 10 +++++----- BepInEx/Bootstrap/TypeLoader.cs | 2 +- BepInEx/ConsoleUtil/ConsoleWindow.cs | 4 ++-- BepInEx/Logging/ConsoleLogListener.cs | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 0e2828e..fb157b5 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -280,17 +280,17 @@ namespace BepInEx.Preloader.Patching private static readonly ConfigEntry 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.")); + "If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers."); private static readonly ConfigEntry 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.")); + "If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies."); private static readonly ConfigEntry 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.")); + "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."); #endregion } diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index 2ccce75..bbb8223 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -240,32 +240,32 @@ namespace BepInEx.Preloader private static readonly ConfigEntry ConfigEntrypointAssembly = ConfigFile.CoreConfig.AddSetting( "Preloader.Entrypoint", "Assembly", IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll", - new ConfigDescription("The local filename of the assembly to target.")); + "The local filename of the assembly to target."); private static readonly ConfigEntry 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.")); + "The name of the type in the entrypoint assembly to search for the entrypoint method."); private static readonly ConfigEntry 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.")); + "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."); private static readonly ConfigEntry 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.")); + "Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing."); private static readonly ConfigEntry 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.")); + "If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game."); private static readonly ConfigEntry ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.AddSetting( "Logging", "PreloaderConsoleOutRedirection", true, - new ConfigDescription("Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.")); + "Redirects text from Console.Out during preloader patch loading to the BepInEx logging system."); #endregion } diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index c12b312..5d10a01 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -337,27 +337,27 @@ namespace BepInEx.Bootstrap private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.AddSetting( "Logging", "UnityLogListening", true, - new ConfigDescription("Enables showing unity log messages in the BepInEx logging system.")); + "Enables showing unity log messages in the BepInEx logging system."); private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "WriteUnityLog", false, - new ConfigDescription("Include unity log messages in log file output.")); + "Include unity log messages in log file output."); private static readonly ConfigEntry ConfigDiskAppend = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "AppendLog", false, - new ConfigDescription("Appends to the log file instead of overwriting, on game startup.")); + "Appends to the log file instead of overwriting, on game startup."); private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "Enabled", true, - new ConfigDescription("Enables writing log messages to disk.")); + "Enables writing log messages to disk."); private static readonly ConfigEntry ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting( "Logging.Disk", "DisplayedLogLevel", LogLevel.Info, - new ConfigDescription("Only displays the specified log level and above in the console output.")); + "Only displays the specified log level and above in the console output."); #endregion } } \ No newline at end of file diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index f8ace67..f80a2d0 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -260,7 +260,7 @@ namespace BepInEx.Bootstrap private static readonly ConfigEntry 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.")); + "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers."); #endregion } diff --git a/BepInEx/ConsoleUtil/ConsoleWindow.cs b/BepInEx/ConsoleUtil/ConsoleWindow.cs index eeb67e0..b4e6fb9 100644 --- a/BepInEx/ConsoleUtil/ConsoleWindow.cs +++ b/BepInEx/ConsoleUtil/ConsoleWindow.cs @@ -16,12 +16,12 @@ namespace UnityInjector.ConsoleUtil public static readonly ConfigEntry ConfigConsoleEnabled = ConfigFile.CoreConfig.AddSetting( "Logging.Console", "Enabled", false, - new ConfigDescription("Enables showing a console for log output.")); + "Enables showing a console for log output."); public static readonly ConfigEntry ConfigConsoleShiftJis = ConfigFile.CoreConfig.AddSetting( "Logging.Console", "ShiftJisEncoding", false, - new ConfigDescription("If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.")); + "If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding."); public static bool IsAttached { get; private set; } private static IntPtr _cOut; diff --git a/BepInEx/Logging/ConsoleLogListener.cs b/BepInEx/Logging/ConsoleLogListener.cs index 1bc9d59..290d9b9 100644 --- a/BepInEx/Logging/ConsoleLogListener.cs +++ b/BepInEx/Logging/ConsoleLogListener.cs @@ -26,6 +26,6 @@ namespace BepInEx.Logging private static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting( "Logging.Console","DisplayedLogLevel", LogLevel.Info, - new ConfigDescription("Only displays the specified log level and above in the console output.")); + "Only displays the specified log level and above in the console output."); } } \ No newline at end of file From a34d0c6528aa6713d67b00b82e6f9a79aa2fab5c Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Wed, 18 Sep 2019 18:04:07 +0200 Subject: [PATCH 52/54] Fix UnescapedPathString test --- BepInExTests/Configuration/ConfigFileTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index e474130..855336e 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -298,6 +298,7 @@ namespace BepInEx.Configuration.Tests public void UnescapedPathString() { var c = MakeConfig(); + var w = c.AddSetting("Cat", "Key", "", new ConfigDescription("Test")); var unescaped = @"D:\test\p ath"; foreach (string testVal in new[] { unescaped, @"D:\\test\\p ath" }) @@ -305,8 +306,6 @@ namespace BepInEx.Configuration.Tests File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); c.Reload(); - var w = c.AddSetting("Cat", "Key", "", new ConfigDescription("Test")); - Assert.AreEqual(unescaped, w.Value); w.SetSerializedValue(w.GetSerializedValue()); From 9c6046d9fbd70190b8334829c6a761fc2d77946a Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Wed, 18 Sep 2019 19:15:42 +0200 Subject: [PATCH 53/54] Added config section and key parameter checks --- BepInEx/Configuration/ConfigDefinition.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/BepInEx/Configuration/ConfigDefinition.cs b/BepInEx/Configuration/ConfigDefinition.cs index 3a0899f..71f48e5 100644 --- a/BepInEx/Configuration/ConfigDefinition.cs +++ b/BepInEx/Configuration/ConfigDefinition.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace BepInEx.Configuration { @@ -26,8 +27,18 @@ namespace BepInEx.Configuration /// Name of the setting, case sensitive. public ConfigDefinition(string section, string key) { - Key = key ?? throw new ArgumentNullException(nameof(key)); - Section = section ?? throw new ArgumentNullException(nameof(section)); + CheckInvalidConfigChars(section, nameof(section)); + CheckInvalidConfigChars(key, nameof(key)); + Key = key; + Section = section; + } + + private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'' }; + private static void CheckInvalidConfigChars(string val, string name) + { + if (val == null) throw new ArgumentNullException(name); + if (val != val.Trim()) throw new ArgumentException("Cannot use whitespace characters at start or end of section and key names", name); + if (val.Any(c => _invalidConfigChars.Contains(c))) throw new ArgumentException(@"Cannot use any of the following characters in section and key names: = \n \t \ "" '", name); } /// From a76d227ac20f01ec53eba7a606bda65102bbd501 Mon Sep 17 00:00:00 2001 From: ManlyMarco Date: Thu, 19 Sep 2019 12:59:37 +0200 Subject: [PATCH 54/54] Fix chainloader error message formatting --- BepInEx/Bootstrap/Chainloader.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index 5d10a01..e8acaf6 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -260,7 +260,7 @@ namespace BepInEx.Bootstrap if (dependsOnInvalidPlugin) { - string message = $"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See above errors for details."; + string message = $"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See previous errors for details."; DependencyErrors.Add(message); Logger.LogWarning(message); continue; @@ -268,14 +268,11 @@ namespace BepInEx.Bootstrap if (missingDependencies.Count != 0) { - string ToMissingString(BepInDependency s) - { - bool emptyVersion = s.MinimumVersion.Major == 0 && s.MinimumVersion.Minor == 0 && s.MinimumVersion.Build == 0 && s.MinimumVersion.Revision == 0; - if (emptyVersion) return "- " + s.DependencyGUID; - return $"- {s.DependencyGUID} (at least v{s.MinimumVersion})"; - } + bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0; - string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: {string.Join(", ", missingDependencies.Select(ToMissingString).ToArray())}"; + string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: { + string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray()) + }"; DependencyErrors.Add(message); Logger.LogError(message); @@ -285,7 +282,9 @@ namespace BepInEx.Bootstrap if (incompatibilities.Count != 0) { - string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {string.Join(", ", incompatibilities.Select(i => i.IncompatibilityGUID).ToArray())}"; + string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: { + string.Join(", ", incompatibilities.Select(i => i.IncompatibilityGUID).ToArray()) + }"; DependencyErrors.Add(message); Logger.LogError(message);