You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
9.9 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using IPA.Config.ConfigProviders;
  7. using IPA.Utilities;
  8. namespace IPA.Config
  9. {
  10. /// <summary>
  11. /// A class to handle updating ConfigProviders automatically
  12. /// </summary>
  13. public static class Config
  14. {
  15. static Config()
  16. {
  17. JsonConfigProvider.RegisterConfig();
  18. }
  19. /// <inheritdoc />
  20. /// <summary>
  21. /// Defines the type of the <see cref="T:IPA.Config.IConfigProvider" />
  22. /// </summary>
  23. [AttributeUsage(AttributeTargets.Class)]
  24. public class TypeAttribute : Attribute
  25. {
  26. /// <summary>
  27. /// The extension associated with this type, without the '.'
  28. /// </summary>
  29. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  30. public string Extension { get; private set; }
  31. /// <inheritdoc />
  32. /// <summary>
  33. /// Constructs the attribute with a specified extension.
  34. /// </summary>
  35. /// <param name="ext">the extension associated with this type, without the '.'</param>
  36. public TypeAttribute(string ext)
  37. {
  38. Extension = ext;
  39. }
  40. }
  41. /// <inheritdoc />
  42. /// <summary>
  43. /// Specifies that a particular parameter is preferred to be a specific type of <see cref="T:IPA.Config.IConfigProvider" />. If it is not available, also specifies backups. If none are available, the default is used.
  44. /// </summary>
  45. [AttributeUsage(AttributeTargets.Parameter)]
  46. public class PreferAttribute : Attribute
  47. {
  48. /// <summary>
  49. /// The order of preference for the config type.
  50. /// </summary>
  51. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  52. public string[] PreferenceOrder { get; private set; }
  53. /// <inheritdoc />
  54. /// <summary>
  55. /// Constructs the attribute with a specific preference list. Each entry is the extension without a '.'
  56. /// </summary>
  57. /// <param name="preference">The preferences in order of preference.</param>
  58. public PreferAttribute(params string[] preference)
  59. {
  60. PreferenceOrder = preference;
  61. }
  62. }
  63. /// <inheritdoc />
  64. /// <summary>
  65. /// Specifies a preferred config name, instead of using the plugin's name.
  66. /// </summary>
  67. public class NameAttribute : Attribute
  68. {
  69. /// <summary>
  70. /// The name to use for the config.
  71. /// </summary>
  72. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  73. public string Name { get; private set; }
  74. /// <inheritdoc />
  75. /// <summary>
  76. /// Constructs the attribute with a specific name.
  77. /// </summary>
  78. /// <param name="name">the name to use for the config.</param>
  79. public NameAttribute(string name)
  80. {
  81. Name = name;
  82. }
  83. }
  84. private static readonly Dictionary<string, Type> registeredProviders = new Dictionary<string, Type>();
  85. /// <summary>
  86. /// Registers a <see cref="IConfigProvider"/> to use for configs.
  87. /// </summary>
  88. /// <typeparam name="T">the type to register</typeparam>
  89. public static void Register<T>() where T : IConfigProvider => Register(typeof(T));
  90. /// <summary>
  91. /// Registers a <see cref="IConfigProvider"/> to use for configs.
  92. /// </summary>
  93. /// <param name="type">the type to register</param>
  94. public static void Register(Type type)
  95. {
  96. if (!(type.GetCustomAttribute(typeof(TypeAttribute)) is TypeAttribute ext))
  97. throw new InvalidOperationException("Type does not have TypeAttribute");
  98. if (!typeof(IConfigProvider).IsAssignableFrom(type))
  99. throw new InvalidOperationException("Type not IConfigProvider");
  100. if (registeredProviders.ContainsKey(ext.Extension))
  101. throw new InvalidOperationException($"Extension provider for {ext.Extension} already exists");
  102. registeredProviders.Add(ext.Extension, type);
  103. }
  104. private static List<Tuple<Ref<DateTime>, IConfigProvider>> configProviders = new List<Tuple<Ref<DateTime>, IConfigProvider>>();
  105. /// <summary>
  106. /// Gets an <see cref="IConfigProvider"/> using the specified list pf preferred config types.
  107. /// </summary>
  108. /// <param name="configName">the name of the mod for this config</param>
  109. /// <param name="extensions">the preferred config types to try to get</param>
  110. /// <returns>an <see cref="IConfigProvider"/> of the requested type, or of type JSON.</returns>
  111. public static IConfigProvider GetProviderFor(string configName, params string[] extensions)
  112. {
  113. var chosenExt = extensions.FirstOrDefault(s => registeredProviders.ContainsKey(s)) ?? "json";
  114. var type = registeredProviders[chosenExt];
  115. var provider = Activator.CreateInstance(type) as IConfigProvider;
  116. if (provider != null)
  117. {
  118. provider.Filename = Path.Combine(BeatSaber.UserDataPath, configName);
  119. configProviders.Add(Tuple.Create(Ref.Create(provider.LastModified), provider));
  120. }
  121. return provider;
  122. }
  123. internal static IConfigProvider GetProviderFor(string modName, ParameterInfo info)
  124. {
  125. var prefs = new string[0];
  126. if (info.GetCustomAttribute<PreferAttribute>() is PreferAttribute prefer)
  127. prefs = prefer.PreferenceOrder;
  128. if (info.GetCustomAttribute<NameAttribute>() is NameAttribute name)
  129. modName = name.Name;
  130. return GetProviderFor(modName, prefs);
  131. }
  132. private static Dictionary<IConfigProvider, Action> linkedProviders =
  133. new Dictionary<IConfigProvider, Action>();
  134. /// <summary>
  135. /// Creates a linked <see cref="Ref{T}"/> for the config provider. This <see cref="Ref{T}"/> will be automatically updated whenever the file on-disk changes.
  136. /// </summary>
  137. /// <typeparam name="T">the type of the parsed value</typeparam>
  138. /// <param name="config">the <see cref="IConfigProvider"/> to create a link to</param>
  139. /// <param name="onChange">an action to perform on value change</param>
  140. /// <returns>a <see cref="Ref{T}"/> to an ever-changing value, mirroring whatever the file contains.</returns>
  141. public static Ref<T> MakeLink<T>(this IConfigProvider config, Action<IConfigProvider, Ref<T>> onChange = null)
  142. {
  143. Ref<T> @ref = config.Parse<T>();
  144. void ChangeDelegate()
  145. {
  146. @ref.Value = config.Parse<T>();
  147. onChange?.Invoke(config, @ref);
  148. }
  149. if (linkedProviders.ContainsKey(config))
  150. linkedProviders[config] = (Action) Delegate.Combine(linkedProviders[config], (Action) ChangeDelegate);
  151. else
  152. linkedProviders.Add(config, ChangeDelegate);
  153. ChangeDelegate();
  154. return @ref;
  155. }
  156. /// <summary>
  157. /// Removes all linked <see cref="Ref{T}"/> such that they are no longer updated.
  158. /// </summary>
  159. /// <param name="config">the <see cref="IConfigProvider"/> to unlink</param>
  160. public static void RemoveLinks(this IConfigProvider config)
  161. {
  162. if (linkedProviders.ContainsKey(config))
  163. linkedProviders.Remove(config);
  164. }
  165. internal static void Update()
  166. {
  167. foreach (var provider in configProviders)
  168. {
  169. if (provider.Item2.LastModified > provider.Item1.Value)
  170. {
  171. try
  172. {
  173. provider.Item2.Load(); // auto reload if it changes
  174. provider.Item1.Value = provider.Item2.LastModified;
  175. }
  176. catch (Exception e)
  177. {
  178. Logging.Logger.config.Error("Error when trying to load config");
  179. Logging.Logger.config.Error(e);
  180. }
  181. }
  182. if (provider.Item2.HasChanged)
  183. {
  184. try
  185. {
  186. provider.Item2.Save();
  187. provider.Item1.Value = DateTime.Now;
  188. }
  189. catch (Exception e)
  190. {
  191. Logging.Logger.config.Error("Error when trying to save config");
  192. Logging.Logger.config.Error(e);
  193. }
  194. }
  195. if (provider.Item2.InMemoryChanged)
  196. {
  197. provider.Item2.InMemoryChanged = false;
  198. try
  199. {
  200. if (linkedProviders.ContainsKey(provider.Item2))
  201. linkedProviders[provider.Item2]();
  202. }
  203. catch (Exception e)
  204. {
  205. Logging.Logger.config.Error("Error running link change events");
  206. Logging.Logger.config.Error(e);
  207. }
  208. }
  209. }
  210. }
  211. internal static void Save()
  212. {
  213. foreach (var provider in configProviders)
  214. if (provider.Item2.HasChanged)
  215. try
  216. {
  217. provider.Item2.Save();
  218. }
  219. catch (Exception e)
  220. {
  221. Logging.Logger.config.Error("Error when trying to save config");
  222. Logging.Logger.config.Error(e);
  223. }
  224. }
  225. }
  226. }