Browse Source

Adds IniConfigProvider

- Adds IniConfigProvider
- Renames some variables for readability in Config.cs
- Changes IConfigProvider Filename to include extension
pull/11/head
MichaelXVo 5 years ago
parent
commit
93b4e9c302
6 changed files with 315 additions and 38 deletions
  1. +25
    -24
      IPA.Loader/Config/Config.cs
  2. +268
    -0
      IPA.Loader/Config/ConfigProviders/IniConfigProvider.cs
  3. +6
    -6
      IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs
  4. +2
    -2
      IPA.Loader/Config/IConfigProvider.cs
  5. +10
    -6
      IPA.Loader/Config/SelfConfig.cs
  6. +4
    -0
      IPA.Loader/IPA.Loader.csproj

+ 25
- 24
IPA.Loader/Config/Config.cs View File

@ -16,6 +16,7 @@ namespace IPA.Config
static Config() static Config()
{ {
JsonConfigProvider.RegisterConfig(); JsonConfigProvider.RegisterConfig();
IniConfigProvider.RegisterConfig();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -115,10 +116,10 @@ namespace IPA.Config
registeredProviders.Add(ext.Extension, type); registeredProviders.Add(ext.Extension, type);
} }
private static List<Tuple<Ref<DateTime>, IConfigProvider>> configProviders = new List<Tuple<Ref<DateTime>, IConfigProvider>>();
private static List<Tuple<Ref<DateTime>, IConfigProvider>> lastModTimeConfigProvider = new List<Tuple<Ref<DateTime>, IConfigProvider>>();
/// <summary> /// <summary>
/// Gets an <see cref="IConfigProvider"/> using the specified list pf preferred config types.
/// Gets an <see cref="IConfigProvider"/> using the specified list of preferred config types.
/// </summary> /// </summary>
/// <param name="configName">the name of the mod for this config</param> /// <param name="configName">the name of the mod for this config</param>
/// <param name="extensions">the preferred config types to try to get</param> /// <param name="extensions">the preferred config types to try to get</param>
@ -130,8 +131,8 @@ namespace IPA.Config
var provider = Activator.CreateInstance(type) as IConfigProvider; var provider = Activator.CreateInstance(type) as IConfigProvider;
if (provider != null) 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; return provider;
@ -148,7 +149,7 @@ namespace IPA.Config
return GetProviderFor(modName, prefs); return GetProviderFor(modName, prefs);
} }
private static Dictionary<IConfigProvider, Action> linkedProviders =
private static Dictionary<IConfigProvider, Action> ProviderChangeDelegate =
new Dictionary<IConfigProvider, Action>(); new Dictionary<IConfigProvider, Action>();
/// <summary> /// <summary>
@ -167,10 +168,10 @@ namespace IPA.Config
onChange?.Invoke(config, @ref); 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 else
linkedProviders.Add(config, ChangeDelegate);
ProviderChangeDelegate.Add(config, ChangeDelegate);
ChangeDelegate(); ChangeDelegate();
@ -183,21 +184,21 @@ namespace IPA.Config
/// <param name="config">the <see cref="IConfigProvider"/> to unlink</param> /// <param name="config">the <see cref="IConfigProvider"/> to unlink</param>
public static void RemoveLinks(this IConfigProvider config) 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() 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 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) catch (Exception e)
{ {
@ -205,12 +206,12 @@ namespace IPA.Config
Logging.Logger.config.Error(e); Logging.Logger.config.Error(e);
} }
} }
if (provider.Item2.HasChanged)
if (timeProviderTuple.Item2.HasChanged)
{ {
try try
{ {
provider.Item2.Save();
provider.Item1.Value = DateTime.Now;
timeProviderTuple.Item2.Save();
timeProviderTuple.Item1.Value = DateTime.Now;
} }
catch (Exception e) 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 try
{ {
if (linkedProviders.ContainsKey(provider.Item2))
linkedProviders[provider.Item2]();
if (ProviderChangeDelegate.ContainsKey(timeProviderTuple.Item2))
ProviderChangeDelegate[timeProviderTuple.Item2]();
} }
catch (Exception e) catch (Exception e)
{ {
@ -238,11 +239,11 @@ namespace IPA.Config
internal static void Save() internal static void Save()
{ {
foreach (var provider in configProviders)
if (provider.Item2.HasChanged)
foreach (var timeProviderTuple in lastModTimeConfigProvider)
if (timeProviderTuple.Item2.HasChanged)
try try
{ {
provider.Item2.Save();
timeProviderTuple.Item2.Save();
} }
catch (Exception e) catch (Exception e)
{ {


+ 268
- 0
IPA.Loader/Config/ConfigProviders/IniConfigProvider.cs View File

@ -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<IniConfigProvider>();
}
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<T>()
{
// Create an instance of the config object to return
T configObj = Activator.CreateInstance<T>();
// 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<string, FieldInfo> classConfigField = new Dictionary<string, FieldInfo>();
// 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>(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];
}
}
}

+ 6
- 6
IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs View File

@ -24,7 +24,7 @@ namespace IPA.Config.ConfigProviders
public bool HasChanged { get; private set; } public bool HasChanged { get; private set; }
public bool InMemoryChanged { get; set; } public bool InMemoryChanged { get; set; }
public DateTime LastModified => File.GetLastWriteTime(Filename + ".json");
public DateTime LastModified => File.GetLastWriteTime(Filename);
private string _filename; private string _filename;
@ -41,9 +41,9 @@ namespace IPA.Config.ConfigProviders
public void Load() 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) if (fileInfo.Exists)
{ {
string json = File.ReadAllText(fileInfo.FullName); string json = File.ReadAllText(fileInfo.FullName);
@ -53,7 +53,7 @@ namespace IPA.Config.ConfigProviders
} }
catch (Exception e) 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); Logger.config.Error(e);
jsonObj = new JObject(); jsonObj = new JObject();
File.WriteAllText(fileInfo.FullName, JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); File.WriteAllText(fileInfo.FullName, JsonConvert.SerializeObject(jsonObj, Formatting.Indented));
@ -102,10 +102,10 @@ namespace IPA.Config.ConfigProviders
public void Save() public void Save()
{ {
Logger.config.Debug($"Saving file {Filename}.json");
Logger.config.Debug($"Saving file {Filename}");
if (!Directory.Exists(Path.GetDirectoryName(Filename))) if (!Directory.Exists(Path.GetDirectoryName(Filename)))
Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException()); 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; HasChanged = false;
} }


+ 2
- 2
IPA.Loader/Config/IConfigProvider.cs View File

@ -36,9 +36,9 @@ namespace IPA.Config
/// </summary> /// </summary>
bool InMemoryChanged { get; set; } bool InMemoryChanged { get; set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
string Filename { set; }
string Filename { set; get; }
/// <summary> /// <summary>
/// Gets the last time the config was modified. /// Gets the last time the config was modified.
/// </summary> /// </summary>


+ 10
- 6
IPA.Loader/Config/SelfConfig.cs View File

@ -7,6 +7,13 @@ namespace IPA.Config
{ {
private static IConfigProvider _loaderConfig; private static IConfigProvider _loaderConfig;
private static void ConfigFileChangeDelegate(IConfigProvider configProvider, Ref<SelfConfig> selfConfigRef)
{
if (selfConfigRef.Value.Regenerate)
configProvider.Store(selfConfigRef.Value = new SelfConfig { Regenerate = false });
StandardLogger.Configure(selfConfigRef.Value);
}
public static IConfigProvider LoaderConfig public static IConfigProvider LoaderConfig
{ {
get => _loaderConfig; get => _loaderConfig;
@ -14,13 +21,10 @@ namespace IPA.Config
{ {
_loaderConfig?.RemoveLinks(); _loaderConfig?.RemoveLinks();
value.Load(); value.Load();
SelfConfigRef = value.MakeLink<SelfConfig>((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<SelfConfig>(ConfigFileChangeDelegate);
_loaderConfig = value; _loaderConfig = value;
} }
} }


+ 4
- 0
IPA.Loader/IPA.Loader.csproj View File

@ -59,6 +59,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Config\Config.cs" /> <Compile Include="Config\Config.cs" />
<Compile Include="Config\ConfigProviders\IniConfigProvider.cs" />
<Compile Include="Config\ConfigProviders\JsonConfigProvider.cs" /> <Compile Include="Config\ConfigProviders\JsonConfigProvider.cs" />
<Compile Include="Config\IConfigProvider.cs" /> <Compile Include="Config\IConfigProvider.cs" />
<Compile Include="Config\SelfConfig.cs" /> <Compile Include="Config\SelfConfig.cs" />
@ -111,6 +112,9 @@
<Compile Include="Utilities\Utils.cs" /> <Compile Include="Utilities\Utils.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ini-parser">
<Version>2.5.2</Version>
</PackageReference>
<PackageReference Include="Ionic.Zip"> <PackageReference Include="Ionic.Zip">
<Version>1.9.1.8</Version> <Version>1.9.1.8</Version>
</PackageReference> </PackageReference>


Loading…
Cancel
Save