diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj index 117be32f..306b9131 100644 --- a/IPA.Injector/IPA.Injector.csproj +++ b/IPA.Injector/IPA.Injector.csproj @@ -89,6 +89,9 @@ Libraries\Mono\System.Runtime.Serialization.dll Always + + Always + diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 1cf1f244..0e296c07 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -7,6 +7,7 @@ using System; using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using UnityEngine; using static IPA.Logging.Logger; using MethodAttributes = Mono.Cecil.MethodAttributes; @@ -16,6 +17,8 @@ namespace IPA.Injector // ReSharper disable once UnusedMember.Global public static class Injector { + private static Task pluginAsyncLoadTask; + // ReSharper disable once UnusedParameter.Global public static void Main(string[] args) { // entry point for doorstop @@ -36,6 +39,8 @@ namespace IPA.Injector InstallBootstrapPatch(); Updates.InstallPendingUpdates(); + + pluginAsyncLoadTask = PluginLoader.LoadTask(); } catch (Exception e) { @@ -170,28 +175,16 @@ namespace IPA.Injector { if (_loadingDone) return; _loadingDone = true; - #region Add Library load locations AppDomain.CurrentDomain.AssemblyResolve += LibLoader.AssemblyLibLoader; - /*try - { - if (!SetDllDirectory(LibLoader.NativeDir)) - { - libLoader.Warn("Unable to add native library path to load path"); - } - } - catch (Exception) { }*/ - #endregion } - -/* - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool SetDllDirectory(string lpPathName); -*/ - + private static void Bootstrapper_Destroyed() { - PluginComponent.Create(); + // wait for plugins to finish loading + pluginAsyncLoadTask.Wait(); + log.Debug("Plugins loaded"); + log.Debug(string.Join(", ", PluginLoader.PluginsMetadata)); + //PluginComponent.Create(); } } } diff --git a/IPA.Injector/Libraries/Mono/Mono.Debugger.Soft.dll b/IPA.Injector/Libraries/Mono/Mono.Debugger.Soft.dll new file mode 100644 index 00000000..0467709c Binary files /dev/null and b/IPA.Injector/Libraries/Mono/Mono.Debugger.Soft.dll differ diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index beceabed..4c2551fc 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -60,6 +60,7 @@ + @@ -71,9 +72,9 @@ - - - + + + diff --git a/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs b/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs new file mode 100644 index 00000000..18cfa2e0 --- /dev/null +++ b/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs @@ -0,0 +1,25 @@ +using System; +using IPA.Updating.ModSaber; +using Newtonsoft.Json; +using SemVer; + +namespace IPA.JsonConverters +{ + internal class ModSaberDependencyConverter : JsonConverter + { + public override ApiEndpoint.Mod.Dependency ReadJson(JsonReader reader, Type objectType, ApiEndpoint.Mod.Dependency existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var parts = (reader.Value as string)?.Split('@'); + return new ApiEndpoint.Mod.Dependency + { + Name = parts?[0], + VersionRange = new Range(parts?[1]) + }; + } + + public override void WriteJson(JsonWriter writer, ApiEndpoint.Mod.Dependency value, JsonSerializer serializer) + { + writer.WriteValue($"{value.Name}@{value.VersionRange}"); + } + } +} diff --git a/IPA.Loader/Updating/Converters/SemverRangeConverter.cs b/IPA.Loader/JsonConverters/SemverRangeConverter.cs similarity index 87% rename from IPA.Loader/Updating/Converters/SemverRangeConverter.cs rename to IPA.Loader/JsonConverters/SemverRangeConverter.cs index 08e837df..f570b4af 100644 --- a/IPA.Loader/Updating/Converters/SemverRangeConverter.cs +++ b/IPA.Loader/JsonConverters/SemverRangeConverter.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using SemVer; -using System; +using System; using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using SemVer; -namespace IPA.Updating.Converters +namespace IPA.JsonConverters { [SuppressMessage("ReSharper", "UnusedMember.Global")] internal class SemverRangeConverter : JsonConverter diff --git a/IPA.Loader/Updating/Converters/SemverVersionConverter.cs b/IPA.Loader/JsonConverters/SemverVersionConverter.cs similarity index 73% rename from IPA.Loader/Updating/Converters/SemverVersionConverter.cs rename to IPA.Loader/JsonConverters/SemverVersionConverter.cs index 751a89ae..50e13791 100644 --- a/IPA.Loader/Updating/Converters/SemverVersionConverter.cs +++ b/IPA.Loader/JsonConverters/SemverVersionConverter.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json; -using System; +using System; +using Newtonsoft.Json; using Version = SemVer.Version; -namespace IPA.Updating.Converters +namespace IPA.JsonConverters { internal class SemverVersionConverter : JsonConverter { - public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string); + public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string, true); public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) => writer.WriteValue(value.ToString()); } diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index 18575ecc..d7734bbb 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -1,20 +1,256 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Reflection; +using System.Threading.Tasks; +using IPA.Logging; +using IPA.Utilities; +using Newtonsoft.Json; using Version = SemVer.Version; namespace IPA.Loader { - internal class PluginLoader + /// + /// A type to manage the loading of plugins. + /// + public class PluginLoader { + /// + /// The directory to load plugins from. + /// + public static string PluginsDirectory => Path.Combine(BeatSaber.InstallPath, "Plugins"); + + internal static Task LoadTask() => Task.Run(() => + { + LoadMetadata(); + Resolve(); + ComputeLoadOrder(); + }); + + /// + /// A class which describes + /// public class PluginMetadata { - public Assembly Assembly; - public Type PluginType; - public string Name; - public Version Version; + // ReSharper disable once UnusedAutoPropertyAccessor.Global + /// + /// The assembly the plugin was loaded from. + /// + public Assembly Assembly { get; internal set; } + /// + /// The Type that is the main type for the plugin. + /// + public Type PluginType { get; internal set; } + /// + /// The human readable name of the plugin. + /// + public string Name { get; internal set; } + /// + /// The ModSaber ID of the plugin, or null if it doesn't have one. + /// + public string Id { get; internal set; } + /// + /// The version of the plugin. + /// + public Version Version { get; internal set; } + /// + /// The file the plugin was loaded from. + /// + public FileInfo File { get; internal set; } + // ReSharper disable once UnusedAutoPropertyAccessor.Global + /// + /// The features this plugin requests. + /// + public string[] Features { get; internal set; } + + private PluginManifest manifest; + internal PluginManifest Manifest + { + get => manifest; + set + { + manifest = value; + Name = value.Name; + Version = value.Version; + Id = value.Id; + Features = value.Features; + } + } + + /// + public override string ToString() => $"{Name}({Id}@{Version})({PluginType.AssemblyQualifiedName}) from '{File.Name}'"; + } + + /// + /// A container object for all the data relating to a plugin. + /// + public class PluginInfo + { + internal IBeatSaberPlugin Plugin { get; set; } + internal string Filename { get; set; } + /// + /// Metadata for the plugin. + /// + public PluginMetadata Metadata { get; internal set; } = new PluginMetadata(); + } + + internal static List PluginsMetadata = new List(); + + internal static void LoadMetadata() + { + string[] plugins = Directory.GetFiles(PluginsDirectory, "*.dll"); + + Assembly.ReflectionOnlyLoadFrom(Assembly.GetExecutingAssembly().Location); // load self as reflection only + + foreach (var plugin in plugins) + { // should probably do patching first /shrug + try + { + var metadata = new PluginMetadata(); + + var assembly = Assembly.ReflectionOnlyLoadFrom(plugin); + metadata.Assembly = assembly; + metadata.File = new FileInfo(plugin); + + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + types = e.Types; + } + foreach (var type in types) + { + if (type == null) continue; + + var iInterface = type.GetInterface(nameof(IBeatSaberPlugin)); + if (iInterface == null) continue; + metadata.PluginType = type; + break; + } + + if (metadata.PluginType == null) + { + Logger.log.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}"); + continue; + } + + Stream metadataStream; + try + { + metadataStream = assembly.GetManifestResourceStream(metadata.PluginType, "manifest.json"); + if (metadataStream == null) + { + Logger.log.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}"); + continue; + } + } + catch (FileNotFoundException) + { + Logger.log.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}"); + continue; + } + + string manifest; + using (var manifestReader = new StreamReader(metadataStream)) + manifest = manifestReader.ReadToEnd(); + + metadata.Manifest = JsonConvert.DeserializeObject(manifest); + + PluginsMetadata.Add(metadata); + } + catch (Exception e) + { + Logger.log.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); + Logger.log.Error(e); + } + } + } + + internal static void Resolve() + { // resolves duplicates and conflicts, etc + PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version)); + + var ids = new HashSet(); + var ignore = new HashSet(); + var resolved = new List(PluginsMetadata.Count); + foreach (var meta in PluginsMetadata) + { + if (meta.Id != null) + { + if (ids.Contains(meta.Id)) + { + Logger.log.Warn($"Found duplicates of {meta.Id}, using newest"); + continue; // because of sorted order, hightest order will always be the first one + } + + bool processedLater = false; + foreach (var meta2 in PluginsMetadata) + { + if (ignore.Contains(meta2)) continue; + if (meta == meta2) + { + processedLater = true; + continue; + } + if (meta2.Manifest.Conflicts.ContainsKey(meta.Id)) + { + var range = meta2.Manifest.Conflicts[meta.Id]; + if (range.IsSatisfied(meta.Version)) + { + //TODO: actually choose the one most depended on + + Logger.log.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}"); + + if (processedLater) + { + Logger.log.Warn($"Ignoring {meta2.Name}"); + ignore.Add(meta2); + } + else + { + Logger.log.Warn($"Ignoring {meta.Name}"); + ignore.Add(meta); + break; + } + } + } + } + } + + if (ignore.Contains(meta)) continue; + if (meta.Id != null) ids.Add(meta.Id); + + resolved.Add(meta); + } + + PluginsMetadata = resolved; + } + + internal static void ComputeLoadOrder() + { + PluginsMetadata.Sort((a, b) => + { + if (a.Id == b.Id) return 0; + if (a.Id != null) + { + if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return 1; + if (b.Manifest.LoadBefore.Contains(a.Id)) return -1; + } + if (b.Id != null) + { + if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return -1; + if (a.Manifest.LoadBefore.Contains(b.Id)) return 1; + } + + return 0; + }); } - public static void LoadMetadata() + internal static void LoadPlugins() { } diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 860ad191..79efd65f 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -13,8 +13,10 @@ using IPA.Old; using IPA.Updating; using IPA.Utilities; using Mono.Cecil; +using SemVer; using UnityEngine; using Logger = IPA.Logging.Logger; +using static IPA.Loader.PluginLoader; namespace IPA.Loader { @@ -25,18 +27,7 @@ namespace IPA.Loader { #pragma warning disable CS0618 // Type or member is obsolete (IPlugin) - /// - /// A container object for all the data relating to a plugin. - /// - public class PluginInfo - { - internal IBeatSaberPlugin Plugin { get; set; } - internal string Filename { get; set; } - /// - /// The ModSaber updating info for the mod, or null. - /// - public ModsaberModInfo ModSaberInfo { get; internal set; } - } + /// /// An of new Beat Saber plugins @@ -82,7 +73,7 @@ namespace IPA.Loader /// the plugin info for the requested plugin or null public static PluginInfo GetPluginFromModSaberName(string name) { - return BSMetas.FirstOrDefault(p => p.ModSaberInfo.InternalName == name); + return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); } /// @@ -136,6 +127,7 @@ namespace IPA.Loader string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll"); foreach (string s in originalPlugins) { + if (PluginLoader.PluginsMetadata.Select(m => m.File.Name).Contains(s)) continue; string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s)); #region Fix assemblies for refactor @@ -184,7 +176,16 @@ namespace IPA.Loader Filename = Path.Combine(BeatSaber.InstallPath, "IPA.exe"), Plugin = SelfPlugin.Instance }; - selfPlugin.ModSaberInfo = selfPlugin.Plugin.ModInfo; + selfPlugin.Metadata.Manifest = new PluginManifest + { + Author = "DaNike", + Features = new string[0], + Description = "", + Version = new SemVer.Version(SelfPlugin.IPA_Version), + GameVersion = BeatSaber.GameVersion, + Id = "beatsaber-ipa-reloaded" + }; + selfPlugin.Metadata.File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")); _bsPlugins.Add(selfPlugin); @@ -236,15 +237,15 @@ namespace IPA.Loader try { T pluginInstance = Activator.CreateInstance(t) as T; - string[] filter = null; + /*string[] filter = null; if (typeof(T) == typeof(IPlugin) && pluginInstance is IEnhancedPlugin enhancedPlugin) filter = enhancedPlugin.Filter; else if (pluginInstance is IGenericEnhancedPlugin plugin) - filter = plugin.Filter; + filter = plugin.Filter;*/ - if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase)) - return pluginInstance; + //if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase)) + return pluginInstance; } catch (Exception e) { @@ -310,7 +311,7 @@ namespace IPA.Loader { Plugin = bsPlugin, Filename = file.Replace("\\.cache", ""), // quick and dirty fix - ModSaberInfo = bsPlugin.ModInfo + //ModSaberInfo = bsPlugin.ModInfo }); } catch (AmbiguousMatchException) diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs new file mode 100644 index 00000000..d24e8f52 --- /dev/null +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using IPA.JsonConverters; +using Newtonsoft.Json; +using SemVer; + +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)] + public string Description; + + [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] + public Version Version; + + [JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] + public Version 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.Always)] + public string[] Features; + + [JsonProperty("loadBefore", Required = Required.DisallowNull)] + public string[] LoadBefore = new string[0]; + + [JsonProperty("loadAfter", Required = Required.DisallowNull)] + public string[] LoadAfter = new string[0]; + } +} diff --git a/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs b/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs index 027c3e1f..6a853c96 100644 --- a/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs +++ b/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs @@ -1,4 +1,7 @@ // ReSharper disable CheckNamespace + +using System; + namespace IPA { /// @@ -10,6 +13,7 @@ namespace IPA /// Gets a list of executables this plugin should be executed on (without the file ending) /// /// { "PlayClub", "PlayClubStudio" } + [Obsolete("Ignored.")] string[] Filter { get; } /// diff --git a/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs b/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs deleted file mode 100644 index f5415d3c..00000000 --- a/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Newtonsoft.Json; -using SemVer; -using static IPA.Updating.ModSaber.ApiEndpoint.Mod; - -namespace IPA.Updating.Converters -{ - internal class ModSaberDependencyConverter : JsonConverter - { - public override Dependency ReadJson(JsonReader reader, Type objectType, Dependency existingValue, bool hasExistingValue, JsonSerializer serializer) - { - var parts = (reader.Value as string)?.Split('@'); - return new Dependency - { - Name = parts?[0], - VersionRange = new Range(parts?[1]) - }; - } - - public override void WriteJson(JsonWriter writer, Dependency value, JsonSerializer serializer) - { - writer.WriteValue($"{value.Name}@{value.VersionRange}"); - } - } -} diff --git a/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs b/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs index f941655c..2516da6a 100644 --- a/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs +++ b/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using IPA.Updating.Converters; +using IPA.JsonConverters; using IPA.Utilities; using Newtonsoft.Json; using SemVer; diff --git a/IPA.Loader/Updating/ModSaber/Updater.cs b/IPA.Loader/Updating/ModSaber/Updater.cs index 26307c36..d29b4597 100644 --- a/IPA.Loader/Updating/ModSaber/Updater.cs +++ b/IPA.Loader/Updating/ModSaber/Updater.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Ionic.Zip; +using IPA.Loader; using IPA.Utilities; using Newtonsoft.Json; using SemVer; @@ -62,7 +63,7 @@ namespace IPA.Updating.ModSaber public bool MetaRequestFailed { get; set; } - public PluginInfo LocalPluginMeta { get; set; } + public PluginLoader.PluginInfo LocalPluginMeta { get; set; } public override string ToString() { @@ -169,13 +170,13 @@ namespace IPA.Updating.ModSaber foreach (var plugin in BSMetas) { // initialize with data to resolve (1.1) - if (plugin.ModSaberInfo != null) + if (plugin.Metadata.Id != null) { // updatable - var msinfo = plugin.ModSaberInfo; + var msinfo = plugin.Metadata; depList.Value.Add(new DependencyObject { - Name = msinfo.InternalName, - Version = msinfo.SemverVersion, - Requirement = new Range($">={msinfo.CurrentVersion}"), + Name = msinfo.Id, + Version = msinfo.Version, + Requirement = new Range($">={msinfo.Version}"), LocalPluginMeta = plugin }); } diff --git a/Refs/UnityEngine.CoreModule.dll b/Refs/UnityEngine.CoreModule.dll index a802a5c6..0913c47b 100644 Binary files a/Refs/UnityEngine.CoreModule.dll and b/Refs/UnityEngine.CoreModule.dll differ