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 '.' /// // 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. /// // 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. /// // 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 pf 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); } } } }