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.

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