using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using IPA.Config.ConfigProviders;
using IPA.Utilities;
namespace IPA.Config
{
///
/// A class to handle updating ConfigProviders automatically
///
public static class Config
{
static Config()
{
JsonConfigProvider.RegisterConfig();
}
///
///
/// Defines the type of the
///
[AttributeUsage(AttributeTargets.Class)]
public class TypeAttribute : Attribute
{
///
/// The extension associated with this type, without the '.'
///
/// the extension to register the config provider as
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string Extension { get; private set; }
///
///
/// Constructs the attribute with a specified extension.
///
/// the extension associated with this type, without the '.'
public TypeAttribute(string ext)
{
Extension = ext;
}
}
///
///
/// Specifies that a particular parameter is preferred to be a specific type of . If it is not available, also specifies backups. If none are available, the default is used.
///
[AttributeUsage(AttributeTargets.Parameter)]
public class PreferAttribute : Attribute
{
///
/// The order of preference for the config type.
///
/// the list of config extensions in order of preference
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string[] PreferenceOrder { get; private set; }
///
///
/// Constructs the attribute with a specific preference list. Each entry is the extension without a '.'
///
/// The preferences in order of preference.
public PreferAttribute(params string[] preference)
{
PreferenceOrder = preference;
}
}
///
///
/// Specifies a preferred config name, instead of using the plugin's name.
///
public class NameAttribute : Attribute
{
///
/// The name to use for the config.
///
/// the name to use for the config
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string Name { get; private set; }
///
///
/// Constructs the attribute with a specific name.
///
/// the name to use for the config.
public NameAttribute(string name)
{
Name = name;
}
}
private static readonly Dictionary registeredProviders = new Dictionary();
///
/// Registers a to use for configs.
///
/// the type to register
public static void Register() where T : IConfigProvider => Register(typeof(T));
///
/// Registers a to use for configs.
///
/// the type to register
public static void Register(Type type)
{
if (!(type.GetCustomAttribute(typeof(TypeAttribute)) is TypeAttribute ext))
throw new InvalidOperationException("Type does not have TypeAttribute");
if (!typeof(IConfigProvider).IsAssignableFrom(type))
throw new InvalidOperationException("Type not IConfigProvider");
if (registeredProviders.ContainsKey(ext.Extension))
throw new InvalidOperationException($"Extension provider for {ext.Extension} already exists");
registeredProviders.Add(ext.Extension, type);
}
private static List, IConfigProvider>> configProviders = new List, IConfigProvider>>();
///
/// 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
/// an of the requested type, or of type JSON.
public static IConfigProvider GetProviderFor(string configName, params string[] extensions)
{
var chosenExt = extensions.FirstOrDefault(s => registeredProviders.ContainsKey(s)) ?? "json";
var type = registeredProviders[chosenExt];
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));
}
return provider;
}
internal static IConfigProvider GetProviderFor(string modName, ParameterInfo info)
{
var prefs = new string[0];
if (info.GetCustomAttribute() is PreferAttribute prefer)
prefs = prefer.PreferenceOrder;
if (info.GetCustomAttribute() is NameAttribute name)
modName = name.Name;
return GetProviderFor(modName, prefs);
}
private static Dictionary linkedProviders =
new Dictionary();
///
/// Creates a linked for the config provider. This will be automatically updated whenever the file on-disk changes.
///
/// the type of the parsed value
/// the to create a link to
/// an action to perform on value change
/// a to an ever-changing value, mirroring whatever the file contains.
public static Ref MakeLink(this IConfigProvider config, Action> onChange = null)
{
Ref @ref = config.Parse();
void ChangeDelegate()
{
@ref.Value = config.Parse();
onChange?.Invoke(config, @ref);
}
if (linkedProviders.ContainsKey(config))
linkedProviders[config] = (Action) Delegate.Combine(linkedProviders[config], (Action) ChangeDelegate);
else
linkedProviders.Add(config, ChangeDelegate);
ChangeDelegate();
return @ref;
}
///
/// Removes all linked such that they are no longer updated.
///
/// the to unlink
public static void RemoveLinks(this IConfigProvider config)
{
if (linkedProviders.ContainsKey(config))
linkedProviders.Remove(config);
}
internal static void Update()
{
foreach (var provider in configProviders)
{
if (provider.Item2.LastModified > provider.Item1.Value)
{
try
{
provider.Item2.Load(); // auto reload if it changes
provider.Item1.Value = provider.Item2.LastModified;
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to load config");
Logging.Logger.config.Error(e);
}
}
if (provider.Item2.HasChanged)
{
try
{
provider.Item2.Save();
provider.Item1.Value = DateTime.Now;
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to save config");
Logging.Logger.config.Error(e);
}
}
if (provider.Item2.InMemoryChanged)
{
provider.Item2.InMemoryChanged = false;
try
{
if (linkedProviders.ContainsKey(provider.Item2))
linkedProviders[provider.Item2]();
}
catch (Exception e)
{
Logging.Logger.config.Error("Error running link change events");
Logging.Logger.config.Error(e);
}
}
}
}
internal static void Save()
{
foreach (var provider in configProviders)
if (provider.Item2.HasChanged)
try
{
provider.Item2.Save();
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to save config");
Logging.Logger.config.Error(e);
}
}
}
}