private static Tuple <IEnumerable <PluginInfo>, IEnumerable <IPlugin> > LoadPluginsFromFile(string file)
        {
            List <IPlugin> ipaPlugins = new List <IPlugin>();

            if (!File.Exists(file) || !file.EndsWith(".dll", true, null))
            {
                return(new Tuple <IEnumerable <PluginInfo>, IEnumerable <IPlugin> >(null, ipaPlugins));
            }

            T OptionalGetPlugin <T>(Type t) where T : class
            {
                // use typeof() to allow for easier renaming (in an ideal world this compiles to a string, but ¯\_(ツ)_/¯)
                if (t.GetInterface(typeof(T).Name) != null)
                {
                    try
                    {
                        T pluginInstance = Activator.CreateInstance(t) as T;
                        return(pluginInstance);
                    }
                    catch (Exception e)
                    {
                        Logger.loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}");
                    }
                }

                return(null);
            }

            try
            {
                Assembly assembly = Assembly.LoadFrom(file);

                foreach (Type t in assembly.GetTypes())
                {
                    IPlugin ipaPlugin = OptionalGetPlugin <IPlugin>(t);
                    if (ipaPlugin != null)
                    {
                        ipaPlugins.Add(ipaPlugin);
                    }
                }
            }
            catch (ReflectionTypeLoadException e)
            {
                Logger.loader.Error($"Could not load the following types from {Path.GetFileName(file)}:");
                Logger.loader.Error($"  {string.Join("\n  ", e.LoaderExceptions?.Select(e1 => e1?.Message).StrJP() ?? new string[0])}");
            }
            catch (Exception e)
            {
                Logger.loader.Error($"Could not load {Path.GetFileName(file)}!");
                Logger.loader.Error(e);
            }

            return(new Tuple <IEnumerable <PluginInfo>, IEnumerable <IPlugin> >(null, ipaPlugins));
        }
        internal static void LoadMetadata()
        {
            string[] plugins = Directory.GetFiles(UnityGame.PluginsPath, "*.dll");

            try
            {
                var selfMeta = new PluginMetadata
                {
                    Assembly   = Assembly.GetExecutingAssembly(),
                    File       = new FileInfo(Path.Combine(UnityGame.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 <PluginManifest>(manifest);

                PluginsMetadata.Add(selfMeta);
                SelfMeta = selfMeta;
            }
            catch (Exception e)
            {
                Logger.loader.Critical("Error loading own manifest");
                Logger.loader.Critical(e);
            }

            var resolver = new CecilLibLoader();

            resolver.AddSearchDirectory(UnityGame.LibraryPath);
            resolver.AddSearchDirectory(UnityGame.PluginsPath);
            foreach (var plugin in plugins)
            {
                var metadata = new PluginMetadata
                {
                    File   = new FileInfo(Path.Combine(UnityGame.PluginsPath, plugin)),
                    IsSelf = false
                };

                try
                {
                    var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
                    {
                        ReadingMode      = ReadingMode.Immediate,
                        ReadWrite        = false,
                        AssemblyResolver = resolver
                    }).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 <PluginManifest>(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 rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions

                                    meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt;
                                    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 <string> bareManifests = Directory.GetFiles(UnityGame.PluginsPath, "*.json");
            bareManifests = bareManifests.Concat(Directory.GetFiles(UnityGame.PluginsPath, "*.manifest"));
            foreach (var manifest in bareManifests)
            {
                try
                {
                    var metadata = new PluginMetadata
                    {
                        File   = new FileInfo(Path.Combine(UnityGame.PluginsPath, manifest)),
                        IsSelf = false,
                        IsBare = true,
                    };

                    metadata.Manifest = JsonConvert.DeserializeObject <PluginManifest>(File.ReadAllText(manifest));

                    if (metadata.Manifest.Files.Length < 1)
                    {
                        Logger.loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " +
                                           $"Dependency resolution and verification cannot be completed.");
                    }

                    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 static void Load()
        {
            string pluginDirectory = BeatSaber.PluginsPath;

            // Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL,
            // so we need to resort to P/Invoke
            string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath);

            _bsPlugins  = new List <PluginInfo>();
            _ipaPlugins = new List <IPlugin>();

            if (!Directory.Exists(pluginDirectory))
            {
                return;
            }

            string cacheDir = Path.Combine(pluginDirectory, ".cache");

            if (!Directory.Exists(cacheDir))
            {
                Directory.CreateDirectory(cacheDir);
            }
            else
            {
                foreach (string plugin in Directory.GetFiles(cacheDir, "*"))
                {
                    File.Delete(plugin);
                }
            }

            // initialize BSIPA plugins first
            _bsPlugins.AddRange(PluginLoader.LoadPlugins());

            //Copy plugins to .cache
            string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll");
            foreach (string s in originalPlugins)
            {
                if (PluginsMetadata.Select(m => m.File.FullName).Contains(s))
                {
                    continue;
                }
                string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s));

                #region Fix assemblies for refactor

                var module = ModuleDefinition.ReadModule(Path.Combine(pluginDirectory, s));
                foreach (var @ref in module.AssemblyReferences)
                { // fix assembly references
                    if (@ref.Name == "IllusionPlugin" || @ref.Name == "IllusionInjector")
                    {
                        @ref.Name = "IPA.Loader";
                    }
                }

                foreach (var @ref in module.GetTypeReferences())
                { // fix type references
                    if (@ref.FullName == "IllusionPlugin.IPlugin")
                    {
                        @ref.Namespace = "IPA.Old";                                            //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.IEnhancedPlugin")
                    {
                        @ref.Namespace = "IPA.Old";                                                    //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.IBeatSaberPlugin")
                    {
                        @ref.Namespace = "IPA";                                                     //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.IEnhancedBeatSaberPlugin")
                    {
                        @ref.Namespace = "IPA";                                                             //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.BeatSaber.ModsaberModInfo")
                    {
                        @ref.Namespace = "IPA";                                                              //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.IniFile")
                    {
                        @ref.Namespace = "IPA.Config";                                            //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.IModPrefs")
                    {
                        @ref.Namespace = "IPA.Config";                                              //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.ModPrefs")
                    {
                        @ref.Namespace = "IPA.Config";                                             //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.Utils.ReflectionUtil")
                    {
                        @ref.Namespace = "IPA.Utilities";                                                         //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.Logging.Logger")
                    {
                        @ref.Namespace = "IPA.Logging";                                                   //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionPlugin.Logging.LogPrinter")
                    {
                        @ref.Namespace = "IPA.Logging";                                                       //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.PluginManager")
                    {
                        @ref.Namespace = "IPA.Loader";                                                    //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.PluginComponent")
                    {
                        @ref.Namespace = "IPA.Loader";                                                      //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.CompositeBSPlugin")
                    {
                        @ref.Namespace = "IPA.Loader.Composite";                                                        //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.CompositeIPAPlugin")
                    {
                        @ref.Namespace = "IPA.Loader.Composite";                                                         //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.Logging.UnityLogInterceptor")
                    {
                        @ref.Namespace = "IPA.Logging";                                                                  //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.Logging.StandardLogger")
                    {
                        @ref.Namespace = "IPA.Logging";                                                             //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.Updating.SelfPlugin")
                    {
                        @ref.Namespace = "IPA.Updating";                                                          //@ref.Name = "";
                    }
                    if (@ref.FullName == "IllusionInjector.Updating.Backup.BackupUnit")
                    {
                        @ref.Namespace = "IPA.Updating.Backup";                                                                 //@ref.Name = "";
                    }
                    if (@ref.Namespace == "IllusionInjector.Utilities")
                    {
                        @ref.Namespace = "IPA.Utilities";                                                 //@ref.Name = "";
                    }
                    if (@ref.Namespace == "IllusionInjector.Logging.Printers")
                    {
                        @ref.Namespace = "IPA.Logging.Printers";                                                        //@ref.Name = "";
                    }
                    if (@ref.Namespace == "IllusionInjector.Updating.ModsaberML")
                    {
                        @ref.Namespace = "IPA.Updating.ModSaber";                                                           //@ref.Name = "";
                    }
                }
                module.Write(pluginCopy);

                #endregion
            }

            //Load copied plugins
            string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll");
            foreach (string s in copiedPlugins)
            {
                var result = LoadPluginsFromFile(s);
                _ipaPlugins.AddRange(result.Item2);
            }

            Logger.log.Info(exeName);
            Logger.log.Info($"Running on Unity {Application.unityVersion}");
            Logger.log.Info($"Game version {BeatSaber.GameVersion}");
            Logger.log.Info("-----------------------------");
            Logger.log.Info($"Loading plugins from {Utils.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}");
            Logger.log.Info("-----------------------------");
            foreach (var plugin in _bsPlugins)
            {
                Logger.log.Info($"{plugin.Metadata.Name} ({plugin.Metadata.Id}): {plugin.Metadata.Version}");
            }
            Logger.log.Info("-----------------------------");
            foreach (var plugin in _ipaPlugins)
            {
                Logger.log.Info($"{plugin.Name}: {plugin.Version}");
            }
            Logger.log.Info("-----------------------------");
        }