From cf0119038ce8b212b335da4c7822c7f3d8dda448 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Fri, 26 Mar 2021 23:38:29 -0500 Subject: [PATCH] Implement dependency resolution part of new loader part --- IPA.Loader/Loader/PluginLoader.cs | 136 +++++++++++++++++++++++++++- IPA.Loader/Loader/PluginManifest.cs | 37 ++++---- IPA.Loader/Loader/PluginMetadata.cs | 8 +- 3 files changed, 158 insertions(+), 23 deletions(-) diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index d2735b21..274d0c64 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -14,6 +14,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Version = SemVer.Version; using SemVer; +using System.Diagnostics.CodeAnalysis; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; @@ -52,6 +53,11 @@ namespace IPA.Loader ResolveDependencies(); #endif + + // Features contribute to load order considerations + InitFeatures(); + DoOrderResolution(); + }); internal static void YeetIfNeeded() @@ -159,6 +165,12 @@ namespace IPA.Loader continue; } + if (pluginManifest.Id == null) + { + Logger.loader.Warn($"Plugin '{pluginManifest.Name}' does not have a listed ID, using name"); + pluginManifest.Id = pluginManifest.Name; + } + metadata.Manifest = pluginManifest; void TryGetNamespacedPluginType(string ns, PluginMetadata meta) @@ -720,6 +732,125 @@ namespace IPA.Loader } #endif + internal static void DoOrderResolution() + { + PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version)); + + var metadataCache = new Dictionary(PluginsMetadata.Count); + var pluginsToProcess = new List(PluginsMetadata.Count); + + var disabledIds = DisabledConfig.Instance.DisabledModIds; + var disabledPlugins = new List(); + + // build metadata cache + foreach (var meta in PluginsMetadata) + { + if (!metadataCache.TryGetValue(meta.Id, out var existing)) + { + if (disabledIds.Contains(meta.Id)) + { + metadataCache.Add(meta.Id, (meta, false)); + disabledPlugins.Add(meta); + } + else + { + metadataCache.Add(meta.Id, (meta, true)); + pluginsToProcess.Add(meta); + } + } + else + { + Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); + ignoredPlugins.Add(meta, new(Reason.Duplicate) + { + ReasonText = $"Duplicate entry of same ID ({meta.Id})", + RelatedTo = existing.Meta + }); + } + } + + var loadedPlugins = new Dictionary(); + var outputOrder = new List(PluginsMetadata.Count); + + { + bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored) + { + meta = null; + disabled = false; + ignored = true; + if (loadedPlugins.TryGetValue(id, out var foundMeta)) + { + meta = foundMeta.Meta; + disabled = foundMeta.Disabled; + ignored = foundMeta.Ignored; + return true; + } + if (metadataCache!.TryGetValue(id, out var plugin)) + { + disabled = !plugin.Enabled; + meta = plugin.Meta; + if (!disabled) + { + Resolve(plugin.Meta, out disabled, out ignored); + } + loadedPlugins.Add(id, (plugin.Meta, disabled, ignored)); + return true; + } + return false; + } + + void Resolve(PluginMetadata plugin, out bool disabled, out bool ignored) + { + disabled = false; + ignored = false; + + // first load dependencies + foreach (var dep in plugin.Manifest.Dependencies) + { + if (!TryResolveId(dep.Key, out var depMeta, out var depDisabled, out var depIgnored)) + { + Logger.loader.Warn($"Dependency '{dep.Key}@{dep.Value}' for '{plugin.Id}' does not exist; ignoring '{plugin.Id}'"); + ignoredPlugins.Add(plugin, new(Reason.Dependency) + { + ReasonText = $"Dependency '{dep.Key}@{dep.Value}' not found", + }); + ignored = true; + return; + } + // make a point to propagate ignored + if (depIgnored) + { + Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'"); + ignoredPlugins.Add(plugin, new(Reason.Dependency) + { + ReasonText = $"Dependency '{dep.Key}' ignored", + RelatedTo = depMeta + }); + ignored = true; + return; + } + // make a point to propagate disabled + if (depDisabled) + { + Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' disabled; disabling"); + disabledPlugins!.Add(plugin); + _ = disabledIds!.Add(plugin.Id); + disabled = true; + } + + // we found our dep, lets save the metadata and keep going + _ = plugin.Dependencies.Add(depMeta); + } + + + } + } + + DisabledConfig.Instance.Changed(); + DisabledPlugins = disabledPlugins; + PluginsMetadata = outputOrder; + } + internal static void InitFeatures() { foreach (var meta in PluginsMetadata) @@ -772,7 +903,9 @@ namespace IPA.Loader internal static void ReleaseAll(bool full = false) { if (full) - ignoredPlugins = new Dictionary(); + { + ignoredPlugins = new(); + } else { foreach (var m in PluginsMetadata) @@ -788,6 +921,7 @@ namespace IPA.Loader DisabledPlugins = new List(); Feature.Reset(); GC.Collect(); + GC.WaitForPendingFinalizers(); } internal static void Load(PluginMetadata meta) diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index 7361975e..104b9234 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -1,4 +1,5 @@ -using IPA.JsonConverters; +#nullable enable +using IPA.JsonConverters; using IPA.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -17,31 +18,31 @@ namespace IPA.Loader internal class PluginManifest { [JsonProperty("name", Required = Required.Always)] - public string Name; + public string Name = null!; - [JsonProperty("id", Required = Required.AllowNull)] - public string Id; + [JsonProperty("id", Required = Required.AllowNull)] // TODO: on major version bump, make this always + public string? Id; [JsonProperty("description", Required = Required.Always), JsonConverter(typeof(MultilineStringConverter))] - public string Description; + public string Description = null!; [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] - public Version Version; + public Version Version = null!; [JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(AlmostVersionConverter))] - public AlmostVersion GameVersion; + public AlmostVersion GameVersion = null!; [JsonProperty("author", Required = Required.Always)] - public string Author; + public string Author = null!; [JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Dependencies = new Dictionary(); + public Dictionary Dependencies = new(); [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Conflicts = new Dictionary(); + public Dictionary Conflicts = new(); [JsonProperty("features", Required = Required.DisallowNull), JsonConverter(typeof(FeaturesFieldConverter))] - public Dictionary Features = new Dictionary(); + public Dictionary Features = new(); [JsonProperty("loadBefore", Required = Required.DisallowNull)] public string[] LoadBefore = Array.Empty(); @@ -50,7 +51,7 @@ namespace IPA.Loader public string[] LoadAfter = Array.Empty(); [JsonProperty("icon", Required = Required.DisallowNull)] - public string IconPath = null; + public string? IconPath = null; [JsonProperty("files", Required = Required.DisallowNull)] public string[] Files = Array.Empty(); @@ -59,26 +60,26 @@ namespace IPA.Loader public class LinksObject { [JsonProperty("project-home", Required = Required.DisallowNull)] - public Uri ProjectHome = null; + public Uri? ProjectHome = null; [JsonProperty("project-source", Required = Required.DisallowNull)] - public Uri ProjectSource = null; + public Uri? ProjectSource = null; [JsonProperty("donate", Required = Required.DisallowNull)] - public Uri Donate = null; + public Uri? Donate = null; } [JsonProperty("links", Required = Required.DisallowNull)] - public LinksObject Links = null; + public LinksObject? Links = null; [Serializable] public class MiscObject { [JsonProperty("plugin-hint", Required = Required.DisallowNull)] - public string PluginMainHint = null; + public string? PluginMainHint = null; } [JsonProperty("misc", Required = Required.DisallowNull)] - public MiscObject Misc = null; + public MiscObject? Misc = null; } } \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginMetadata.cs b/IPA.Loader/Loader/PluginMetadata.cs index 10c13394..d158edc8 100644 --- a/IPA.Loader/Loader/PluginMetadata.cs +++ b/IPA.Loader/Loader/PluginMetadata.cs @@ -39,10 +39,10 @@ namespace IPA.Loader public string Name => manifest.Name; /// - /// The BeatMods ID of the plugin, or null if it doesn't have one. + /// The ID of the plugin. /// - /// the updater ID of the plugin - public string Id => manifest.Id; + /// the ID of the plugin + public string Id => manifest.Id!; // by the time that this is publicly visible, it's always non-null /// /// The name of the author that wrote this plugin. @@ -91,7 +91,7 @@ namespace IPA.Loader /// The name of the resource in the plugin assembly containing the plugin's icon. /// /// the name of the plugin's icon - public string IconName => manifest.IconPath; + public string? IconName => manifest.IconPath; /// /// A link to this plugin's home page, if any.