/// <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; } }
/// <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); }
/// <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; }