/// <summary>
        /// Processes the ProviderKey bundle attribute.
        /// </summary>
        /// <param name="sourceLineNumbers">Source line number for the parent element.</param>
        /// <param name="parentElement">Parent element of attribute.</param>
        /// <param name="attribute">The XML attribute for the ProviderKey attribute.</param>
        private void ParseProviderKeyAttribute(SourceLineNumberCollection sourceLineNumbers, XmlElement parentElement, XmlAttribute attribute)
        {
            string id          = null;
            string providerKey = null;
            int    illegalChar = -1;

            switch (attribute.LocalName)
            {
            case "ProviderKey":
                providerKey = this.Core.GetAttributeValue(sourceLineNumbers, attribute);
                break;

            default:
                this.Core.UnexpectedAttribute(sourceLineNumbers, attribute);
                break;
            }

            // Make sure the key does not contain any illegal characters or values.
            if (String.IsNullOrEmpty(providerKey))
            {
                this.Core.OnMessage(WixErrors.IllegalEmptyAttributeValue(sourceLineNumbers, parentElement.LocalName, attribute.LocalName));
            }
            else if (0 <= (illegalChar = providerKey.IndexOfAny(DependencyCommon.InvalidCharacters)))
            {
                StringBuilder sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
                Array.ForEach <char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));

                this.Core.OnMessage(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "ProviderKey", providerKey[illegalChar], sb.ToString()));
            }
            else if ("ALL" == providerKey)
            {
                this.Core.OnMessage(DependencyErrors.ReservedValue(sourceLineNumbers, parentElement.LocalName, "ProviderKey", providerKey));
            }

            // Generate the primary key for the row.
            id = this.Core.GenerateIdentifier("dep", attribute.LocalName, providerKey);

            if (!this.Core.EncounteredError)
            {
                // Create the provider row for the bundle. The Component_ field is required
                // in the table definition but unused for bundles, so just set it to the valid ID.
                Row row = this.Core.CreateRow(sourceLineNumbers, "WixDependencyProvider");
                row[0] = id;
                row[1] = id;
                row[2] = providerKey;
                row[5] = DependencyCommon.ProvidesAttributesBundle;
            }
        }
Esempio n. 2
0
        /// <summary>
        /// The entrypoint for the BepInEx plugin system.
        /// </summary>
        public static void Start()
        {
            if (_loaded)
            {
                return;
            }

            if (!_initialized)
            {
                throw new InvalidOperationException("BepInEx has not been initialized. Please call Chainloader.Initialize prior to starting the chainloader instance.");
            }

            if (!Directory.Exists(Paths.PluginPath))
            {
                Directory.CreateDirectory(Paths.PluginPath);
            }

            if (!Directory.Exists(Paths.PatcherPluginPath))
            {
                Directory.CreateDirectory(Paths.PatcherPluginPath);
            }

            try
            {
                var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);

                if (ConsoleManager.ConsoleActive)
                {
                    ConsoleManager.SetConsoleTitle($"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Paths.ProcessName}");
                }

                Logger.LogMessage("Chainloader started");

                ManagerObject = new GameObject("BepInEx_Manager");

                UnityEngine.Object.DontDestroyOnLoad(ManagerObject);

                var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
                foreach (var keyValuePair in pluginsToLoad)
                {
                    foreach (var pluginInfo in keyValuePair.Value)
                    {
                        pluginInfo.Location = keyValuePair.Key;
                    }
                }
                var pluginInfos      = pluginsToLoad.SelectMany(p => p.Value).ToList();
                var loadedAssemblies = new Dictionary <string, Assembly>();

                Logger.LogInfo($"{pluginInfos.Count} plugins to load");

                // We use a sorted dictionary to ensure consistent load order
                var dependencyDict = new SortedDictionary <string, IEnumerable <string> >(StringComparer.InvariantCultureIgnoreCase);
                var pluginsByGUID  = new Dictionary <string, PluginInfo>();

                foreach (var pluginInfoGroup in pluginInfos.GroupBy(info => info.Metadata.GUID))
                {
                    PluginInfo loadedVersion = null;
                    foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
                    {
                        if (loadedVersion != null)
                        {
                            Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})");
                            continue;
                        }

                        loadedVersion = pluginInfo;

                        // Perform checks that will prevent loading plugins in this run
                        var  filters            = pluginInfo.Processes.ToList();
                        bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));

                        if (invalidProcessName)
                        {
                            Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})");
                            continue;
                        }

                        dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
                        pluginsByGUID[pluginInfo.Metadata.GUID]  = pluginInfo;
                    }
                }

                foreach (var pluginInfo in pluginsByGUID.Values.ToList())
                {
                    if (pluginInfo.Incompatibilities.Any(incompatibility => pluginsByGUID.ContainsKey(incompatibility.IncompatibilityGUID)))
                    {
                        pluginsByGUID.Remove(pluginInfo.Metadata.GUID);
                        dependencyDict.Remove(pluginInfo.Metadata.GUID);

                        var    incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID).Where(x => pluginsByGUID.ContainsKey(x)).ToArray();
                        string message             = $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
                        DependencyErrors.Add(message);
                        Logger.LogError(message);
                    }
                    else if (PluginTargetsWrongBepin(pluginInfo))
                    {
                        string message = $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
                        DependencyErrors.Add(message);
                        Logger.LogWarning(message);
                    }
                }

                var emptyDependencies = new string[0];

                // Sort plugins by their dependencies.
                // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list.
                var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys, x => dependencyDict.TryGetValue(x, out var deps) ? deps : emptyDependencies).ToList();

                var invalidPlugins   = new HashSet <string>();
                var processedPlugins = new Dictionary <string, Version>();

                foreach (var pluginGUID in sortedPlugins)
                {
                    // If the plugin is missing, don't process it
                    if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo))
                    {
                        continue;
                    }

                    var dependsOnInvalidPlugin = false;
                    var missingDependencies    = new List <BepInDependency>();
                    foreach (var dependency in pluginInfo.Dependencies)
                    {
                        bool IsHardDependency(BepInDependency dep) => (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;

                        // If the dependency wasn't already processed, it's missing altogether
                        bool dependencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
                        if (!dependencyExists || pluginVersion < dependency.MinimumVersion)
                        {
                            // If the dependency is hard, collect it into a list to show
                            if (IsHardDependency(dependency))
                            {
                                missingDependencies.Add(dependency);
                            }
                            continue;
                        }

                        // If the dependency is invalid (e.g. has missing dependencies) and hard, report that to the user
                        if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
                        {
                            dependsOnInvalidPlugin = true;
                            break;
                        }
                    }

                    processedPlugins.Add(pluginGUID, pluginInfo.Metadata.Version);

                    if (dependsOnInvalidPlugin)
                    {
                        string message = $"Skipping [{pluginInfo}] because it has a dependency that was not loaded. See previous errors for details.";
                        DependencyErrors.Add(message);
                        Logger.LogWarning(message);
                        continue;
                    }

                    if (missingDependencies.Count != 0)
                    {
                        bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0;

                        string message = $@"Could not load [{pluginInfo}] because it has missing dependencies: {
							string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray())
							}"                            ;
                        DependencyErrors.Add(message);
                        Logger.LogError(message);

                        invalidPlugins.Add(pluginGUID);
                        continue;
                    }

                    try
                    {
                        Logger.LogInfo($"Loading [{pluginInfo}]");

                        if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
                        {
                            loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
                        }

                        PluginInfos[pluginGUID] = pluginInfo;
                        pluginInfo.Instance     = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));

                        _plugins.Add(pluginInfo.Instance);
                    }
                    catch (Exception ex)
                    {
                        invalidPlugins.Add(pluginGUID);
                        PluginInfos.Remove(pluginGUID);

                        Logger.LogError($"Error loading [{pluginInfo}] : {ex.Message}");
                        if (ex is ReflectionTypeLoadException re)
                        {
                            Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
                        }
                        else
                        {
                            Logger.LogDebug(ex);
                        }
                    }
                }
            }
        /// <summary>
        /// Processes the Requires element.
        /// </summary>
        /// <param name="node">The XML node for the Requires element.</param>
        /// <param name="providerId">The parent provider identifier.</param>
        /// <param name="requiresAction">Whether the Requires custom action should be referenced.</param>
        private void ParseRequiresElement(XmlNode node, string providerId, bool requiresAction)
        {
            SourceLineNumberCollection sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
            string id          = null;
            string providerKey = null;
            string minVersion  = null;
            string maxVersion  = null;
            int    attributes  = 0;
            int    illegalChar = -1;

            foreach (XmlAttribute attrib in node.Attributes)
            {
                if (0 == attrib.NamespaceURI.Length || attrib.NamespaceURI == this.schema.TargetNamespace)
                {
                    switch (attrib.LocalName)
                    {
                    case "Id":
                        id = this.Core.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
                        break;

                    case "ProviderKey":
                        providerKey = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
                        break;

                    case "Minimum":
                        minVersion = this.Core.GetAttributeVersionValue(sourceLineNumbers, attrib, true);
                        break;

                    case "Maximum":
                        maxVersion = this.Core.GetAttributeVersionValue(sourceLineNumbers, attrib, true);
                        break;

                    case "IncludeMinimum":
                        if (Wix.YesNoType.Yes == this.Core.GetAttributeYesNoValue(sourceLineNumbers, attrib))
                        {
                            attributes |= DependencyCommon.RequiresAttributesMinVersionInclusive;
                        }
                        break;

                    case "IncludeMaximum":
                        if (Wix.YesNoType.Yes == this.Core.GetAttributeYesNoValue(sourceLineNumbers, attrib))
                        {
                            attributes |= DependencyCommon.RequiresAttributesMaxVersionInclusive;
                        }
                        break;

                    default:
                        this.Core.UnexpectedAttribute(sourceLineNumbers, attrib);
                        break;
                    }
                }
                else
                {
                    this.Core.UnsupportedExtensionAttribute(sourceLineNumbers, attrib);
                }
            }

            foreach (XmlNode child in node.ChildNodes)
            {
                if (XmlNodeType.Element == child.NodeType)
                {
                    if (child.NamespaceURI == this.schema.TargetNamespace)
                    {
                        this.Core.UnexpectedElement(node, child);
                    }
                    else
                    {
                        this.Core.UnsupportedExtensionElement(node, child);
                    }
                }
            }

            if (String.IsNullOrEmpty(id))
            {
                // Generate an ID only if this element is authored under a Provides element; otherwise, a RequiresRef
                // element will be necessary and the Id attribute will be required.
                if (!String.IsNullOrEmpty(providerId))
                {
                    id = this.Core.GenerateIdentifier("dep", node.LocalName, providerKey);
                }
                else
                {
                    this.Core.OnMessage(WixErrors.ExpectedAttributeWhenElementNotUnderElement(sourceLineNumbers, node.LocalName, "Id", "Provides"));
                }
            }

            if (String.IsNullOrEmpty(providerKey))
            {
                this.Core.OnMessage(WixErrors.ExpectedAttribute(sourceLineNumbers, node.LocalName, "ProviderKey"));
            }
            // Make sure the key does not contain any illegal characters.
            else if (0 <= (illegalChar = providerKey.IndexOfAny(DependencyCommon.InvalidCharacters)))
            {
                StringBuilder sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
                Array.ForEach <char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));

                this.Core.OnMessage(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "ProviderKey", providerKey[illegalChar], sb.ToString()));
            }


            if (!this.Core.EncounteredError)
            {
                // Reference the Require custom action if required.
                if (requiresAction)
                {
                    if (Platform.ARM == this.Core.CurrentPlatform)
                    {
                        // Ensure the ARM version of the CA is referenced.
                        this.Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "CustomAction", "WixDependencyRequire_ARM");
                    }
                    else
                    {
                        // All other supported platforms use x86.
                        this.Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "CustomAction", "WixDependencyRequire");
                    }
                }

                Row row = this.Core.CreateRow(sourceLineNumbers, "WixDependency");
                row[0] = id;
                row[1] = providerKey;
                row[2] = minVersion;
                row[3] = maxVersion;

                if (0 != attributes)
                {
                    row[4] = attributes;
                }

                // Create the relationship between this WixDependency row and the WixDependencyProvider row.
                if (!String.IsNullOrEmpty(providerId))
                {
                    // Create the relationship between the WixDependency row and the parent WixDependencyProvider row.
                    row    = this.Core.CreateRow(sourceLineNumbers, "WixDependencyRef");
                    row[0] = providerId;
                    row[1] = id;
                }
            }
        }
        /// <summary>
        /// Processes the Provides element.
        /// </summary>
        /// <param name="node">The XML node for the Provides element.</param>
        /// <param name="packageType">The type of the package being chained into a bundle, or "None" if building an MSI package.</param>
        /// <param name="keyPath">Explicit key path.</param>
        /// <param name="parentId">The identifier of the parent component or package.</param>
        /// <returns>The type of key path if set.</returns>
        private CompilerExtension.ComponentKeypathType ParseProvidesElement(XmlNode node, PackageType packageType, ref string keyPath, string parentId)
        {
            SourceLineNumberCollection sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);

            CompilerExtension.ComponentKeypathType keyPathType = CompilerExtension.ComponentKeypathType.None;
            string id          = null;
            string key         = null;
            string version     = null;
            string displayName = null;
            int    attributes  = 0;
            int    illegalChar = -1;

            foreach (XmlAttribute attrib in node.Attributes)
            {
                if (0 == attrib.NamespaceURI.Length || attrib.NamespaceURI == this.schema.TargetNamespace)
                {
                    switch (attrib.LocalName)
                    {
                    case "Id":
                        id = this.Core.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
                        break;

                    case "Key":
                        key = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
                        break;

                    case "Version":
                        version = this.Core.GetAttributeVersionValue(sourceLineNumbers, attrib, true);
                        break;

                    case "DisplayName":
                        displayName = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
                        break;

                    default:
                        this.Core.UnexpectedAttribute(sourceLineNumbers, attrib);
                        break;
                    }
                }
                else
                {
                    this.Core.UnsupportedExtensionAttribute(sourceLineNumbers, attrib);
                }
            }

            // Make sure the key is valid. The key will default to the ProductCode for MSI packages
            // and the package code for MSP packages in the binder if not specified.
            if (!String.IsNullOrEmpty(key))
            {
                // Make sure the key does not contain any illegal characters or values.
                if (0 <= (illegalChar = key.IndexOfAny(DependencyCommon.InvalidCharacters)))
                {
                    StringBuilder sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
                    Array.ForEach <char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));

                    this.Core.OnMessage(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "Key", key[illegalChar], sb.ToString()));
                }
                else if ("ALL" == key)
                {
                    this.Core.OnMessage(DependencyErrors.ReservedValue(sourceLineNumbers, node.LocalName, "Key", key));
                }
            }
            else if (PackageType.ExePackage == packageType || PackageType.MsuPackage == packageType)
            {
                // Must specify the provider key when authored for a package.
                this.Core.OnMessage(WixErrors.ExpectedAttribute(sourceLineNumbers, node.LocalName, "Key"));
            }
            else if (PackageType.None == packageType)
            {
                // Make sure the ProductCode is authored and set the key.
                this.Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "Property", "ProductCode");
                key = "!(bind.property.ProductCode)";
            }

            // The Version attribute should not be authored in or for an MSI package.
            if (!String.IsNullOrEmpty(version))
            {
                switch (packageType)
                {
                case PackageType.None:
                    this.Core.OnMessage(DependencyWarnings.DiscouragedVersionAttribute(sourceLineNumbers));
                    break;

                case PackageType.MsiPackage:
                    this.Core.OnMessage(DependencyWarnings.DiscouragedVersionAttribute(sourceLineNumbers, parentId));
                    break;
                }
            }
            else if (PackageType.MspPackage == packageType || PackageType.MsuPackage == packageType)
            {
                // Must specify the Version when authored for packages that do not contain a version.
                this.Core.OnMessage(WixErrors.ExpectedAttribute(sourceLineNumbers, node.LocalName, "Version"));
            }

            // Need the element ID for child element processing, so generate now if not authored.
            if (String.IsNullOrEmpty(id))
            {
                id = this.Core.GenerateIdentifier("dep", node.LocalName, parentId, key);
            }

            foreach (XmlNode child in node.ChildNodes)
            {
                if (XmlNodeType.Element == child.NodeType)
                {
                    if (child.NamespaceURI == this.schema.TargetNamespace)
                    {
                        switch (child.LocalName)
                        {
                        case "Requires":
                            this.ParseRequiresElement(child, id, PackageType.None == packageType);
                            break;

                        case "RequiresRef":
                            this.ParseRequiresRefElement(child, id, PackageType.None == packageType);
                            break;

                        default:
                            this.Core.UnexpectedElement(node, child);
                            break;
                        }
                    }
                    else
                    {
                        this.Core.UnsupportedExtensionElement(node, child);
                    }
                }
            }

            if (!this.Core.EncounteredError)
            {
                // Create the row in the provider table.
                Row row = this.Core.CreateRow(sourceLineNumbers, "WixDependencyProvider");
                row[0] = id;
                row[1] = parentId;
                row[2] = key;

                if (!String.IsNullOrEmpty(version))
                {
                    row[3] = version;
                }

                if (!String.IsNullOrEmpty(displayName))
                {
                    row[4] = displayName;
                }

                if (0 != attributes)
                {
                    row[5] = attributes;
                }

                if (PackageType.None == packageType)
                {
                    // Reference the Check custom action to check for dependencies on the current provider.
                    if (Platform.ARM == this.Core.CurrentPlatform)
                    {
                        // Ensure the ARM version of the CA is referenced.
                        this.Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "CustomAction", "WixDependencyCheck_ARM");
                    }
                    else
                    {
                        // All other supported platforms use x86.
                        this.Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "CustomAction", "WixDependencyCheck");
                    }

                    // Generate registry rows for the provider using binder properties.
                    string keyProvides = String.Concat(DependencyCommon.RegistryRoot, key);

                    row    = this.Core.CreateRow(sourceLineNumbers, "Registry");
                    row[0] = this.Core.GenerateIdentifier("reg", id, "(Default)");
                    row[1] = -1;
                    row[2] = keyProvides;
                    row[3] = null;
                    row[4] = "[ProductCode]";
                    row[5] = parentId;

                    // Use the Version registry value as the key path if not already set.
                    string idVersion = this.Core.GenerateIdentifier("reg", id, "Version");
                    if (String.IsNullOrEmpty(keyPath))
                    {
                        keyPath     = idVersion;
                        keyPathType = CompilerExtension.ComponentKeypathType.Registry;
                    }

                    row    = this.Core.CreateRow(sourceLineNumbers, "Registry");
                    row[0] = idVersion;
                    row[1] = -1;
                    row[2] = keyProvides;
                    row[3] = "Version";
                    row[4] = !String.IsNullOrEmpty(version) ? version : "[ProductVersion]";
                    row[5] = parentId;

                    row    = this.Core.CreateRow(sourceLineNumbers, "Registry");
                    row[0] = this.Core.GenerateIdentifier("reg", id, "DisplayName");
                    row[1] = -1;
                    row[2] = keyProvides;
                    row[3] = "DisplayName";
                    row[4] = !String.IsNullOrEmpty(displayName) ? displayName : "[ProductName]";
                    row[5] = parentId;

                    if (0 != attributes)
                    {
                        row    = this.Core.CreateRow(sourceLineNumbers, "Registry");
                        row[0] = this.Core.GenerateIdentifier("reg", id, "Attributes");
                        row[1] = -1;
                        row[2] = keyProvides;
                        row[3] = "Attributes";
                        row[4] = String.Concat("#", attributes.ToString(CultureInfo.InvariantCulture.NumberFormat));
                        row[5] = parentId;
                    }
                }
            }

            return(keyPathType);
        }
Esempio n. 5
0
        /// <summary>
        /// The entrypoint for the BepInEx plugin system.
        /// </summary>
        public static void Start()
        {
            if (_loaded)
            {
                return;
            }

            if (!_initialized)
            {
                throw new InvalidOperationException("BepInEx has not been initialized. Please call Chainloader.Initialize prior to starting the chainloader instance.");
            }

            if (!Directory.Exists(Paths.PluginPath))
            {
                Directory.CreateDirectory(Paths.PluginPath);
            }

            if (!Directory.Exists(Paths.PatcherPluginPath))
            {
                Directory.CreateDirectory(Paths.PatcherPluginPath);
            }

            try
            {
                var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
                if (productNameProp != null)
                {
                    ConsoleWindow.Title = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}";
                }

                Logger.LogMessage("Chainloader started");

                ManagerObject = new GameObject("BepInEx_Manager");

                UnityEngine.Object.DontDestroyOnLoad(ManagerObject);

                var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
                foreach (var keyValuePair in pluginsToLoad)
                {
                    foreach (var pluginInfo in keyValuePair.Value)
                    {
                        pluginInfo.Location = keyValuePair.Key;
                    }
                }
                var pluginInfos      = pluginsToLoad.SelectMany(p => p.Value).ToList();
                var loadedAssemblies = new Dictionary <string, Assembly>();

                Logger.LogInfo($"{pluginInfos.Count} plugins to load");

                var dependencyDict = new Dictionary <string, IEnumerable <string> >();
                var pluginsByGUID  = new Dictionary <string, PluginInfo>();

                foreach (var pluginInfo in pluginInfos)
                {
                    // Perform checks that will prevent loading plugins in this run
                    var  filters            = pluginInfo.Processes.ToList();
                    bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));

                    if (invalidProcessName)
                    {
                        Logger.LogWarning($"Skipping over plugin [{pluginInfo.Metadata.GUID}] due to process filter");
                        continue;
                    }

                    if (dependencyDict.ContainsKey(pluginInfo.Metadata.GUID))
                    {
                        Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because its GUID ({pluginInfo.Metadata.GUID}) is already used by another plugin.");
                        continue;
                    }

                    dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID).Concat(pluginInfo.Incompatibilities.Select(i => i.IncompatibilityGUID));
                    pluginsByGUID[pluginInfo.Metadata.GUID]  = pluginInfo;
                }

                var emptyDependencies = new string[0];

                // Sort plugins by their dependencies.
                // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list.
                var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys, x => dependencyDict.TryGetValue(x, out var deps) ? deps : emptyDependencies).ToList();

                var invalidPlugins   = new HashSet <string>();
                var processedPlugins = new Dictionary <string, Version>();

                foreach (var pluginGUID in sortedPlugins)
                {
                    // If the plugin is missing, don't process it
                    if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo))
                    {
                        continue;
                    }

                    var dependsOnInvalidPlugin = false;
                    var missingDependencies    = new List <BepInDependency>();
                    foreach (var dependency in pluginInfo.Dependencies)
                    {
                        // If the depenency wasn't already processed, it's missing altogether
                        bool depenencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
                        if (!depenencyExists || pluginVersion < dependency.MinimumVersion)
                        {
                            // If the dependency is hard, collect it into a list to show
                            if ((dependency.Flags & BepInDependency.DependencyFlags.HardDependency) != 0)
                            {
                                missingDependencies.Add(dependency);
                            }
                            continue;
                        }

                        // If the dependency is invalid (e.g. has missing depedencies), report that to the user
                        if (invalidPlugins.Contains(dependency.DependencyGUID))
                        {
                            dependsOnInvalidPlugin = true;
                            break;
                        }
                    }

                    var incompatibilities = new List <BepInIncompatibility>();
                    foreach (var incompatibility in pluginInfo.Incompatibilities)
                    {
                        if (processedPlugins.ContainsKey(incompatibility.IncompatibilityGUID))
                        {
                            incompatibilities.Add(incompatibility);
                        }
                    }

                    processedPlugins.Add(pluginGUID, pluginInfo.Metadata.Version);

                    if (dependsOnInvalidPlugin)
                    {
                        string message = $"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See above errors for details.";
                        DependencyErrors.Add(message);
                        Logger.LogWarning(message);
                        continue;
                    }

                    if (missingDependencies.Count != 0)
                    {
                        string ToMissingString(BepInDependency s)
                        {
                            bool emptyVersion = s.MinimumVersion.Major == 0 && s.MinimumVersion.Minor == 0 && s.MinimumVersion.Build == 0 && s.MinimumVersion.Revision == 0;

                            if (emptyVersion)
                            {
                                return("- " + s.DependencyGUID);
                            }
                            return($"- {s.DependencyGUID} (at least v{s.MinimumVersion})");
                        }

                        string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: {string.Join(", ", missingDependencies.Select(ToMissingString).ToArray())}";
                        DependencyErrors.Add(message);
                        Logger.LogError(message);

                        invalidPlugins.Add(pluginGUID);
                        continue;
                    }

                    if (incompatibilities.Count != 0)
                    {
                        string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {string.Join(", ", incompatibilities.Select(i => i.IncompatibilityGUID).ToArray())}";
                        DependencyErrors.Add(message);
                        Logger.LogError(message);

                        invalidPlugins.Add(pluginGUID);
                        continue;
                    }

                    try
                    {
                        Logger.LogInfo($"Loading [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");

                        if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
                        {
                            loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
                        }

                        PluginInfos[pluginGUID] = pluginInfo;
                        pluginInfo.Instance     = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));

                        Plugins.Add(pluginInfo.Instance);
                    }
                    catch (Exception ex)
                    {
                        invalidPlugins.Add(pluginGUID);
                        PluginInfos.Remove(pluginGUID);

                        Logger.LogError($"Error loading [{pluginInfo.Metadata.Name}] : {ex.Message}");
                        if (ex is ReflectionTypeLoadException re)
                        {
                            Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
                        }
                        else
                        {
                            Logger.LogDebug(ex);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                ConsoleWindow.Attach();

                Console.WriteLine("Error occurred starting the game");
                Console.WriteLine(ex.ToString());
            }

            Logger.LogMessage("Chainloader startup complete");

            _loaded = true;
        }