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