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.

1018 lines
44 KiB

  1. using IPA.Config;
  2. using IPA.Loader.Features;
  3. using IPA.Logging;
  4. using IPA.Utilities;
  5. using Mono.Cecil;
  6. using Newtonsoft.Json;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Reflection;
  12. using System.Text.RegularExpressions;
  13. using System.Threading.Tasks;
  14. using Version = SemVer.Version;
  15. using SemVer;
  16. using System.Linq.Expressions;
  17. #if NET4
  18. using Task = System.Threading.Tasks.Task;
  19. using TaskEx = System.Threading.Tasks.Task;
  20. #endif
  21. #if NET3
  22. using Net3_Proxy;
  23. using Path = Net3_Proxy.Path;
  24. using File = Net3_Proxy.File;
  25. using Directory = Net3_Proxy.Directory;
  26. #endif
  27. namespace IPA.Loader
  28. {
  29. /// <summary>
  30. /// A type to manage the loading of plugins.
  31. /// </summary>
  32. public class PluginLoader
  33. {
  34. internal static Task LoadTask() =>
  35. TaskEx.Run(() =>
  36. {
  37. YeetIfNeeded();
  38. LoadMetadata();
  39. Resolve();
  40. ComputeLoadOrder();
  41. FilterDisabled();
  42. ResolveDependencies();
  43. });
  44. /// <summary>
  45. /// A class which describes a loaded plugin.
  46. /// </summary>
  47. public class PluginMetadata
  48. {
  49. /// <summary>
  50. /// The assembly the plugin was loaded from.
  51. /// </summary>
  52. /// <value>the loaded Assembly that contains the plugin main type</value>
  53. public Assembly Assembly { get; internal set; }
  54. /// <summary>
  55. /// The TypeDefinition for the main type of the plugin.
  56. /// </summary>
  57. /// <value>the Cecil definition for the plugin main type</value>
  58. public TypeDefinition PluginType { get; internal set; }
  59. /// <summary>
  60. /// The human readable name of the plugin.
  61. /// </summary>
  62. /// <value>the name of the plugin</value>
  63. public string Name { get; internal set; }
  64. /// <summary>
  65. /// The BeatMods ID of the plugin, or null if it doesn't have one.
  66. /// </summary>
  67. /// <value>the updater ID of the plugin</value>
  68. public string Id { get; internal set; }
  69. /// <summary>
  70. /// The version of the plugin.
  71. /// </summary>
  72. /// <value>the version of the plugin</value>
  73. public Version Version { get; internal set; }
  74. /// <summary>
  75. /// The file the plugin was loaded from.
  76. /// </summary>
  77. /// <value>the file the plugin was loaded from</value>
  78. public FileInfo File { get; internal set; }
  79. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  80. /// <summary>
  81. /// The features this plugin requests.
  82. /// </summary>
  83. /// <value>the list of features requested by the plugin</value>
  84. public IReadOnlyList<Feature> Features => InternalFeatures;
  85. internal readonly List<Feature> InternalFeatures = new List<Feature>();
  86. internal bool IsSelf;
  87. /// <summary>
  88. /// Whether or not this metadata object represents a bare manifest.
  89. /// </summary>
  90. /// <value><see langword="true"/> if it is bare, <see langword="false"/> otherwise</value>
  91. public bool IsBare { get; internal set; }
  92. private PluginManifest manifest;
  93. internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>();
  94. internal PluginManifest Manifest
  95. {
  96. get => manifest;
  97. set
  98. {
  99. manifest = value;
  100. Name = value.Name;
  101. Version = value.Version;
  102. Id = value.Id;
  103. }
  104. }
  105. public RuntimeOptions RuntimeOptions { get; internal set; }
  106. public bool IsAttributePlugin { get; internal set; } = false;
  107. /// <summary>
  108. /// Gets all of the metadata as a readable string.
  109. /// </summary>
  110. /// <returns>the readable printable metadata string</returns>
  111. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  112. }
  113. internal class PluginExecutor
  114. {
  115. public PluginMetadata Metadata { get; }
  116. public PluginExecutor(PluginMetadata meta)
  117. {
  118. Metadata = meta;
  119. PrepareDelegates();
  120. }
  121. private object pluginObject = null;
  122. private Func<PluginMetadata, object> CreatePlugin { get; set; }
  123. private Action<object> LifecycleEnable { get; set; }
  124. // disable may be async (#24)
  125. private Func<object, Task> LifecycleDisable { get; set; }
  126. public void Create()
  127. {
  128. if (pluginObject != null) return;
  129. pluginObject = CreatePlugin(Metadata);
  130. }
  131. public void Enable() => LifecycleEnable(pluginObject);
  132. public Task Disable() => LifecycleDisable(pluginObject);
  133. private void PrepareDelegates()
  134. { // TODO: use custom exception types or something
  135. Load(Metadata);
  136. var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName);
  137. CreatePlugin = MakeCreateFunc(type, Metadata.Name);
  138. LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name);
  139. LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name);
  140. }
  141. private static Func<PluginMetadata, object> MakeCreateFunc(Type type, string name)
  142. { // TODO: what do i want the visibiliy of Init methods to be?
  143. var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
  144. .Select(c => (c, attr: c.GetCustomAttribute<InitAttribute>()))
  145. .NonNull(t => t.attr)
  146. .OrderByDescending(t => t.c.GetParameters().Length)
  147. .Select(t => t.c).ToArray();
  148. if (ctors.Length > 1)
  149. Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters.");
  150. bool usingDefaultCtor = false;
  151. var ctor = ctors.FirstOrDefault();
  152. if (ctor == null)
  153. { // this is a normal case
  154. usingDefaultCtor = true;
  155. ctor = type.GetConstructor(Type.EmptyTypes);
  156. if (ctor == null)
  157. throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]");
  158. }
  159. var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  160. .Select(m => (m, attr: m.GetCustomAttribute<InitAttribute>()))
  161. .NonNull(t => t.attr).Select(t => t.m).ToArray();
  162. // verify that they don't have lifecycle attributes on them
  163. foreach (var method in initMethods)
  164. {
  165. var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false);
  166. if (attrs.Length != 0)
  167. throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute.");
  168. }
  169. // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3?
  170. var metaParam = Expression.Parameter(typeof(PluginMetadata));
  171. var objVar = Expression.Variable(type);
  172. var createExpr = Expression.Lambda<Func<PluginMetadata, object>>(
  173. Expression.Block(
  174. initMethods
  175. .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es)))
  176. .Prepend(Expression.Assign(objVar,
  177. usingDefaultCtor
  178. ? Expression.New(ctor)
  179. : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es))))
  180. .Append(Expression.Convert(objVar, typeof(object)))),
  181. metaParam);
  182. // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler
  183. return createExpr.Compile();
  184. }
  185. private static Action<object> MakeLifecycleEnableFunc(Type type, string name)
  186. {
  187. var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  188. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  189. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  190. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable))
  191. .Select(t => t.m).ToArray();
  192. if (enableMethods.Length == 0)
  193. {
  194. Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?");
  195. return o => { };
  196. }
  197. foreach (var m in enableMethods)
  198. {
  199. if (m.GetParameters().Length > 0)
  200. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters.");
  201. if (m.ReturnType != typeof(void))
  202. Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored.");
  203. }
  204. var objParam = Expression.Parameter(typeof(object));
  205. var instVar = Expression.Variable(type);
  206. var createExpr = Expression.Lambda<Action<object>>(
  207. Expression.Block(
  208. enableMethods
  209. .Select(m => Expression.Call(instVar, m))
  210. .Prepend<Expression>(Expression.Assign(instVar, Expression.Convert(objParam, type)))),
  211. objParam);
  212. return createExpr.Compile();
  213. }
  214. private static Func<object, Task> MakeLifecycleDisableFunc(Type type, string name)
  215. {
  216. var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  217. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  218. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  219. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable))
  220. .Select(t => t.m).ToArray();
  221. if (disableMethods.Length == 0)
  222. {
  223. Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?");
  224. return o => Task.CompletedTask;
  225. }
  226. var taskMethods = new List<MethodInfo>();
  227. var nonTaskMethods = new List<MethodInfo>();
  228. foreach (var m in disableMethods)
  229. {
  230. if (m.GetParameters().Length > 0)
  231. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters.");
  232. if (m.ReturnType != typeof(void))
  233. {
  234. if (typeof(Task).IsAssignableFrom(m.ReturnType))
  235. {
  236. taskMethods.Add(m);
  237. continue;
  238. }
  239. else
  240. Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored.");
  241. }
  242. nonTaskMethods.Add(m);
  243. }
  244. Expression<Func<Task>> completedTaskDel = () => Task.CompletedTask;
  245. var getCompletedTask = completedTaskDel.Body;
  246. var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static);
  247. var objParam = Expression.Parameter(typeof(object));
  248. var instVar = Expression.Variable(type);
  249. var createExpr = Expression.Lambda<Func<object, Task>>(
  250. Expression.Block(
  251. nonTaskMethods
  252. .Select(m => Expression.Call(instVar, m))
  253. .Prepend<Expression>(Expression.Assign(instVar, Expression.Convert(objParam, type)))
  254. .Append(
  255. taskMethods.Count == 0
  256. ? getCompletedTask
  257. : Expression.Call(taskWhenAll,
  258. Expression.NewArrayInit(typeof(Task),
  259. taskMethods.Select(m =>
  260. Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
  261. objParam);
  262. return createExpr.Compile();
  263. }
  264. }
  265. /// <summary>
  266. /// A container object for all the data relating to a plugin.
  267. /// </summary>
  268. [Obsolete("No longer useful as a construct")]
  269. public class PluginInfo
  270. {
  271. internal IPlugin Plugin { get; set; }
  272. /// <summary>
  273. /// Metadata for the plugin.
  274. /// </summary>
  275. /// <value>the metadata for this plugin</value>
  276. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  277. }
  278. internal static void YeetIfNeeded()
  279. {
  280. string pluginDir = BeatSaber.PluginsPath;
  281. if (SelfConfig.YeetMods_ && BeatSaber.IsGameVersionBoundary)
  282. {
  283. var oldPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.OldVersion} Plugins");
  284. var newPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.GameVersion} Plugins");
  285. if (Directory.Exists(oldPluginsName))
  286. Directory.Delete(oldPluginsName, true);
  287. Directory.Move(pluginDir, oldPluginsName);
  288. if (Directory.Exists(newPluginsName))
  289. Directory.Move(newPluginsName, pluginDir);
  290. else
  291. Directory.CreateDirectory(pluginDir);
  292. }
  293. }
  294. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  295. internal static List<PluginMetadata> DisabledPlugins = new List<PluginMetadata>();
  296. private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  297. internal static void LoadMetadata()
  298. {
  299. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  300. try
  301. {
  302. var selfMeta = new PluginMetadata
  303. {
  304. Assembly = Assembly.GetExecutingAssembly(),
  305. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  306. PluginType = null,
  307. IsSelf = true
  308. };
  309. string manifest;
  310. using (var manifestReader =
  311. new StreamReader(
  312. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  313. throw new InvalidOperationException()))
  314. manifest = manifestReader.ReadToEnd();
  315. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  316. PluginsMetadata.Add(selfMeta);
  317. }
  318. catch (Exception e)
  319. {
  320. Logger.loader.Critical("Error loading own manifest");
  321. Logger.loader.Critical(e);
  322. }
  323. foreach (var plugin in plugins)
  324. {
  325. var metadata = new PluginMetadata
  326. {
  327. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
  328. IsSelf = false
  329. };
  330. try
  331. {
  332. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  333. {
  334. ReadingMode = ReadingMode.Immediate,
  335. ReadWrite = false,
  336. AssemblyResolver = new CecilLibLoader()
  337. }).MainModule;
  338. string pluginNs = "";
  339. foreach (var resource in pluginModule.Resources)
  340. {
  341. const string manifestSuffix = ".manifest.json";
  342. if (!(resource is EmbeddedResource embedded) ||
  343. !embedded.Name.EndsWith(manifestSuffix)) continue;
  344. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  345. string manifest;
  346. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  347. manifest = manifestReader.ReadToEnd();
  348. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  349. break;
  350. }
  351. if (metadata.Manifest == null)
  352. {
  353. #if DIRE_LOADER_WARNINGS
  354. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  355. #else
  356. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  357. #endif
  358. continue;
  359. }
  360. void TryGetNamespacedPluginType(string ns, PluginMetadata meta)
  361. {
  362. foreach (var type in pluginModule.Types)
  363. {
  364. if (type.Namespace != ns) continue;
  365. if (type.HasCustomAttributes)
  366. {
  367. var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName);
  368. if (attr != null)
  369. {
  370. if (!attr.HasConstructorArguments)
  371. {
  372. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments");
  373. return;
  374. }
  375. var args = attr.ConstructorArguments;
  376. if (args.Count != 1)
  377. {
  378. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments");
  379. return;
  380. }
  381. var rtOptionsArg = args[0];
  382. if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName)
  383. {
  384. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}");
  385. return;
  386. }
  387. var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions
  388. meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt;
  389. meta.IsAttributePlugin = true;
  390. meta.PluginType = type;
  391. return;
  392. }
  393. }
  394. if (type.HasInterface(typeof(IPlugin).FullName))
  395. {
  396. Logger.loader.Warn("Interface-based plugin found");
  397. meta.RuntimeOptions = RuntimeOptions.SingleDynamicInit;
  398. meta.PluginType = type;
  399. return;
  400. }
  401. }
  402. }
  403. var hint = metadata.Manifest.Misc?.PluginMainHint;
  404. if (hint != null)
  405. {
  406. var type = pluginModule.GetType(hint);
  407. if (type != null)
  408. TryGetNamespacedPluginType(hint, metadata);
  409. }
  410. if (metadata.PluginType == null)
  411. TryGetNamespacedPluginType(pluginNs, metadata);
  412. if (metadata.PluginType == null)
  413. {
  414. Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  415. continue;
  416. }
  417. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  418. PluginsMetadata.Add(metadata);
  419. }
  420. catch (Exception e)
  421. {
  422. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  423. Logger.loader.Error(e);
  424. ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
  425. {
  426. ReasonText = "An error ocurred loading the data",
  427. Error = e
  428. });
  429. }
  430. }
  431. IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json");
  432. bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest"));
  433. foreach (var manifest in bareManifests)
  434. { // TODO: maybe find a way to allow a bare manifest to specify an associated file
  435. try
  436. {
  437. var metadata = new PluginMetadata
  438. {
  439. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)),
  440. IsSelf = false,
  441. IsBare = true,
  442. };
  443. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  444. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  445. PluginsMetadata.Add(metadata);
  446. }
  447. catch (Exception e)
  448. {
  449. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  450. Logger.loader.Error(e);
  451. }
  452. }
  453. foreach (var meta in PluginsMetadata)
  454. { // process description include
  455. var lines = meta.Manifest.Description.Split('\n');
  456. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  457. if (m.Success)
  458. {
  459. if (meta.IsBare)
  460. {
  461. Logger.loader.Warn($"Bare manifest cannot specify description file");
  462. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  463. continue;
  464. }
  465. var name = m.Groups[1].Value;
  466. string description;
  467. if (!meta.IsSelf)
  468. {
  469. var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource)
  470. .NonNull()
  471. .FirstOrDefault(r => r.Name == name);
  472. if (resc == null)
  473. {
  474. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  475. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  476. continue;
  477. }
  478. using var reader = new StreamReader(resc.GetResourceStream());
  479. description = reader.ReadToEnd();
  480. }
  481. else
  482. {
  483. using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name));
  484. description = descriptionReader.ReadToEnd();
  485. }
  486. meta.Manifest.Description = description;
  487. }
  488. }
  489. }
  490. internal enum Reason
  491. {
  492. Error, Duplicate, Conflict, Dependency,
  493. Released, Feature, Unsupported
  494. }
  495. internal struct IgnoreReason
  496. {
  497. public Reason Reason { get; }
  498. public string ReasonText { get; set; }
  499. public Exception Error { get; set; }
  500. public PluginMetadata RelatedTo { get; set; }
  501. public IgnoreReason(Reason reason)
  502. {
  503. Reason = reason;
  504. ReasonText = null;
  505. Error = null;
  506. RelatedTo = null;
  507. }
  508. }
  509. // keep track of these for the updater; it should still be able to update mods not loaded
  510. // the thing -> the reason
  511. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  512. internal static void Resolve()
  513. { // resolves duplicates and conflicts, etc
  514. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  515. var ids = new HashSet<string>();
  516. var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
  517. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  518. foreach (var meta in PluginsMetadata)
  519. {
  520. if (meta.Id != null)
  521. {
  522. if (ids.Contains(meta.Id))
  523. {
  524. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  525. var ireason = new IgnoreReason(Reason.Duplicate)
  526. {
  527. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  528. RelatedTo = resolved.First(p => p.Id == meta.Id)
  529. };
  530. ignore.Add(meta, ireason);
  531. ignoredPlugins.Add(meta, ireason);
  532. continue; // because of sorted order, hightest order will always be the first one
  533. }
  534. bool processedLater = false;
  535. foreach (var meta2 in PluginsMetadata)
  536. {
  537. if (ignore.ContainsKey(meta2)) continue;
  538. if (meta == meta2)
  539. {
  540. processedLater = true;
  541. continue;
  542. }
  543. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  544. var range = meta2.Manifest.Conflicts[meta.Id];
  545. if (!range.IsSatisfied(meta.Version)) continue;
  546. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}");
  547. if (processedLater)
  548. {
  549. Logger.loader.Warn($"Ignoring {meta2.Name}");
  550. ignore.Add(meta2, new IgnoreReason(Reason.Conflict)
  551. {
  552. ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}",
  553. RelatedTo = meta
  554. });
  555. }
  556. else
  557. {
  558. Logger.loader.Warn($"Ignoring {meta.Name}");
  559. ignore.Add(meta, new IgnoreReason(Reason.Conflict)
  560. {
  561. ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}",
  562. RelatedTo = meta2
  563. });
  564. break;
  565. }
  566. }
  567. }
  568. if (ignore.TryGetValue(meta, out var reason))
  569. {
  570. ignoredPlugins.Add(meta, reason);
  571. continue;
  572. }
  573. if (meta.Id != null)
  574. ids.Add(meta.Id);
  575. resolved.Add(meta);
  576. }
  577. PluginsMetadata = resolved;
  578. }
  579. private static void FilterDisabled()
  580. {
  581. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  582. var disabled = DisabledConfig.Instance.DisabledModIds;
  583. foreach (var meta in PluginsMetadata)
  584. {
  585. if (disabled.Contains(meta.Id ?? meta.Name))
  586. DisabledPlugins.Add(meta);
  587. else
  588. enabled.Add(meta);
  589. }
  590. PluginsMetadata = enabled;
  591. }
  592. internal static void ComputeLoadOrder()
  593. {
  594. #if DEBUG
  595. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  596. #endif
  597. static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  598. { // this is slow, and hella recursive
  599. bool inserted = false;
  600. foreach (var sr in root)
  601. {
  602. inserted = inserted || InsertInto(sr.Dependencies, meta);
  603. if (meta.Id != null)
  604. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  605. inserted = inserted || sr.Dependencies.Add(meta);
  606. if (sr.Id != null)
  607. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  608. inserted = inserted || sr.Dependencies.Add(meta);
  609. }
  610. if (isRoot)
  611. {
  612. foreach (var sr in root)
  613. {
  614. InsertInto(meta.Dependencies, sr);
  615. if (sr.Id != null)
  616. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  617. meta.Dependencies.Add(sr);
  618. if (meta.Id != null)
  619. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  620. meta.Dependencies.Add(sr);
  621. }
  622. root.Add(meta);
  623. }
  624. return inserted;
  625. }
  626. var pluginTree = new HashSet<PluginMetadata>();
  627. foreach (var meta in PluginsMetadata)
  628. InsertInto(pluginTree, meta, true);
  629. static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  630. {
  631. foreach (var st in tree)
  632. if (!into.Contains(st))
  633. {
  634. DeTree(into, st.Dependencies);
  635. into.Add(st);
  636. }
  637. }
  638. PluginsMetadata = new List<PluginMetadata>();
  639. DeTree(PluginsMetadata, pluginTree);
  640. #if DEBUG
  641. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  642. #endif
  643. }
  644. internal static void ResolveDependencies()
  645. {
  646. var metadata = new List<PluginMetadata>();
  647. var pluginsToLoad = new Dictionary<string, Version>();
  648. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  649. foreach (var meta in PluginsMetadata)
  650. {
  651. var missingDeps = new List<(string id, Range version, bool disabled)>();
  652. foreach (var dep in meta.Manifest.Dependencies)
  653. {
  654. #if DEBUG
  655. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  656. #endif
  657. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  658. continue;
  659. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  660. {
  661. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  662. missingDeps.Add((dep.Key, dep.Value, true));
  663. }
  664. else
  665. {
  666. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  667. missingDeps.Add((dep.Key, dep.Value, false));
  668. }
  669. }
  670. if (missingDeps.Count == 0)
  671. {
  672. metadata.Add(meta);
  673. if (meta.Id != null)
  674. pluginsToLoad.Add(meta.Id, meta.Version);
  675. }
  676. else if (missingDeps.Any(t => !t.disabled))
  677. { // missing deps
  678. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  679. {
  680. ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}"
  681. });
  682. }
  683. else
  684. {
  685. DisabledPlugins.Add(meta);
  686. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  687. }
  688. }
  689. PluginsMetadata = metadata;
  690. }
  691. internal static void InitFeatures()
  692. {
  693. var parsedFeatures = PluginsMetadata.Select(m =>
  694. (metadata: m,
  695. features: m.Manifest.Features.Select(feature =>
  696. (feature, parsed: Ref.Create<Feature.FeatureParse?>(null))
  697. ).ToList()
  698. )
  699. ).ToList();
  700. while (DefineFeature.NewFeature)
  701. {
  702. DefineFeature.NewFeature = false;
  703. foreach (var (metadata, features) in parsedFeatures)
  704. for (var i = 0; i < features.Count; i++)
  705. {
  706. var feature = features[i];
  707. var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj,
  708. out var exception, out var valid, out var parsed, feature.parsed.Value);
  709. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  710. feature.parsed.Value = parsed;
  711. else if (success)
  712. {
  713. if (valid && featureObj.StoreOnPlugin)
  714. metadata.InternalFeatures.Add(featureObj);
  715. else if (!valid)
  716. Logger.features.Warn(
  717. $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}");
  718. features.RemoveAt(i--);
  719. }
  720. else
  721. {
  722. Logger.features.Error($"Error parsing feature definition on {metadata.Name}");
  723. Logger.features.Error(exception);
  724. features.RemoveAt(i--);
  725. }
  726. }
  727. foreach (var plugin in PluginsMetadata)
  728. foreach (var feature in plugin.Features)
  729. feature.Evaluate();
  730. }
  731. foreach (var plugin in parsedFeatures)
  732. {
  733. if (plugin.features.Count <= 0) continue;
  734. Logger.features.Warn($"On plugin {plugin.metadata.Name}:");
  735. foreach (var feature in plugin.features)
  736. Logger.features.Warn($" Feature not found with name {feature.feature}");
  737. }
  738. }
  739. internal static void ReleaseAll(bool full = false)
  740. {
  741. if (full)
  742. ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  743. else
  744. {
  745. foreach (var m in PluginsMetadata)
  746. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  747. foreach (var m in ignoredPlugins.Keys)
  748. { // clean them up so we can still use the metadata for updates
  749. m.InternalFeatures.Clear();
  750. m.PluginType = null;
  751. m.Assembly = null;
  752. }
  753. }
  754. PluginsMetadata = new List<PluginMetadata>();
  755. DisabledPlugins = new List<PluginMetadata>();
  756. Feature.Reset();
  757. GC.Collect();
  758. }
  759. internal static void Load(PluginMetadata meta)
  760. {
  761. if (meta.Assembly == null && meta.PluginType != null)
  762. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  763. }
  764. internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  765. {
  766. if (meta.IsAttributePlugin)
  767. {
  768. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Attribute plugins are currently not supported" });
  769. return null;
  770. }
  771. if (meta.PluginType == null)
  772. return new PluginInfo()
  773. {
  774. Metadata = meta,
  775. Plugin = null
  776. };
  777. var info = new PluginInfo();
  778. if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
  779. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  780. try
  781. {
  782. foreach (var dep in meta.Dependencies)
  783. {
  784. if (alreadyLoaded.Contains(dep)) continue;
  785. // otherwise...
  786. if (ignoredPlugins.TryGetValue(dep, out var reason))
  787. { // was added to the ignore list
  788. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  789. {
  790. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  791. RelatedTo = dep
  792. });
  793. }
  794. else
  795. { // was not added to ignore list
  796. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  797. {
  798. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  799. RelatedTo = dep
  800. });
  801. }
  802. return null;
  803. }
  804. Load(meta);
  805. Feature denyingFeature = null;
  806. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  807. {
  808. Logger.loader.Warn(
  809. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  810. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  811. {
  812. ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  813. });
  814. return null;
  815. }
  816. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  817. var instance = Activator.CreateInstance(type) as IPlugin;
  818. info.Metadata = meta;
  819. info.Plugin = instance;
  820. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  821. if (init != null)
  822. {
  823. denyingFeature = null;
  824. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  825. {
  826. Logger.loader.Warn(
  827. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  828. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  829. {
  830. ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  831. });
  832. return null;
  833. }
  834. var args = PluginInitInjector.Inject(init.GetParameters(), meta);
  835. init.Invoke(info.Plugin, args);
  836. }
  837. foreach (var feature in meta.Features)
  838. try
  839. {
  840. feature.AfterInit(info, info.Plugin);
  841. }
  842. catch (Exception e)
  843. {
  844. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  845. }
  846. }
  847. catch (AmbiguousMatchException)
  848. {
  849. Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  850. Logger.loader.Critical("@Developer: you *really* should fix this");
  851. // not adding to ignoredPlugins here because this should only happen in a development context
  852. // if someone fucks this up on release thats on them
  853. return null;
  854. }
  855. catch (Exception e)
  856. {
  857. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  858. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  859. {
  860. ReasonText = "Error ocurred while initializing",
  861. Error = e
  862. });
  863. return null;
  864. }
  865. return info;
  866. }
  867. internal static List<PluginInfo> LoadPlugins()
  868. {
  869. InitFeatures();
  870. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  871. var list = new List<PluginInfo>();
  872. var loaded = new HashSet<PluginMetadata>();
  873. foreach (var meta in PluginsMetadata)
  874. {
  875. var info = InitPlugin(meta, loaded);
  876. if (info != null)
  877. {
  878. list.Add(info);
  879. loaded.Add(meta);
  880. }
  881. }
  882. return list;
  883. }
  884. }
  885. }