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.

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