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.

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