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];
|
|
}
|
|
}
|
|
}
|