From e90304073c487fa8d2fcd1fb56b3caac4ce7e23f Mon Sep 17 00:00:00 2001 From: ghorsington Date: Sun, 4 Jul 2021 15:16:42 +0300 Subject: [PATCH] Start work on config provider type conversion --- .../Configuration/ConfigDefinition.cs | 10 +-- BepInEx.Core/Configuration/ConfigEntryBase.cs | 9 ++- BepInEx.Core/Configuration/ConfigFile.cs | 73 ++++++++++++++++--- ...ypeConverter.cs => ConfigTypeConverter.cs} | 46 +++++++----- .../Configuration/KeyboardShortcut.cs | 2 +- BepInEx.Unity/UnityTomlTypeConverters.cs | 10 +-- 6 files changed, 107 insertions(+), 43 deletions(-) rename BepInEx.Core/Configuration/{TomlTypeConverter.cs => ConfigTypeConverter.cs} (86%) diff --git a/BepInEx.Core/Configuration/ConfigDefinition.cs b/BepInEx.Core/Configuration/ConfigDefinition.cs index b646667..e16b6ff 100644 --- a/BepInEx.Core/Configuration/ConfigDefinition.cs +++ b/BepInEx.Core/Configuration/ConfigDefinition.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using HarmonyLib; namespace BepInEx.Configuration { @@ -11,7 +12,7 @@ namespace BepInEx.Configuration /// public class ConfigDefinition : IEquatable { - private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']' }; + private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']', '.' }; /// /// Create a new definition. Definitions with same section and key are equal. @@ -35,6 +36,8 @@ namespace BepInEx.Configuration /// Name of the setting. /// public string Key { get; } + + internal string[] ConfigPath => Section.Split('.').AddItem(Key).ToArray(); /// /// Check if the definitions are the same. @@ -50,12 +53,9 @@ namespace BepInEx.Configuration 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 \ "" ' [ ]", + ArgumentException(@"Cannot use any of the following characters in section and key names: = \n \t \ "" ' [ ] .", name); } diff --git a/BepInEx.Core/Configuration/ConfigEntryBase.cs b/BepInEx.Core/Configuration/ConfigEntryBase.cs index a5b822f..d12744e 100644 --- a/BepInEx.Core/Configuration/ConfigEntryBase.cs +++ b/BepInEx.Core/Configuration/ConfigEntryBase.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using BepInEx.Logging; +using HarmonyLib; namespace BepInEx.Configuration { @@ -120,7 +123,7 @@ namespace BepInEx.Configuration /// /// Get the serialized representation of the value. /// - public string GetSerializedValue() => TomlTypeConverter.ConvertToString(BoxedValue, SettingType); + public string GetSerializedValue() => ConfigTypeConverter.ConvertToString(BoxedValue, SettingType); /// /// Set the value by using its serialized form. @@ -129,7 +132,7 @@ namespace BepInEx.Configuration { try { - var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); + var newValue = ConfigTypeConverter.ConvertToValue(value, SettingType); BoxedValue = newValue; } catch (Exception e) @@ -164,7 +167,7 @@ namespace BepInEx.Configuration writer.WriteLine("# Setting type: " + SettingType.Name); - writer.WriteLine("# Default value: " + TomlTypeConverter.ConvertToString(DefaultValue, SettingType)); + writer.WriteLine("# Default value: " + ConfigTypeConverter.ConvertToString(DefaultValue, SettingType)); if (Description.AcceptableValues != null) { diff --git a/BepInEx.Core/Configuration/ConfigFile.cs b/BepInEx.Core/Configuration/ConfigFile.cs index d83d03f..e29e6e9 100644 --- a/BepInEx.Core/Configuration/ConfigFile.cs +++ b/BepInEx.Core/Configuration/ConfigFile.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using BepInEx.Logging; +using HarmonyLib; // TODO: Check if IDictionary implementation is reasonable @@ -37,7 +39,7 @@ namespace BepInEx.Configuration public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true); - public IConfigurationProvider ConfigurationProvider { get; set; } + public IConfigurationProvider ConfigurationProvider { get; } /// /// All config entries inside @@ -313,24 +315,15 @@ namespace BepInEx.Configuration 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.TryGetValue(configDefinition, out var rawEntry)) return (ConfigEntry) rawEntry; var entry = new ConfigEntry(this, configDefinition, defaultValue, configDescription); - Entries[configDefinition] = entry; - if (OrphanedEntries.TryGetValue(configDefinition, out var homelessValue)) - { - entry.SetSerializedValue(homelessValue); - OrphanedEntries.Remove(configDefinition); - } + entry.BoxedValue = GetValue(configDefinition.ConfigPath, typeof(T)) ?? defaultValue; if (SaveOnConfigSet) Save(); @@ -338,6 +331,64 @@ namespace BepInEx.Configuration return entry; } } + + private object GetValue(string[] path, Type type) + { + // We have direct value; simply resolve it normally + if (ConfigTypeConverter.CanConvertDirectly(type)) + { + var val = ConfigurationProvider.Get(path); + if (val != null) + return ConfigTypeConverter.ConvertToValue(val.Value, type); + return null; + } + else if (type.IsArray) + { + var elType = type.GetElementType(); + var indexPath = path.AddItem("0").ToArray(); + var values = new List(); + for (var i = 0;; i++) + { + indexPath[indexPath.Length - 1] = i.ToString(CultureInfo.InvariantCulture); + var val = GetValue(indexPath, elType); + if (val == null) + break; + values.Add(val); + } + var arr = Array.CreateInstance(elType, values.Count); + for (var i = 0; i < values.Count; i++) + arr.SetValue(values[i], i); + return arr; + } + else if (typeof(IList<>).IsAssignableFrom(type)) + { + var res = Activator.CreateInstance(type); + var elType = type.GetGenericArguments()[0]; + var add = MethodInvoker.GetHandler(AccessTools.Method(type, "Add", new[] { elType })); + var indexPath = path.AddItem("0").ToArray(); + for (var i = 0;; i++) + { + indexPath[indexPath.Length - 1] = i.ToString(CultureInfo.InvariantCulture); + var val = GetValue(indexPath, elType); + if (val == null) + break; + add(val); + } + + return res; + } else if (typeof(IDictionary<,>).IsAssignableFrom(type)) + { + var res = Activator.CreateInstance(type); + var gArgs = type.GetGenericArguments(); + if (gArgs[0] != typeof(string)) + throw new Exception("Only dictionaries with string keys are supported"); + var elType = gArgs[1]; + var add = MethodInvoker.GetHandler(AccessTools.Method(type, "Add", new[] { typeof(string), elType })); + + } + + return null; + } /// /// Create a new setting. The setting is saved to drive and loaded automatically. diff --git a/BepInEx.Core/Configuration/TomlTypeConverter.cs b/BepInEx.Core/Configuration/ConfigTypeConverter.cs similarity index 86% rename from BepInEx.Core/Configuration/TomlTypeConverter.cs rename to BepInEx.Core/Configuration/ConfigTypeConverter.cs index eaf8315..798d331 100644 --- a/BepInEx.Core/Configuration/TomlTypeConverter.cs +++ b/BepInEx.Core/Configuration/ConfigTypeConverter.cs @@ -10,21 +10,21 @@ namespace BepInEx.Configuration /// /// Serializer/deserializer used by the config system. /// - public static class TomlTypeConverter + public static class ConfigTypeConverter { // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead - private static Dictionary TypeConverters { get; } = new() + private static Dictionary DirectConverters { get; } = new() { [typeof(string)] = new TypeConverter { - 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 Unescape(str); - } + ConvertToString = (obj, type) => obj.ToString(), //Escape((string) obj), + ConvertToObject = (str, type) => str, + // { + // // 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 Unescape(str); + // } }, [typeof(bool)] = new TypeConverter { @@ -98,15 +98,25 @@ namespace BepInEx.Configuration ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo) }, - //enums are special + // Special direct converters [typeof(Enum)] = new TypeConverter { ConvertToString = (obj, type) => obj.ToString(), ConvertToObject = (str, type) => Enum.Parse(type, str, true) + }, + [typeof(DateTime)] = new TypeConverter + { + ConvertToString = (obj, type) => ((DateTime) obj).ToString("O"), + ConvertToObject = (str, type) => DateTime.ParseExact(str, "O", CultureInfo.InvariantCulture) + }, + [typeof(DateTimeOffset)] = new TypeConverter + { + ConvertToString = (obj, type) => ((DateTimeOffset) obj).ToString("O"), + ConvertToObject = (str, type) => DateTimeOffset.ParseExact(str, "O", CultureInfo.InvariantCulture) } }; - + /// /// Convert object of a given type to a string using available converters. /// @@ -145,9 +155,9 @@ namespace BepInEx.Configuration throw new ArgumentNullException(nameof(valueType)); if (valueType.IsEnum) - return TypeConverters[typeof(Enum)]; + return DirectConverters[typeof(Enum)]; - TypeConverters.TryGetValue(valueType, out var result); + DirectConverters.TryGetValue(valueType, out var result); return result; } @@ -160,25 +170,25 @@ namespace BepInEx.Configuration { if (type == null) throw new ArgumentNullException(nameof(type)); if (converter == null) throw new ArgumentNullException(nameof(converter)); - if (CanConvert(type)) + if (CanConvertDirectly(type)) { Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName); return false; } - TypeConverters.Add(type, converter); + DirectConverters.Add(type, converter); return true; } /// /// Check if a given type can be converted to and from string. /// - public static bool CanConvert(Type type) => GetConverter(type) != null; + public static bool CanConvertDirectly(Type type) => GetConverter(type) != null; /// /// Give a list of types with registered converters. /// - public static IEnumerable GetSupportedTypes() => TypeConverters.Keys; + public static IEnumerable GetSupportedTypes() => DirectConverters.Keys; private static string Escape(this string txt) { diff --git a/BepInEx.Unity/Configuration/KeyboardShortcut.cs b/BepInEx.Unity/Configuration/KeyboardShortcut.cs index 596b043..23d847c 100644 --- a/BepInEx.Unity/Configuration/KeyboardShortcut.cs +++ b/BepInEx.Unity/Configuration/KeyboardShortcut.cs @@ -20,7 +20,7 @@ namespace BepInEx.Configuration { static KeyboardShortcut() { - TomlTypeConverter.AddConverter( + ConfigTypeConverter.AddConverter( typeof(KeyboardShortcut), new TypeConverter { diff --git a/BepInEx.Unity/UnityTomlTypeConverters.cs b/BepInEx.Unity/UnityTomlTypeConverters.cs index 9d16ad7..f389eb7 100644 --- a/BepInEx.Unity/UnityTomlTypeConverters.cs +++ b/BepInEx.Unity/UnityTomlTypeConverters.cs @@ -24,7 +24,7 @@ namespace BepInEx.Unity } }; - TomlTypeConverter.AddConverter(typeof(Color), colorConverter); + ConfigTypeConverter.AddConverter(typeof(Color), colorConverter); var jsonConverter = new TypeConverter { @@ -32,10 +32,10 @@ namespace BepInEx.Unity 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); + ConfigTypeConverter.AddConverter(typeof(Vector2), jsonConverter); + ConfigTypeConverter.AddConverter(typeof(Vector3), jsonConverter); + ConfigTypeConverter.AddConverter(typeof(Vector4), jsonConverter); + ConfigTypeConverter.AddConverter(typeof(Quaternion), jsonConverter); } } }