From 58fcf9a5f5d03e951d160fb6364e16d9e58eb909 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Fri, 3 Jan 2020 16:43:42 -0600 Subject: [PATCH] Added more support for annotations --- IPA.Loader/Loader/PluginLoader.cs | 1654 +++++++++-------- IPA.Loader/Loader/PluginManifest.cs | 148 +- .../Attributes/PluginAttribute.cs | 1 + 3 files changed, 936 insertions(+), 867 deletions(-) diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index d60f3bf4..3da0cae5 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -1,799 +1,857 @@ -using IPA.Config; -using IPA.Loader.Features; -using IPA.Logging; -using IPA.Utilities; -using Mono.Cecil; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Version = SemVer.Version; -using SemVer; -#if NET4 -using Task = System.Threading.Tasks.Task; -using TaskEx = System.Threading.Tasks.Task; -#endif -#if NET3 -using Net3_Proxy; -using Path = Net3_Proxy.Path; -using File = Net3_Proxy.File; -using Directory = Net3_Proxy.Directory; -#endif - -namespace IPA.Loader -{ - /// - /// A type to manage the loading of plugins. - /// - public class PluginLoader - { - internal static Task LoadTask() => - TaskEx.Run(() => - { - YeetIfNeeded(); - - LoadMetadata(); - Resolve(); - ComputeLoadOrder(); - FilterDisabled(); - - ResolveDependencies(); - }); - - /// - /// A class which describes a loaded plugin. - /// - public class PluginMetadata - { - /// - /// The assembly the plugin was loaded from. - /// - /// the loaded Assembly that contains the plugin main type - public Assembly Assembly { get; internal set; } - - /// - /// The TypeDefinition for the main type of the plugin. - /// - /// the Cecil definition for the plugin main type - public TypeDefinition PluginType { get; internal set; } - - /// - /// The human readable name of the plugin. - /// - /// the name of the plugin - public string Name { get; internal set; } - - /// - /// The BeatMods ID of the plugin, or null if it doesn't have one. - /// - /// the updater ID of the plugin - public string Id { get; internal set; } - - /// - /// The version of the plugin. - /// - /// the version of the plugin - public Version Version { get; internal set; } - - /// - /// The file the plugin was loaded from. - /// - /// the file the plugin was loaded from - public FileInfo File { get; internal set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Global - /// - /// The features this plugin requests. - /// - /// the list of features requested by the plugin - public IReadOnlyList Features => InternalFeatures; - - internal readonly List InternalFeatures = new List(); - - internal bool IsSelf; - - /// - /// Whether or not this metadata object represents a bare manifest. - /// - /// if it is bare, otherwise - public bool IsBare { get; internal set; } - - private PluginManifest manifest; - - internal HashSet Dependencies { get; } = new HashSet(); - - internal PluginManifest Manifest - { - get => manifest; - set - { - manifest = value; - Name = value.Name; - Version = value.Version; - Id = value.Id; - } - } - - /// - /// Gets all of the metadata as a readable string. - /// - /// the readable printable metadata string - public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; - } - - /// - /// A container object for all the data relating to a plugin. - /// - public class PluginInfo - { - internal IPlugin Plugin { get; set; } - - /// - /// Metadata for the plugin. - /// - /// the metadata for this plugin - public PluginMetadata Metadata { get; internal set; } = new PluginMetadata(); - } - - internal static void YeetIfNeeded() - { - string pluginDir = BeatSaber.PluginsPath; - - if (SelfConfig.YeetMods_ && BeatSaber.IsGameVersionBoundary) - { - var oldPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.OldVersion} Plugins"); - var newPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.GameVersion} Plugins"); - - if (Directory.Exists(oldPluginsName)) - Directory.Delete(oldPluginsName, true); - Directory.Move(pluginDir, oldPluginsName); - if (Directory.Exists(newPluginsName)) - Directory.Move(newPluginsName, pluginDir); - else - Directory.CreateDirectory(pluginDir); - } - } - - internal static List PluginsMetadata = new List(); - internal static List DisabledPlugins = new List(); - - private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline); - - internal static void LoadMetadata() - { - string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll"); - - try - { - var selfMeta = new PluginMetadata - { - Assembly = Assembly.GetExecutingAssembly(), - File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")), - PluginType = null, - IsSelf = true - }; - - string manifest; - using (var manifestReader = - new StreamReader( - selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ?? - throw new InvalidOperationException())) - manifest = manifestReader.ReadToEnd(); - - selfMeta.Manifest = JsonConvert.DeserializeObject(manifest); - - PluginsMetadata.Add(selfMeta); - } - catch (Exception e) - { - Logger.loader.Critical("Error loading own manifest"); - Logger.loader.Critical(e); - } - - foreach (var plugin in plugins) - { - var metadata = new PluginMetadata - { - File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)), - IsSelf = false - }; - - try - { - var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters - { - ReadingMode = ReadingMode.Immediate, - ReadWrite = false, - AssemblyResolver = new CecilLibLoader() - }).MainModule; - - string pluginNs = ""; - - foreach (var resource in pluginModule.Resources) - { - const string manifestSuffix = ".manifest.json"; - if (!(resource is EmbeddedResource embedded) || - !embedded.Name.EndsWith(manifestSuffix)) continue; - - pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length); - - string manifest; - using (var manifestReader = new StreamReader(embedded.GetResourceStream())) - manifest = manifestReader.ReadToEnd(); - - metadata.Manifest = JsonConvert.DeserializeObject(manifest); - break; - } - - if (metadata.Manifest == null) - { -#if DIRE_LOADER_WARNINGS - Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}"); -#else - Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}"); -#endif - continue; - } - - foreach (var type in pluginModule.Types) - { - if (type.Namespace != pluginNs) continue; - - if (type.HasInterface(typeof(IPlugin).FullName)) - { - metadata.PluginType = type; - break; - } - } - - if (metadata.PluginType == null) - { - Logger.loader.Error($"No plugin found in the manifest namespace ({pluginNs}) in {Path.GetFileName(plugin)}"); - continue; - } - - Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}"); - PluginsMetadata.Add(metadata); - } - catch (Exception e) - { - Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); - Logger.loader.Error(e); - ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error) - { - ReasonText = "An error ocurred loading the data", - Error = e - }); - } - } - - IEnumerable bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json"); - bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest")); - foreach (var manifest in bareManifests) - { // TODO: maybe find a way to allow a bare manifest to specify an associated file - try - { - var metadata = new PluginMetadata - { - File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)), - IsSelf = false, - IsBare = true, - }; - - metadata.Manifest = JsonConvert.DeserializeObject(File.ReadAllText(manifest)); - - Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}"); - PluginsMetadata.Add(metadata); - } - catch (Exception e) - { - Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}"); - Logger.loader.Error(e); - } - } - - foreach (var meta in PluginsMetadata) - { // process description include - var lines = meta.Manifest.Description.Split('\n'); - var m = embeddedTextDescriptionPattern.Match(lines[0]); - if (m.Success) - { - if (meta.IsBare) - { - Logger.loader.Warn($"Bare manifest cannot specify description file"); - meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line - continue; - } - - var name = m.Groups[1].Value; - string description; - if (!meta.IsSelf) - { - var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource) - .NonNull() - .FirstOrDefault(r => r.Name == name); - if (resc == null) - { - Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include"); - meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line - continue; - } - - using var reader = new StreamReader(resc.GetResourceStream()); - description = reader.ReadToEnd(); - } - else - { - using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name)); - description = descriptionReader.ReadToEnd(); - } - - meta.Manifest.Description = description; - } - } - } - - internal enum Reason - { - Error, Duplicate, Conflict, Dependency, - Released, - Feature - } - internal struct IgnoreReason - { - - public Reason Reason { get; } - public string ReasonText { get; set; } - public Exception Error { get; set; } - public PluginMetadata RelatedTo { get; set; } - public IgnoreReason(Reason reason) - { - Reason = reason; - ReasonText = null; - Error = null; - RelatedTo = null; - } - } - - // keep track of these for the updater; it should still be able to update mods not loaded - // the thing -> the reason - internal static Dictionary ignoredPlugins = new Dictionary(); - - internal static void Resolve() - { // resolves duplicates and conflicts, etc - PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version)); - - var ids = new HashSet(); - var ignore = new Dictionary(); - var resolved = new List(PluginsMetadata.Count); - foreach (var meta in PluginsMetadata) - { - if (meta.Id != null) - { - if (ids.Contains(meta.Id)) - { - Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); - var ireason = new IgnoreReason(Reason.Duplicate) - { - ReasonText = $"Duplicate entry of same ID ({meta.Id})", - RelatedTo = resolved.First(p => p.Id == meta.Id) - }; - ignore.Add(meta, ireason); - ignoredPlugins.Add(meta, ireason); - continue; // because of sorted order, hightest order will always be the first one - } - - bool processedLater = false; - foreach (var meta2 in PluginsMetadata) - { - if (ignore.ContainsKey(meta2)) continue; - if (meta == meta2) - { - processedLater = true; - continue; - } - - if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue; - - var range = meta2.Manifest.Conflicts[meta.Id]; - if (!range.IsSatisfied(meta.Version)) continue; - - Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}"); - - if (processedLater) - { - Logger.loader.Warn($"Ignoring {meta2.Name}"); - ignore.Add(meta2, new IgnoreReason(Reason.Conflict) - { - ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}", - RelatedTo = meta - }); - } - else - { - Logger.loader.Warn($"Ignoring {meta.Name}"); - ignore.Add(meta, new IgnoreReason(Reason.Conflict) - { - ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}", - RelatedTo = meta2 - }); - break; - } - } - } - - if (ignore.TryGetValue(meta, out var reason)) - { - ignoredPlugins.Add(meta, reason); - continue; - } - if (meta.Id != null) - ids.Add(meta.Id); - - resolved.Add(meta); - } - - PluginsMetadata = resolved; - } - - private static void FilterDisabled() - { - var enabled = new List(PluginsMetadata.Count); - - var disabled = DisabledConfig.Instance.DisabledModIds; - foreach (var meta in PluginsMetadata) - { - if (disabled.Contains(meta.Id ?? meta.Name)) - DisabledPlugins.Add(meta); - else - enabled.Add(meta); - } - - PluginsMetadata = enabled; - } - - internal static void ComputeLoadOrder() - { -#if DEBUG - Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); -#endif - - static bool InsertInto(HashSet root, PluginMetadata meta, bool isRoot = false) - { // this is slow, and hella recursive - bool inserted = false; - foreach (var sr in root) - { - inserted = inserted || InsertInto(sr.Dependencies, meta); - - if (meta.Id != null) - if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id)) - inserted = inserted || sr.Dependencies.Add(meta); - if (sr.Id != null) - if (meta.Manifest.LoadBefore.Contains(sr.Id)) - inserted = inserted || sr.Dependencies.Add(meta); - } - - if (isRoot) - { - foreach (var sr in root) - { - InsertInto(meta.Dependencies, sr); - - if (sr.Id != null) - if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id)) - meta.Dependencies.Add(sr); - if (meta.Id != null) - if (sr.Manifest.LoadBefore.Contains(meta.Id)) - meta.Dependencies.Add(sr); - } - - root.Add(meta); - } - - return inserted; - } - - var pluginTree = new HashSet(); - foreach (var meta in PluginsMetadata) - InsertInto(pluginTree, meta, true); - - static void DeTree(List into, HashSet tree) - { - foreach (var st in tree) - if (!into.Contains(st)) - { - DeTree(into, st.Dependencies); - into.Add(st); - } - } - - PluginsMetadata = new List(); - DeTree(PluginsMetadata, pluginTree); - -#if DEBUG - Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); -#endif - } - - internal static void ResolveDependencies() - { - var metadata = new List(); - var pluginsToLoad = new Dictionary(); - var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version); - foreach (var meta in PluginsMetadata) - { - var missingDeps = new List<(string id, Range version, bool disabled)>(); - foreach (var dep in meta.Manifest.Dependencies) - { -#if DEBUG - Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}"); -#endif - if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) - continue; - - if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key])) - { - Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too."); - missingDeps.Add((dep.Key, dep.Value, true)); - } - else - { - Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); - missingDeps.Add((dep.Key, dep.Value, false)); - } - } - - if (missingDeps.Count == 0) - { - metadata.Add(meta); - if (meta.Id != null) - pluginsToLoad.Add(meta.Id, meta.Version); - } - else if (missingDeps.Any(t => !t.disabled)) - { // missing deps - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) - { - ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}" - }); - } - else - { - DisabledPlugins.Add(meta); - DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name); - } - } - - PluginsMetadata = metadata; - } - - internal static void InitFeatures() - { - var parsedFeatures = PluginsMetadata.Select(m => - (metadata: m, - features: m.Manifest.Features.Select(feature => - (feature, parsed: Ref.Create(null)) - ).ToList() - ) - ).ToList(); - - while (DefineFeature.NewFeature) - { - DefineFeature.NewFeature = false; - - foreach (var (metadata, features) in parsedFeatures) - for (var i = 0; i < features.Count; i++) - { - var feature = features[i]; - - var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj, - out var exception, out var valid, out var parsed, feature.parsed.Value); - - if (!success && !valid && featureObj == null && exception == null) // no feature of type found - feature.parsed.Value = parsed; - else if (success) - { - if (valid && featureObj.StoreOnPlugin) - metadata.InternalFeatures.Add(featureObj); - else if (!valid) - Logger.features.Warn( - $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}"); - features.RemoveAt(i--); - } - else - { - Logger.features.Error($"Error parsing feature definition on {metadata.Name}"); - Logger.features.Error(exception); - features.RemoveAt(i--); - } - } - - foreach (var plugin in PluginsMetadata) - foreach (var feature in plugin.Features) - feature.Evaluate(); - } - - foreach (var plugin in parsedFeatures) - { - if (plugin.features.Count <= 0) continue; - - Logger.features.Warn($"On plugin {plugin.metadata.Name}:"); - foreach (var feature in plugin.features) - Logger.features.Warn($" Feature not found with name {feature.feature}"); - } - } - - internal static void ReleaseAll(bool full = false) - { - if (full) - ignoredPlugins = new Dictionary(); - else - { - foreach (var m in PluginsMetadata) - ignoredPlugins.Add(m, new IgnoreReason(Reason.Released)); - foreach (var m in ignoredPlugins.Keys) - { // clean them up so we can still use the metadata for updates - m.InternalFeatures.Clear(); - m.PluginType = null; - m.Assembly = null; - } - } - PluginsMetadata = new List(); - DisabledPlugins = new List(); - Feature.Reset(); - GC.Collect(); - } - - internal static void Load(PluginMetadata meta) - { - if (meta.Assembly == null && meta.PluginType != null) - meta.Assembly = Assembly.LoadFrom(meta.File.FullName); - } - - internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) - { - if (meta.PluginType == null) - return new PluginInfo() - { - Metadata = meta, - Plugin = null - }; - - var info = new PluginInfo(); - - if (meta.Manifest.GameVersion != BeatSaber.GameVersion) - Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); - - try - { - foreach (var dep in meta.Dependencies) - { - if (alreadyLoaded.Contains(dep)) continue; - - // otherwise... - - if (ignoredPlugins.TryGetValue(dep, out var reason)) - { // was added to the ignore list - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) - { - ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}", - RelatedTo = dep - }); - } - else - { // was not added to ignore list - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) - { - ReasonText = $"Dependency was not already loaded at load time, but was also not ignored", - RelatedTo = dep - }); - } - - return null; - } - - Load(meta); - - Feature denyingFeature = null; - if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta))) - { - Logger.loader.Warn( - $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}"); - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) - { - ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" - }); - return null; - } - - var type = meta.Assembly.GetType(meta.PluginType.FullName); - var instance = Activator.CreateInstance(type) as IPlugin; - - info.Metadata = meta; - info.Plugin = instance; - - var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public); - if (init != null) - { - denyingFeature = null; - if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info))) - { - Logger.loader.Warn( - $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}"); - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) - { - ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" - }); - return null; - } - - PluginInitInjector.Inject(init, info); - } - - foreach (var feature in meta.Features) - try - { - feature.AfterInit(info, info.Plugin); - } - catch (Exception e) - { - Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}"); - } - - /*try // TODO: move this out to after all plugins have been inited - { - instance.OnEnable(); - } - catch (Exception e) - { - Logger.loader.Error($"Error occurred trying to enable {meta.Name}"); - Logger.loader.Error(e); - return null; // is enable failure a full load failure? - }*/ - } - catch (AmbiguousMatchException) - { - Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})"); - Logger.loader.Critical("@Developer: you *really* should fix this"); - // not adding to ignoredPlugins here because this should only happen in a development context - // if someone fucks this up on release thats on them - return null; - } - catch (Exception e) - { - Logger.loader.Error($"Could not init plugin {meta.Name}: {e}"); - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error) - { - ReasonText = "Error ocurred while initializing", - Error = e - }); - return null; - } - - return info; - } - - internal static List LoadPlugins() - { - InitFeatures(); - DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily - - var list = new List(); - var loaded = new HashSet(); - foreach (var meta in PluginsMetadata) - { - var info = InitPlugin(meta, loaded); - if (info != null) - { - list.Add(info); - loaded.Add(meta); - } - } - - return list; - } - } +using IPA.Config; +using IPA.Loader.Features; +using IPA.Logging; +using IPA.Utilities; +using Mono.Cecil; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Version = SemVer.Version; +using SemVer; +#if NET4 +using Task = System.Threading.Tasks.Task; +using TaskEx = System.Threading.Tasks.Task; +#endif +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +using Directory = Net3_Proxy.Directory; +#endif + +namespace IPA.Loader +{ + /// + /// A type to manage the loading of plugins. + /// + public class PluginLoader + { + internal static Task LoadTask() => + TaskEx.Run(() => + { + YeetIfNeeded(); + + LoadMetadata(); + Resolve(); + ComputeLoadOrder(); + FilterDisabled(); + + ResolveDependencies(); + }); + + /// + /// A class which describes a loaded plugin. + /// + public class PluginMetadata + { + /// + /// The assembly the plugin was loaded from. + /// + /// the loaded Assembly that contains the plugin main type + public Assembly Assembly { get; internal set; } + + /// + /// The TypeDefinition for the main type of the plugin. + /// + /// the Cecil definition for the plugin main type + public TypeDefinition PluginType { get; internal set; } + + /// + /// The human readable name of the plugin. + /// + /// the name of the plugin + public string Name { get; internal set; } + + /// + /// The BeatMods ID of the plugin, or null if it doesn't have one. + /// + /// the updater ID of the plugin + public string Id { get; internal set; } + + /// + /// The version of the plugin. + /// + /// the version of the plugin + public Version Version { get; internal set; } + + /// + /// The file the plugin was loaded from. + /// + /// the file the plugin was loaded from + public FileInfo File { get; internal set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + /// + /// The features this plugin requests. + /// + /// the list of features requested by the plugin + public IReadOnlyList Features => InternalFeatures; + + internal readonly List InternalFeatures = new List(); + + internal bool IsSelf; + + /// + /// Whether or not this metadata object represents a bare manifest. + /// + /// if it is bare, otherwise + public bool IsBare { get; internal set; } + + private PluginManifest manifest; + + internal HashSet Dependencies { get; } = new HashSet(); + + internal PluginManifest Manifest + { + get => manifest; + set + { + manifest = value; + Name = value.Name; + Version = value.Version; + Id = value.Id; + } + } + + public RuntimeOptions RuntimeOptions { get; internal set; } + public bool IsAttributePlugin { get; internal set; } = false; + + /// + /// Gets all of the metadata as a readable string. + /// + /// the readable printable metadata string + public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; + } + + /// + /// A container object for all the data relating to a plugin. + /// + public class PluginInfo + { + internal IPlugin Plugin { get; set; } + + /// + /// Metadata for the plugin. + /// + /// the metadata for this plugin + public PluginMetadata Metadata { get; internal set; } = new PluginMetadata(); + } + + internal static void YeetIfNeeded() + { + string pluginDir = BeatSaber.PluginsPath; + + if (SelfConfig.YeetMods_ && BeatSaber.IsGameVersionBoundary) + { + var oldPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.OldVersion} Plugins"); + var newPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {BeatSaber.GameVersion} Plugins"); + + if (Directory.Exists(oldPluginsName)) + Directory.Delete(oldPluginsName, true); + Directory.Move(pluginDir, oldPluginsName); + if (Directory.Exists(newPluginsName)) + Directory.Move(newPluginsName, pluginDir); + else + Directory.CreateDirectory(pluginDir); + } + } + + internal static List PluginsMetadata = new List(); + internal static List DisabledPlugins = new List(); + + private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline); + + internal static void LoadMetadata() + { + string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll"); + + try + { + var selfMeta = new PluginMetadata + { + Assembly = Assembly.GetExecutingAssembly(), + File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")), + PluginType = null, + IsSelf = true + }; + + string manifest; + using (var manifestReader = + new StreamReader( + selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ?? + throw new InvalidOperationException())) + manifest = manifestReader.ReadToEnd(); + + selfMeta.Manifest = JsonConvert.DeserializeObject(manifest); + + PluginsMetadata.Add(selfMeta); + } + catch (Exception e) + { + Logger.loader.Critical("Error loading own manifest"); + Logger.loader.Critical(e); + } + + foreach (var plugin in plugins) + { + var metadata = new PluginMetadata + { + File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)), + IsSelf = false + }; + + try + { + var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters + { + ReadingMode = ReadingMode.Immediate, + ReadWrite = false, + AssemblyResolver = new CecilLibLoader() + }).MainModule; + + string pluginNs = ""; + + foreach (var resource in pluginModule.Resources) + { + const string manifestSuffix = ".manifest.json"; + if (!(resource is EmbeddedResource embedded) || + !embedded.Name.EndsWith(manifestSuffix)) continue; + + pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length); + + string manifest; + using (var manifestReader = new StreamReader(embedded.GetResourceStream())) + manifest = manifestReader.ReadToEnd(); + + metadata.Manifest = JsonConvert.DeserializeObject(manifest); + break; + } + + if (metadata.Manifest == null) + { +#if DIRE_LOADER_WARNINGS + Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}"); +#else + Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}"); +#endif + continue; + } + + void TryGetNamespacedPluginType(string ns, PluginMetadata meta) + { + foreach (var type in pluginModule.Types) + { + if (type.Namespace != ns) continue; + + if (type.HasCustomAttributes) + { + var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName); + if (attr != null) + { + if (!attr.HasConstructorArguments) + { + Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments"); + return; + } + + var args = attr.ConstructorArguments; + if (args.Count != 1) + { + Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments"); + return; + } + var rtOptionsArg = args[0]; + if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName) + { + Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}"); + return; + } + + var val = rtOptionsArg.Value?.GetType(); + Logger.loader.Debug($"value type is {(val == null ? "null" : val.FullName)}"); + + meta.IsAttributePlugin = true; + meta.PluginType = type; + return; + } + } + + if (type.HasInterface(typeof(IPlugin).FullName)) + { + Logger.loader.Warn("Interface-based plugin found"); + meta.RuntimeOptions = RuntimeOptions.SingleDynamicInit; + meta.PluginType = type; + return; + } + } + } + + var hint = metadata.Manifest.Misc?.PluginMainHint; + + if (hint != null) + { + var type = pluginModule.GetType(hint); + if (type != null) + TryGetNamespacedPluginType(hint, metadata); + } + + if (metadata.PluginType == null) + TryGetNamespacedPluginType(pluginNs, metadata); + + if (metadata.PluginType == null) + { + Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}"); + continue; + } + + Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}"); + PluginsMetadata.Add(metadata); + } + catch (Exception e) + { + Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); + Logger.loader.Error(e); + ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error) + { + ReasonText = "An error ocurred loading the data", + Error = e + }); + } + } + + IEnumerable bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json"); + bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest")); + foreach (var manifest in bareManifests) + { // TODO: maybe find a way to allow a bare manifest to specify an associated file + try + { + var metadata = new PluginMetadata + { + File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)), + IsSelf = false, + IsBare = true, + }; + + metadata.Manifest = JsonConvert.DeserializeObject(File.ReadAllText(manifest)); + + Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}"); + PluginsMetadata.Add(metadata); + } + catch (Exception e) + { + Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}"); + Logger.loader.Error(e); + } + } + + foreach (var meta in PluginsMetadata) + { // process description include + var lines = meta.Manifest.Description.Split('\n'); + var m = embeddedTextDescriptionPattern.Match(lines[0]); + if (m.Success) + { + if (meta.IsBare) + { + Logger.loader.Warn($"Bare manifest cannot specify description file"); + meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line + continue; + } + + var name = m.Groups[1].Value; + string description; + if (!meta.IsSelf) + { + var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource) + .NonNull() + .FirstOrDefault(r => r.Name == name); + if (resc == null) + { + Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include"); + meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line + continue; + } + + using var reader = new StreamReader(resc.GetResourceStream()); + description = reader.ReadToEnd(); + } + else + { + using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name)); + description = descriptionReader.ReadToEnd(); + } + + meta.Manifest.Description = description; + } + } + } + + internal enum Reason + { + Error, Duplicate, Conflict, Dependency, + Released, Feature, Unsupported + } + internal struct IgnoreReason + { + + public Reason Reason { get; } + public string ReasonText { get; set; } + public Exception Error { get; set; } + public PluginMetadata RelatedTo { get; set; } + public IgnoreReason(Reason reason) + { + Reason = reason; + ReasonText = null; + Error = null; + RelatedTo = null; + } + } + + // keep track of these for the updater; it should still be able to update mods not loaded + // the thing -> the reason + internal static Dictionary ignoredPlugins = new Dictionary(); + + internal static void Resolve() + { // resolves duplicates and conflicts, etc + PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version)); + + var ids = new HashSet(); + var ignore = new Dictionary(); + var resolved = new List(PluginsMetadata.Count); + foreach (var meta in PluginsMetadata) + { + if (meta.Id != null) + { + if (ids.Contains(meta.Id)) + { + Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); + var ireason = new IgnoreReason(Reason.Duplicate) + { + ReasonText = $"Duplicate entry of same ID ({meta.Id})", + RelatedTo = resolved.First(p => p.Id == meta.Id) + }; + ignore.Add(meta, ireason); + ignoredPlugins.Add(meta, ireason); + continue; // because of sorted order, hightest order will always be the first one + } + + bool processedLater = false; + foreach (var meta2 in PluginsMetadata) + { + if (ignore.ContainsKey(meta2)) continue; + if (meta == meta2) + { + processedLater = true; + continue; + } + + if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue; + + var range = meta2.Manifest.Conflicts[meta.Id]; + if (!range.IsSatisfied(meta.Version)) continue; + + Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}"); + + if (processedLater) + { + Logger.loader.Warn($"Ignoring {meta2.Name}"); + ignore.Add(meta2, new IgnoreReason(Reason.Conflict) + { + ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}", + RelatedTo = meta + }); + } + else + { + Logger.loader.Warn($"Ignoring {meta.Name}"); + ignore.Add(meta, new IgnoreReason(Reason.Conflict) + { + ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}", + RelatedTo = meta2 + }); + break; + } + } + } + + if (ignore.TryGetValue(meta, out var reason)) + { + ignoredPlugins.Add(meta, reason); + continue; + } + if (meta.Id != null) + ids.Add(meta.Id); + + resolved.Add(meta); + } + + PluginsMetadata = resolved; + } + + private static void FilterDisabled() + { + var enabled = new List(PluginsMetadata.Count); + + var disabled = DisabledConfig.Instance.DisabledModIds; + foreach (var meta in PluginsMetadata) + { + if (disabled.Contains(meta.Id ?? meta.Name)) + DisabledPlugins.Add(meta); + else + enabled.Add(meta); + } + + PluginsMetadata = enabled; + } + + internal static void ComputeLoadOrder() + { +#if DEBUG + Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); +#endif + + static bool InsertInto(HashSet root, PluginMetadata meta, bool isRoot = false) + { // this is slow, and hella recursive + bool inserted = false; + foreach (var sr in root) + { + inserted = inserted || InsertInto(sr.Dependencies, meta); + + if (meta.Id != null) + if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id)) + inserted = inserted || sr.Dependencies.Add(meta); + if (sr.Id != null) + if (meta.Manifest.LoadBefore.Contains(sr.Id)) + inserted = inserted || sr.Dependencies.Add(meta); + } + + if (isRoot) + { + foreach (var sr in root) + { + InsertInto(meta.Dependencies, sr); + + if (sr.Id != null) + if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id)) + meta.Dependencies.Add(sr); + if (meta.Id != null) + if (sr.Manifest.LoadBefore.Contains(meta.Id)) + meta.Dependencies.Add(sr); + } + + root.Add(meta); + } + + return inserted; + } + + var pluginTree = new HashSet(); + foreach (var meta in PluginsMetadata) + InsertInto(pluginTree, meta, true); + + static void DeTree(List into, HashSet tree) + { + foreach (var st in tree) + if (!into.Contains(st)) + { + DeTree(into, st.Dependencies); + into.Add(st); + } + } + + PluginsMetadata = new List(); + DeTree(PluginsMetadata, pluginTree); + +#if DEBUG + Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); +#endif + } + + internal static void ResolveDependencies() + { + var metadata = new List(); + var pluginsToLoad = new Dictionary(); + var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version); + foreach (var meta in PluginsMetadata) + { + var missingDeps = new List<(string id, Range version, bool disabled)>(); + foreach (var dep in meta.Manifest.Dependencies) + { +#if DEBUG + Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}"); +#endif + if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) + continue; + + if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key])) + { + Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too."); + missingDeps.Add((dep.Key, dep.Value, true)); + } + else + { + Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); + missingDeps.Add((dep.Key, dep.Value, false)); + } + } + + if (missingDeps.Count == 0) + { + metadata.Add(meta); + if (meta.Id != null) + pluginsToLoad.Add(meta.Id, meta.Version); + } + else if (missingDeps.Any(t => !t.disabled)) + { // missing deps + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}" + }); + } + else + { + DisabledPlugins.Add(meta); + DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name); + } + } + + PluginsMetadata = metadata; + } + + internal static void InitFeatures() + { + var parsedFeatures = PluginsMetadata.Select(m => + (metadata: m, + features: m.Manifest.Features.Select(feature => + (feature, parsed: Ref.Create(null)) + ).ToList() + ) + ).ToList(); + + while (DefineFeature.NewFeature) + { + DefineFeature.NewFeature = false; + + foreach (var (metadata, features) in parsedFeatures) + for (var i = 0; i < features.Count; i++) + { + var feature = features[i]; + + var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj, + out var exception, out var valid, out var parsed, feature.parsed.Value); + + if (!success && !valid && featureObj == null && exception == null) // no feature of type found + feature.parsed.Value = parsed; + else if (success) + { + if (valid && featureObj.StoreOnPlugin) + metadata.InternalFeatures.Add(featureObj); + else if (!valid) + Logger.features.Warn( + $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}"); + features.RemoveAt(i--); + } + else + { + Logger.features.Error($"Error parsing feature definition on {metadata.Name}"); + Logger.features.Error(exception); + features.RemoveAt(i--); + } + } + + foreach (var plugin in PluginsMetadata) + foreach (var feature in plugin.Features) + feature.Evaluate(); + } + + foreach (var plugin in parsedFeatures) + { + if (plugin.features.Count <= 0) continue; + + Logger.features.Warn($"On plugin {plugin.metadata.Name}:"); + foreach (var feature in plugin.features) + Logger.features.Warn($" Feature not found with name {feature.feature}"); + } + } + + internal static void ReleaseAll(bool full = false) + { + if (full) + ignoredPlugins = new Dictionary(); + else + { + foreach (var m in PluginsMetadata) + ignoredPlugins.Add(m, new IgnoreReason(Reason.Released)); + foreach (var m in ignoredPlugins.Keys) + { // clean them up so we can still use the metadata for updates + m.InternalFeatures.Clear(); + m.PluginType = null; + m.Assembly = null; + } + } + PluginsMetadata = new List(); + DisabledPlugins = new List(); + Feature.Reset(); + GC.Collect(); + } + + internal static void Load(PluginMetadata meta) + { + if (meta.Assembly == null && meta.PluginType != null) + meta.Assembly = Assembly.LoadFrom(meta.File.FullName); + } + + internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) + { + if (meta.IsAttributePlugin) + { + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Attribute plugins are currently not supported" }); + return null; + } + + if (meta.PluginType == null) + return new PluginInfo() + { + Metadata = meta, + Plugin = null + }; + + var info = new PluginInfo(); + + if (meta.Manifest.GameVersion != BeatSaber.GameVersion) + Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); + + try + { + foreach (var dep in meta.Dependencies) + { + if (alreadyLoaded.Contains(dep)) continue; + + // otherwise... + + if (ignoredPlugins.TryGetValue(dep, out var reason)) + { // was added to the ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}", + RelatedTo = dep + }); + } + else + { // was not added to ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was not already loaded at load time, but was also not ignored", + RelatedTo = dep + }); + } + + return null; + } + + Load(meta); + + Feature denyingFeature = null; + if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta))) + { + Logger.loader.Warn( + $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}"); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" + }); + return null; + } + + var type = meta.Assembly.GetType(meta.PluginType.FullName); + var instance = Activator.CreateInstance(type) as IPlugin; + + info.Metadata = meta; + info.Plugin = instance; + + var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public); + if (init != null) + { + denyingFeature = null; + if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info))) + { + Logger.loader.Warn( + $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}"); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" + }); + return null; + } + + PluginInitInjector.Inject(init, info); + } + + foreach (var feature in meta.Features) + try + { + feature.AfterInit(info, info.Plugin); + } + catch (Exception e) + { + Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}"); + } + + /*try // TODO: move this out to after all plugins have been inited + { + instance.OnEnable(); + } + catch (Exception e) + { + Logger.loader.Error($"Error occurred trying to enable {meta.Name}"); + Logger.loader.Error(e); + return null; // is enable failure a full load failure? + }*/ + } + catch (AmbiguousMatchException) + { + Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})"); + Logger.loader.Critical("@Developer: you *really* should fix this"); + // not adding to ignoredPlugins here because this should only happen in a development context + // if someone fucks this up on release thats on them + return null; + } + catch (Exception e) + { + Logger.loader.Error($"Could not init plugin {meta.Name}: {e}"); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error) + { + ReasonText = "Error ocurred while initializing", + Error = e + }); + return null; + } + + return info; + } + + internal static List LoadPlugins() + { + InitFeatures(); + DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily + + var list = new List(); + var loaded = new HashSet(); + foreach (var meta in PluginsMetadata) + { + var info = InitPlugin(meta, loaded); + if (info != null) + { + list.Add(info); + loaded.Add(meta); + } + } + + return list; + } + } } \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index 4ac57519..d0eba0f4 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -1,70 +1,80 @@ -using IPA.JsonConverters; -using IPA.Utilities; -using Newtonsoft.Json; -using SemVer; -using System; -using System.Collections.Generic; -using AlmostVersionConverter = IPA.JsonConverters.AlmostVersionConverter; -using Version = SemVer.Version; -#if NET3 -using Net3_Proxy; -using Array = Net3_Proxy.Array; -#endif - -namespace IPA.Loader -{ - internal class PluginManifest - { - [JsonProperty("name", Required = Required.Always)] - public string Name; - - [JsonProperty("id", Required = Required.AllowNull)] - public string Id; - - [JsonProperty("description", Required = Required.Always), JsonConverter(typeof(MultilineStringConverter))] - public string Description; - - [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] - public Version Version; - - [JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(AlmostVersionConverter))] - public AlmostVersion GameVersion; - - [JsonProperty("author", Required = Required.Always)] - public string Author; - - [JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Dependencies = new Dictionary(); - - [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Conflicts = new Dictionary(); - - [JsonProperty("features", Required = Required.DisallowNull)] - public string[] Features = Array.Empty(); - - [JsonProperty("loadBefore", Required = Required.DisallowNull)] - public string[] LoadBefore = Array.Empty(); - - [JsonProperty("loadAfter", Required = Required.DisallowNull)] - public string[] LoadAfter = Array.Empty(); - - [JsonProperty("icon", Required = Required.DisallowNull)] - public string IconPath = null; - - [Serializable] - public class LinksObject - { - [JsonProperty("project-home", Required = Required.DisallowNull)] - public Uri ProjectHome = null; - - [JsonProperty("project-source", Required = Required.DisallowNull)] - public Uri ProjectSource = null; - - [JsonProperty("donate", Required = Required.DisallowNull)] - public Uri Donate = null; - } - - [JsonProperty("links", Required = Required.DisallowNull)] - public LinksObject Links = null; - } +using IPA.JsonConverters; +using IPA.Utilities; +using Newtonsoft.Json; +using SemVer; +using System; +using System.Collections.Generic; +using AlmostVersionConverter = IPA.JsonConverters.AlmostVersionConverter; +using Version = SemVer.Version; +#if NET3 +using Net3_Proxy; +using Array = Net3_Proxy.Array; +#endif + +namespace IPA.Loader +{ + internal class PluginManifest + { + [JsonProperty("name", Required = Required.Always)] + public string Name; + + [JsonProperty("id", Required = Required.AllowNull)] + public string Id; + + [JsonProperty("description", Required = Required.Always), JsonConverter(typeof(MultilineStringConverter))] + public string Description; + + [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] + public Version Version; + + [JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(AlmostVersionConverter))] + public AlmostVersion GameVersion; + + [JsonProperty("author", Required = Required.Always)] + public string Author; + + [JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] + public Dictionary Dependencies = new Dictionary(); + + [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] + public Dictionary Conflicts = new Dictionary(); + + [JsonProperty("features", Required = Required.DisallowNull)] + public string[] Features = Array.Empty(); + + [JsonProperty("loadBefore", Required = Required.DisallowNull)] + public string[] LoadBefore = Array.Empty(); + + [JsonProperty("loadAfter", Required = Required.DisallowNull)] + public string[] LoadAfter = Array.Empty(); + + [JsonProperty("icon", Required = Required.DisallowNull)] + public string IconPath = null; + + [Serializable] + public class LinksObject + { + [JsonProperty("project-home", Required = Required.DisallowNull)] + public Uri ProjectHome = null; + + [JsonProperty("project-source", Required = Required.DisallowNull)] + public Uri ProjectSource = null; + + [JsonProperty("donate", Required = Required.DisallowNull)] + public Uri Donate = null; + } + + [JsonProperty("links", Required = Required.DisallowNull)] + public LinksObject Links = null; + + [Serializable] + public class MiscObject + { + [JsonProperty("plugin-hint", Required = Required.DisallowNull)] + public string PluginMainHint = null; + } + + [JsonProperty("misc", Required = Required.DisallowNull)] + public MiscObject Misc = null; + } } \ No newline at end of file diff --git a/IPA.Loader/PluginInterfaces/Attributes/PluginAttribute.cs b/IPA.Loader/PluginInterfaces/Attributes/PluginAttribute.cs index 0cb8385f..024c7db6 100644 --- a/IPA.Loader/PluginInterfaces/Attributes/PluginAttribute.cs +++ b/IPA.Loader/PluginInterfaces/Attributes/PluginAttribute.cs @@ -9,6 +9,7 @@ namespace IPA [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class PluginAttribute : Attribute { + // whenever this changes, PluginLoader.LoadMetadata must also change public RuntimeOptions RuntimeOptions { get; } public PluginAttribute(RuntimeOptions runtimeOptions) {