diff --git a/IPA.Loader/Config/Config.cs b/IPA.Loader/Config/Config.cs index 3cb99a40..13f08da9 100644 --- a/IPA.Loader/Config/Config.cs +++ b/IPA.Loader/Config/Config.cs @@ -16,6 +16,7 @@ namespace IPA.Config static Config() { JsonConfigProvider.RegisterConfig(); + IniConfigProvider.RegisterConfig(); } /// @@ -115,10 +116,10 @@ namespace IPA.Config registeredProviders.Add(ext.Extension, type); } - private static List, IConfigProvider>> configProviders = new List, IConfigProvider>>(); + private static List, IConfigProvider>> lastModTimeConfigProvider = new List, IConfigProvider>>(); /// - /// Gets an using the specified list pf preferred config types. + /// Gets an using the specified list of preferred config types. /// /// the name of the mod for this config /// the preferred config types to try to get @@ -130,8 +131,8 @@ namespace IPA.Config var provider = Activator.CreateInstance(type) as IConfigProvider; if (provider != null) { - provider.Filename = Path.Combine(BeatSaber.UserDataPath, configName); - configProviders.Add(Tuple.Create(Ref.Create(provider.LastModified), provider)); + provider.Filename = Path.Combine(BeatSaber.UserDataPath, (configName + "." + chosenExt)); + lastModTimeConfigProvider.Add(Tuple.Create(Ref.Create(provider.LastModified), provider)); } return provider; @@ -148,7 +149,7 @@ namespace IPA.Config return GetProviderFor(modName, prefs); } - private static Dictionary linkedProviders = + private static Dictionary ProviderChangeDelegate = new Dictionary(); /// @@ -167,10 +168,10 @@ namespace IPA.Config onChange?.Invoke(config, @ref); } - if (linkedProviders.ContainsKey(config)) - linkedProviders[config] = (Action) Delegate.Combine(linkedProviders[config], (Action) ChangeDelegate); + if (ProviderChangeDelegate.ContainsKey(config)) + ProviderChangeDelegate[config] = (Action) Delegate.Combine(ProviderChangeDelegate[config], (Action) ChangeDelegate); else - linkedProviders.Add(config, ChangeDelegate); + ProviderChangeDelegate.Add(config, ChangeDelegate); ChangeDelegate(); @@ -183,21 +184,21 @@ namespace IPA.Config /// the to unlink public static void RemoveLinks(this IConfigProvider config) { - if (linkedProviders.ContainsKey(config)) - linkedProviders.Remove(config); + if (ProviderChangeDelegate.ContainsKey(config)) + ProviderChangeDelegate.Remove(config); } internal static void Update() { - foreach (var provider in configProviders) + foreach (var timeProviderTuple in lastModTimeConfigProvider) { - if (provider.Item2.LastModified > provider.Item1.Value) + if (timeProviderTuple.Item2.LastModified > timeProviderTuple.Item1.Value) { try { - provider.Item2.Load(); // auto reload if it changes - provider.Item1.Value = provider.Item2.LastModified; + timeProviderTuple.Item2.Load(); // auto reload if it changes + timeProviderTuple.Item1.Value = timeProviderTuple.Item2.LastModified; } catch (Exception e) { @@ -205,12 +206,12 @@ namespace IPA.Config Logging.Logger.config.Error(e); } } - if (provider.Item2.HasChanged) + if (timeProviderTuple.Item2.HasChanged) { try { - provider.Item2.Save(); - provider.Item1.Value = DateTime.Now; + timeProviderTuple.Item2.Save(); + timeProviderTuple.Item1.Value = DateTime.Now; } catch (Exception e) { @@ -219,13 +220,13 @@ namespace IPA.Config } } - if (provider.Item2.InMemoryChanged) + if (timeProviderTuple.Item2.InMemoryChanged) { - provider.Item2.InMemoryChanged = false; + timeProviderTuple.Item2.InMemoryChanged = false; try { - if (linkedProviders.ContainsKey(provider.Item2)) - linkedProviders[provider.Item2](); + if (ProviderChangeDelegate.ContainsKey(timeProviderTuple.Item2)) + ProviderChangeDelegate[timeProviderTuple.Item2](); } catch (Exception e) { @@ -238,11 +239,11 @@ namespace IPA.Config internal static void Save() { - foreach (var provider in configProviders) - if (provider.Item2.HasChanged) + foreach (var timeProviderTuple in lastModTimeConfigProvider) + if (timeProviderTuple.Item2.HasChanged) try { - provider.Item2.Save(); + timeProviderTuple.Item2.Save(); } catch (Exception e) { diff --git a/IPA.Loader/Config/ConfigProviders/IniConfigProvider.cs b/IPA.Loader/Config/ConfigProviders/IniConfigProvider.cs new file mode 100644 index 00000000..6a18db0d --- /dev/null +++ b/IPA.Loader/Config/ConfigProviders/IniConfigProvider.cs @@ -0,0 +1,268 @@ +using IPA.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using IniParser; +using IniParser.Model; +using System.Reflection; + +namespace IPA.Config.ConfigProviders +{ + [Config.Type("ini")] + internal class IniConfigProvider : IConfigProvider + { + public static void RegisterConfig() + { + Config.Register(); + } + + private IniData _iniData; + + // TODO: create a wrapper that allows empty object creation + public dynamic Dynamic => _iniData; + + + public bool HasChanged { get; private set; } + public bool InMemoryChanged { get; set; } + + public DateTime LastModified => File.GetLastWriteTime(Filename); + + private string _filename; + + public string Filename + { + get => _filename; + set + { + if (_filename != null) + throw new InvalidOperationException("Can only assign to Filename once"); + _filename = value; + } + } + + // Load file + public void Load() + { + Logger.config.Debug($"Loading file {Filename}"); + + var fileInfo = new FileInfo(Filename); + if (fileInfo.Exists) + { + try + { + var parser = new FileIniDataParser(); + parser.Parser.Configuration.CaseInsensitive = true; + + _iniData = parser.ReadFile(fileInfo.FullName); + } + catch (Exception e) + { + Logger.config.Error($"Error parsing INI in file {Filename}; resetting to empty INI"); + Logger.config.Error(e); + + _iniData = new IniData(); + } + } + else + { + Logger.config.Debug($"File {fileInfo.FullName} doesn't exist"); + _iniData = new IniData(); + } + + InMemoryChanged = true; + } + + + // This is basically trying to deserialize from INI data to a config object + public T Parse() + { + // Create an instance of the config object to return + T configObj = Activator.CreateInstance(); + + // Get a list of the fields declared in the config object + Type configObjType = typeof(T); + + // Create a dictionary to record which fields are found in the class files + Dictionary classConfigField = new Dictionary(); + + // This goes through each field of the class to set values + // if found in the configuration file + foreach (FieldInfo field in configObjType.GetFields()) + { + Type fieldType = field.FieldType; + + // If thie field is an object, loop through its fields ("subfields") + if (Type.GetTypeCode(fieldType) == TypeCode.Object) + { + + // Get the sub object value from the config object + object configObjSubObj = field.GetValue(configObj); + + foreach (FieldInfo subField in fieldType.GetFields()) + { + + // If the INI file has a section/key pair corresponding to the field/subfield, + // set the subfield value and store the field info in dictionary + if (_iniData.Sections.ContainsSection(field.Name) && _iniData[field.Name].ContainsKey(subField.Name)) + { + SetFieldValue(subField, configObjSubObj, _iniData[field.Name][subField.Name]); + string fieldName = field.Name + "." + subField.Name; + classConfigField[fieldName.ToUpper()] = subField; + } + else + { + Logger.config.Debug($"{field.Name}.{subField.Name} doesn't have a configuration value! Keeping existing value {subField.GetValue(configObjSubObj)}"); + } + } + } + else + { + // If a field in the configuration object isn't itself an object, then it's a primitive type + // declared in the global section of the INI file + if (_iniData.Global.ContainsKey(field.Name)) + { + SetFieldValue(field, configObj, _iniData.Global[field.Name]); + } + else + { + Logger.config.Debug($"{field.Name} doesn't have a configuration value! Keeping existing value {field.GetValue(configObj)}"); + } + string fieldName = field.Name; + + // store field info in dictionary (case insensitive) + classConfigField[fieldName.ToUpper()] = field; + } + } + + // Loop through the global section of the INI file and see if any of those keys + // don't correspond to a field in the object class by using dictionary + + // If any of them don't correspond to a field in the object class, add a comment to INI file + // mentioning those keys are being ignored + foreach (KeyData globalKey in _iniData.Global) + { + string fieldName = globalKey.KeyName; + if (!classConfigField.ContainsKey(fieldName.ToUpper())) + { + string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED!" + configObj.GetType() + " does not have a field corresponding to " + globalKey.KeyName; + if(!globalKey.Comments.Contains(missingClassFieldComment)) + globalKey.Comments.Add(missingClassFieldComment); + Logger.config.Debug($"{configObj.GetType()} does not have global section key {globalKey.KeyName}"); + } + } + + // Similarly, loop through the other section/key pairings of the INI file and check as well. + foreach (SectionData section in _iniData.Sections) + { + foreach (KeyData key in section.Keys) + { + string fieldName = section.SectionName + "." + key.KeyName; + if (!classConfigField.ContainsKey(fieldName.ToUpper())) + { + string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED! " + configObj.GetType() + " does not have a member corresponding to " + fieldName; + if (!key.Comments.Contains(missingClassFieldComment)) + key.Comments.Add(missingClassFieldComment); + Logger.config.Debug($"{configObj.GetType()} not have {section.SectionName} section key {key.KeyName}"); + } + } + } + + return configObj; + } + internal static void SetFieldValue(FieldInfo fieldInfo, object obj, string str) + { + if (str == null) + { + Logger.config.Debug($"{fieldInfo.Name} doesn't have a configuration value! Keeping existing value {fieldInfo.GetValue(obj)}"); + return; + } + + switch (Type.GetTypeCode(fieldInfo.FieldType)) + { + case TypeCode.String: + fieldInfo.SetValue(obj, str); + break; + case TypeCode.Boolean: + fieldInfo.SetValue(obj, Boolean.Parse(str)); + break; + case TypeCode.DateTime: + fieldInfo.SetValue(obj, DateTime.Parse(str)); + break; + case TypeCode.Int16: + fieldInfo.SetValue(obj, Int16.Parse(str)); + break; + case TypeCode.Int32: + fieldInfo.SetValue(obj, Int32.Parse(str)); + break; + case TypeCode.Int64: + fieldInfo.SetValue(obj, Int64.Parse(str)); + break; + case TypeCode.Double: + fieldInfo.SetValue(obj, Double.Parse(str)); + break; + default: + Logger.config.Debug($"{fieldInfo.FieldType} not supported"); + throw new Exception(); + } + } + + public void Save() + { + Logger.config.Debug($"Saving file {Filename}"); + if (!Directory.Exists(Path.GetDirectoryName(Filename))) + Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException()); + + var parser = new FileIniDataParser(); + parser.WriteFile(Filename, _iniData); + + HasChanged = false; + } + + + // This is basically serializing from an object to INI Data + public void Store(T obj) + { + Type configObjType = typeof(T); + + // Loop through each field in the config object and set the corresponding + // value in the INI Data object. + + // Note if there isn't a corresponding value defined in the INI data object, + // it will add one implicitly by accessing it with the brackets + foreach (FieldInfo field in configObjType.GetFields()) + { + Type fieldType = field.FieldType; + + // if the field is not a primitive type, loop through its subfields + if (Type.GetTypeCode(fieldType) == TypeCode.Object) + { + FieldInfo[] subFields = fieldType.GetFields(); + foreach (FieldInfo subField in subFields) + { + if (Type.GetTypeCode(subField.FieldType) != TypeCode.Object) + _iniData[field.Name][subField.Name] = subField.GetValue(field.GetValue(obj)).ToString(); + } + } + else + { + _iniData.Global[field.Name] = field.GetValue(obj).ToString(); + } + } + + HasChanged = true; + InMemoryChanged = true; + } + public string ReadValue(string section, string key) + { + return _iniData[section][key]; + } + public string ReadValue(string key) + { + return _iniData.Global[key]; + } + } +} \ No newline at end of file diff --git a/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs b/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs index be81baea..7c038945 100644 --- a/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs +++ b/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs @@ -24,7 +24,7 @@ namespace IPA.Config.ConfigProviders public bool HasChanged { get; private set; } public bool InMemoryChanged { get; set; } - public DateTime LastModified => File.GetLastWriteTime(Filename + ".json"); + public DateTime LastModified => File.GetLastWriteTime(Filename); private string _filename; @@ -41,9 +41,9 @@ namespace IPA.Config.ConfigProviders public void Load() { - Logger.config.Debug($"Loading file {Filename}.json"); + Logger.config.Debug($"Loading file {Filename}"); - var fileInfo = new FileInfo(Filename + ".json"); + var fileInfo = new FileInfo(Filename); if (fileInfo.Exists) { string json = File.ReadAllText(fileInfo.FullName); @@ -53,7 +53,7 @@ namespace IPA.Config.ConfigProviders } catch (Exception e) { - Logger.config.Error($"Error parsing JSON in file {Filename}.json; resetting to empty JSON"); + Logger.config.Error($"Error parsing JSON in file {Filename}; resetting to empty JSON"); Logger.config.Error(e); jsonObj = new JObject(); File.WriteAllText(fileInfo.FullName, JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); @@ -102,10 +102,10 @@ namespace IPA.Config.ConfigProviders public void Save() { - Logger.config.Debug($"Saving file {Filename}.json"); + Logger.config.Debug($"Saving file {Filename}"); if (!Directory.Exists(Path.GetDirectoryName(Filename))) Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException()); - File.WriteAllText(Filename + ".json", JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); + File.WriteAllText(Filename, JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); HasChanged = false; } diff --git a/IPA.Loader/Config/IConfigProvider.cs b/IPA.Loader/Config/IConfigProvider.cs index 88f23efe..f8cf3c5a 100644 --- a/IPA.Loader/Config/IConfigProvider.cs +++ b/IPA.Loader/Config/IConfigProvider.cs @@ -36,9 +36,9 @@ namespace IPA.Config /// bool InMemoryChanged { get; set; } /// - /// Will be set with the filename (no extension) to save to. When saving, the implementation should add the appropriate extension. Should error if set multiple times. + /// Will be set with the filename to save to. Should error if set multiple times. /// - string Filename { set; } + string Filename { set; get; } /// /// Gets the last time the config was modified. /// diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index 24575ab7..621670d8 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -7,6 +7,13 @@ namespace IPA.Config { private static IConfigProvider _loaderConfig; + private static void ConfigFileChangeDelegate(IConfigProvider configProvider, Ref selfConfigRef) + { + if (selfConfigRef.Value.Regenerate) + configProvider.Store(selfConfigRef.Value = new SelfConfig { Regenerate = false }); + + StandardLogger.Configure(selfConfigRef.Value); + } public static IConfigProvider LoaderConfig { get => _loaderConfig; @@ -14,13 +21,10 @@ namespace IPA.Config { _loaderConfig?.RemoveLinks(); value.Load(); - SelfConfigRef = value.MakeLink((c, v) => - { - if (v.Value.Regenerate) - c.Store(v.Value = new SelfConfig { Regenerate = false }); - StandardLogger.Configure(v.Value); - }); + // This will set the instance reference to update to a + // new instance every time the config file changes + SelfConfigRef = value.MakeLink(ConfigFileChangeDelegate); _loaderConfig = value; } } diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 7f3fd570..db04c493 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -59,6 +59,7 @@ + @@ -111,6 +112,9 @@ + + 2.5.2 + 1.9.1.8