Implement Patcher and plugin providers

This commit is contained in:
aldelaro5 committed 2024-08-19 19:57:52 -04:00
1 parent 53625800b8
commit 952c74f6fe
29 files changed
+864 -216

No files matched your search

+132 -106
View File
@@ -25,14 +25,14 @@ public abstract class BaseChainloader<TPlugin>
/// <param name="type">Type definition to analyze.</param>
/// <param name="assemblyLocation">The filepath of the assembly, to keep as metadata.</param>
/// <returns>If the type represent a valid plugin, returns a <see cref="PluginInfo" /> instance. Otherwise, return null.</returns>
public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation)
public static PluginInfo ToPluginInfo<T>(TypeDefinition type, IPluginLoader loader, string assemblyLocation)
{
if (type.IsInterface || type.IsAbstract)
return null;
try
{
if (!type.IsSubtypeOf(typeof(TPlugin)))
if (!type.IsSubtypeOf(typeof(T)))
return null;
}
catch (AssemblyResolutionException)
@@ -41,8 +41,13 @@ public abstract class BaseChainloader<TPlugin>
return null;
}
var metadata = BepInPlugin.FromCecilType(type);
if (type.IsSubtypeOf(typeof(BasePluginProvider)))
return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loader, assemblyLocation);
return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loader, assemblyLocation);
}
private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoader loader, string assemblyLocation)
{
// Perform checks that will prevent the plugin from being loaded in ALL cases
if (metadata == null)
{
@@ -85,15 +90,16 @@ public abstract class BaseChainloader<TPlugin>
Incompatibilities = incompatibilities,
TypeName = type.FullName,
TargettedBepInExVersion = bepinVersion,
Location = assemblyLocation
Location = assemblyLocation,
Loader = loader
};
}
protected static bool HasBepinPlugins(AssemblyDefinition ass)
protected static bool HasBepinPluginType<T>(AssemblyDefinition ass)
{
if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName))
return false;
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BepInPlugin).FullName))
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(T).FullName))
return false;
return true;
@@ -116,10 +122,20 @@ public abstract class BaseChainloader<TPlugin>
private bool _initialized;
/// <summary>
/// List of all <see cref="PluginInfo" /> instances loaded via the chainloader.
/// List of all <see cref="PluginInfo" /> instances of all plugins loaded via the chainloader.
/// </summary>
public Dictionary<string, PluginInfo> Plugins { get; } = new();
/// <summary>
/// List of all <see cref="PluginInfo" /> instances of all providers loaded via the chainloader.
/// </summary>
public Dictionary<string, PluginInfo> Providers { get; } = new();
/// <summary>
/// List of all <see cref="IPluginLoader" /> discovered from all the providers
/// </summary>
public List<IPluginLoader> Loaders { get; } = new();
/// <summary>
/// Collection of error chainloader messages that occured during plugin loading.
/// Contains information about what certain plugins were not loaded.
@@ -194,38 +210,30 @@ public abstract class BaseChainloader<TPlugin>
/// <param name="path">Path from which to search the plugins.</param>
/// <param name="cacheName">Cache name to use. If null, results are not cached.</param>
/// <returns>List of discovered plugins and their metadata.</returns>
protected IList<PluginInfo> DiscoverPluginsFrom(string path, string cacheName = "chainloader")
protected List<PluginInfo> DiscoverPluginProvidersFrom(string path, string cacheName = "chainloader_meta")
{
var pluginsToLoad =
TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPlugins, cacheName);
TypeLoader.FindPluginTypes(path, ToPluginInfo<BasePluginProvider>, HasBepinPluginType<BasePluginProvider>, cacheName);
return pluginsToLoad.SelectMany(p => p.Value).ToList();
}
/// <summary>
/// Discovers plugins to load.
/// </summary>
/// <returns>List of plugins to be loaded.</returns>
protected virtual IList<PluginInfo> DiscoverPlugins()
{
return DiscoverPluginsFrom(Paths.PluginPath);
}
/// <summary>
/// Preprocess the plugins and modify the load order.
/// </summary>
/// <remarks>Some plugins may be skipped if they cannot be loaded (wrong metadata, etc).</remarks>
/// <param name="plugins">Plugins to process.</param>
/// <returns>List of plugins to load in the correct load order.</returns>
protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
protected virtual List<T> ModifyLoadOrder<T>(IList<T> plugins, Dictionary<string, T> existingPlugins)
where T : PluginInfo
{
// 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>();
var pluginsByGuid = new Dictionary<string, T>();
foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID))
{
if (Plugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin))
if (existingPlugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin))
{
Logger.Log(LogLevel.Warning,
$"Skipping [{pluginInfoGroup.Key}] because a plugin with a similar GUID ([{loadedPlugin}]) has been already loaded.");
@@ -266,7 +274,7 @@ public abstract class BaseChainloader<TPlugin>
foreach (var pluginInfo in pluginsByGuid.Values.ToList())
if (pluginInfo.Incompatibilities.Any(incompatibility =>
pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID)
|| Plugins.ContainsKey(incompatibility.IncompatibilityGUID))
|| existingPlugins.ContainsKey(incompatibility.IncompatibilityGUID))
)
{
pluginsByGuid.Remove(pluginInfo.Metadata.GUID);
@@ -275,7 +283,7 @@ public abstract class BaseChainloader<TPlugin>
var incompatiblePluginsNew = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
.Where(x => pluginsByGuid.ContainsKey(x));
var incompatiblePluginsExisting = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
.Where(x => Plugins.ContainsKey(x));
.Where(x => existingPlugins.ContainsKey(x));
var incompatiblePlugins = incompatiblePluginsNew.Concat(incompatiblePluginsExisting).ToArray();
var message =
$@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
@@ -306,15 +314,23 @@ public abstract class BaseChainloader<TPlugin>
}
/// <summary>
/// Run the chainloader and load all plugins from the plugins folder.
/// Run the chainloader and load all plugins from all providers.
/// </summary>
public virtual void Execute()
{
try
{
var plugins = DiscoverPlugins();
var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory);
var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath);
Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " +
$"meta plugin{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load");
LoadPlugins(builtinProviders, provider: true);
LoadPlugins(providers, provider: true);
var plugins = TypeLoader.GetPluginsFromLoaders(Loaders, ToPluginInfo<TPlugin>, HasBepinPluginType<BepInPlugin>, "chainloader");
Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
LoadPlugins(plugins);
LoadPlugins(plugins, provider: false);
Finished?.Invoke();
}
catch (Exception ex)
@@ -331,71 +347,19 @@ public abstract class BaseChainloader<TPlugin>
Logger.Log(LogLevel.Message, "Chainloader startup complete");
}
private IList<PluginInfo> LoadPlugins(IList<PluginInfo> plugins)
private void LoadPlugins(List<PluginInfo> plugins, bool provider)
{
var sortedPlugins = ModifyLoadOrder(plugins);
var existingPlugins = provider ? Providers : Plugins;
var sortedPlugins = ModifyLoadOrder(plugins, existingPlugins);
var invalidPlugins = new HashSet<string>();
var processedPlugins = new Dictionary<string, SemanticVersioning.Version>();
var loadedAssemblies = new Dictionary<string, Assembly>();
var loadedPlugins = new List<PluginInfo>();
foreach (var plugin in sortedPlugins)
{
var dependsOnInvalidPlugin = false;
var missingDependencies = new List<BepInDependency>();
foreach (var dependency in plugin.Dependencies)
if (!CheckDependencies(plugin, processedPlugins, invalidPlugins, existingPlugins))
{
static bool IsHardDependency(BepInDependency dep) =>
(dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
// If the dependency wasn't already processed, it's missing altogether
var dependencyExists =
processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
// Alternatively, if the dependency hasn't been loaded before, it's missing too
if (!dependencyExists)
{
dependencyExists = Plugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo);
pluginVersion = pluginInfo?.Metadata.Version;
}
if (!dependencyExists || dependency.VersionRange != null &&
!dependency.VersionRange.IsSatisfied(pluginVersion))
{
// If the dependency is hard, collect it into a list to show
if (IsHardDependency(dependency))
missingDependencies.Add(dependency);
continue;
}
// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
{
dependsOnInvalidPlugin = true;
break;
}
}
processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
if (dependsOnInvalidPlugin)
{
var message =
$"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Warning, message);
continue;
}
if (missingDependencies.Count != 0)
{
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
}";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Error, message);
invalidPlugins.Add(plugin.Metadata.GUID);
continue;
}
@@ -403,45 +367,107 @@ public abstract class BaseChainloader<TPlugin>
{
Logger.Log(LogLevel.Info, $"Loading [{plugin}]");
if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
loadedAssemblies[plugin.Location] = ass = Assembly.LoadFrom(plugin.Location);
Assembly ass = null;
if (provider && !loadedAssemblies.TryGetValue(plugin.Location, out ass))
{
ass = Assembly.LoadFrom(plugin.Location);
loadedAssemblies[plugin.Location] = ass;
}
else if (!provider && !loadedAssemblies.TryGetValue(plugin.Loader.AssemblyIdentifier, out ass))
{
ass = Assembly.Load(plugin.Loader.GetAssemblyData());
loadedAssemblies[plugin.Loader.AssemblyIdentifier] = ass;
}
Plugins[plugin.Metadata.GUID] = plugin;
existingPlugins[plugin.Metadata.GUID] = plugin;
TryRunModuleCtor(plugin, ass);
plugin.Instance = LoadPlugin(plugin, ass);
loadedPlugins.Add(plugin);
if (provider)
{
var type = ass.GetType(plugin.TypeName);
plugin.Instance = Activator.CreateInstance(type);
AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name);
var requestedPluginsStream = ((BasePluginProvider)plugin.Instance).GetPlugins();
Loaders.AddRange(requestedPluginsStream);
}
else
{
plugin.Instance = LoadPlugin(plugin, ass);
}
PluginLoaded?.Invoke(plugin);
}
catch (Exception ex)
{
invalidPlugins.Add(plugin.Metadata.GUID);
Plugins.Remove(plugin.Metadata.GUID);
existingPlugins.Remove(plugin.Metadata.GUID);
Logger.Log(LogLevel.Error,
$"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}");
}
}
return loadedPlugins;
}
/// <summary>
/// Detects and loads all plugins in the specified directories.
/// </summary>
/// <remarks>
/// It is better to collect all paths at once and use a single call to LoadPlugins than multiple calls.
/// This allows to run proper dependency resolving and to load all plugins in one go.
/// </remarks>
/// <param name="pluginsPaths">Directories to search the plugins from.</param>
/// <returns>List of loaded plugin infos.</returns>
public IList<PluginInfo> LoadPlugins(params string[] pluginsPaths)
private bool CheckDependencies<T>(T plugin, Dictionary<string, SemanticVersioning.Version> processedPlugins, HashSet<string> invalidPlugins, Dictionary<string, T> existingPlugins)
where T : PluginInfo
{
// TODO: This is a temporary solution for 3rd party loaders. Instead, this should be done via metaplugins.
var plugins = new List<PluginInfo>();
foreach (var pluginsPath in pluginsPaths)
plugins.AddRange(DiscoverPluginsFrom(pluginsPath));
return LoadPlugins(plugins);
var dependsOnInvalidPlugin = false;
var missingDependencies = new List<BepInDependency>();
foreach (var dependency in plugin.Dependencies)
{
static bool IsHardDependency(BepInDependency dep) =>
(dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
// If the dependency wasn't already processed, it's missing altogether
var dependencyExists =
processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
// Alternatively, if the dependency hasn't been loaded before, it's missing too
if (!dependencyExists)
{
dependencyExists = existingPlugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo);
pluginVersion = pluginInfo?.Metadata.Version;
}
if (!dependencyExists || dependency.VersionRange != null &&
!dependency.VersionRange.IsSatisfied(pluginVersion))
{
// If the dependency is hard, collect it into a list to show
if (IsHardDependency(dependency))
missingDependencies.Add(dependency);
continue;
}
// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
{
dependsOnInvalidPlugin = true;
break;
}
}
processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
if (dependsOnInvalidPlugin)
{
var message =
$"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Warning, message);
return false;
}
if (missingDependencies.Count != 0)
{
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
}";
DependencyErrors.Add(message);
Logger.Log(LogLevel.Error, message);
invalidPlugins.Add(plugin.Metadata.GUID);
return false;
}
return true;
}
private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly)
+81 -16
View File
@@ -89,7 +89,9 @@ public static class TypeLoader
{
Paths.BepInExAssemblyDirectory,
Paths.PluginPath,
Paths.PluginProviderPath,
Paths.PatcherPluginPath,
Paths.PatcherProviderPath,
Paths.ManagedPath
}.Concat(SearchDirectories);
@@ -126,9 +128,8 @@ public static class TypeLoader
/// selector.
/// </returns>
public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory,
Func<TypeDefinition, string, T> typeSelector,
Func<AssemblyDefinition, bool> assemblyFilter =
null,
Func<TypeDefinition, IPluginLoader, string, T> typeSelector,
Func<AssemblyDefinition, bool> assemblyFilter = null,
string cacheName = null)
where T : ICacheable, new()
{
@@ -153,19 +154,7 @@ public static class TypeLoader
continue;
}
using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
Logger.Log(LogLevel.Debug, $"Examining '{dll}'");
if (!assemblyFilter?.Invoke(ass) ?? false)
{
result[dll] = new List<T>();
continue;
}
var matches = ass.MainModule.Types
.Select(t => typeSelector(t, dll))
.Where(t => t != null).ToList();
result[dll] = matches;
result[dll] = ExamineStream(typeSelector, assemblyFilter, dllMs, null, dll);
}
catch (BadImageFormatException e)
{
@@ -183,6 +172,82 @@ public static class TypeLoader
return result;
}
/// <summary>
/// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata.
/// </summary>
/// <typeparam name="T">The specific base type to search for.</typeparam>
/// <param name="loaders">The loaders to obtain the assemblies from.</param>
/// <param name="typeSelector">A function to check if a type should be selected and to build the type metadata.</param>
/// <param name="assemblyFilter">A filter function to quickly determine if the assembly can be loaded.</param>
/// <param name="cacheName">The name of the cache to get cached types from.</param>
/// <returns>
/// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the
/// selector.
/// </returns>
public static List<T> GetPluginsFromLoaders<T>(IList<IPluginLoader> loaders,
Func<TypeDefinition, IPluginLoader, string, T> typeSelector,
Func<AssemblyDefinition, bool> assemblyFilter = null,
string cacheName = null)
where T : ICacheable, new()
{
var result = new Dictionary<string, List<T>>();
var hashes = new Dictionary<string, string>();
Dictionary<string, CachedAssembly<T>> cache = null;
if (cacheName != null)
cache = LoadAssemblyCache<T>(cacheName);
foreach (IPluginLoader pluginLoader in loaders)
{
IList<T> plugins;
if (cache != null && pluginLoader.AssemblyHash != null &&
cache.TryGetValue(pluginLoader.AssemblyIdentifier, out var cacheEntry) &&
pluginLoader.AssemblyHash == cacheEntry.Hash)
{
plugins = cacheEntry.CacheItems;
}
else
{
var assemblyData = pluginLoader.GetAssemblyData();
using var memory = new MemoryStream(assemblyData);
plugins = ExamineStream(typeSelector, assemblyFilter, memory, pluginLoader, null);
}
foreach (T pluginInfo in plugins)
{
if (!result.ContainsKey(pluginLoader.AssemblyIdentifier))
result[pluginLoader.AssemblyIdentifier] = new();
result[pluginLoader.AssemblyIdentifier].Add(pluginInfo);
}
}
if (cache != null)
SaveAssemblyCache(cacheName, result, hashes);
return result.SelectMany(x => x.Value).ToList();
}
private static List<T> ExamineStream<T>(Func<TypeDefinition, IPluginLoader, string, T> typeSelector,
Func<AssemblyDefinition, bool> assemblyFilter,
MemoryStream dllMs,
IPluginLoader loader,
string location)
where T : ICacheable, new()
{
using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
Logger.Log(LogLevel.Debug, $"Examining '{ass.Name}'");
if (!assemblyFilter?.Invoke(ass) ?? false)
{
return new List<T>();
}
var matches = ass.MainModule.Types
.Select(t => typeSelector(t, loader, location))
.Where(t => t != null).ToList();
return matches;
}
/// <summary>
/// Loads an index of type metadatas from a cache.
/// </summary>
+50 -2
View File
@@ -76,6 +76,32 @@ public class BepInPlugin : Attribute
}
}
/// <summary>
/// This attribute denotes that a class is a plugin provider, and specifies the required metadata.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class BepInPluginProvider : BepInPlugin
{
/// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
/// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
/// <param name="Version">The specific version of the plugin.</param>
public BepInPluginProvider(string GUID, string Name, string Version) : base(GUID, Name, Version)
{
}
internal static BepInPluginProvider FromCecilType(TypeDefinition td)
{
var attr = MetadataHelper.GetCustomAttributes<BepInPluginProvider>(td, false).FirstOrDefault();
if (attr == null)
return null;
return new BepInPluginProvider((string) attr.ConstructorArguments[0].Value,
(string) attr.ConstructorArguments[1].Value,
(string) attr.ConstructorArguments[2].Value);
}
}
/// <summary>
/// This attribute specifies any dependencies that this plugin has on other plugins.
/// </summary>
@@ -264,7 +290,7 @@ public static class MetadataHelper
/// </summary>
/// <param name="pluginType">The plugin type.</param>
/// <returns>The BepInPlugin metadata of the plugin type.</returns>
public static BepInPlugin GetMetadata(Type pluginType)
public static BepInPlugin GetPluginMetadata(Type pluginType)
{
var attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false);
@@ -279,7 +305,29 @@ public static class MetadataHelper
/// </summary>
/// <param name="plugin">The plugin instance.</param>
/// <returns>The BepInPlugin metadata of the plugin instance.</returns>
public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType());
public static BepInPlugin GetPluginMetadata(object plugin) => GetPluginMetadata(plugin.GetType());
/// <summary>
/// Retrieves the BepInPluginProvider metadata from a plugin type.
/// </summary>
/// <param name="pluginType">The plugin type.</param>
/// <returns>The BepInPluginProvider metadata of the plugin type.</returns>
public static BepInPlugin GetPluginProviderMetadata(Type pluginType)
{
var attributes = pluginType.GetCustomAttributes(typeof(BepInPluginProvider), false);
if (attributes.Length == 0)
return null;
return (BepInPlugin) attributes[0];
}
/// <summary>
/// Retrieves the BepInPluginProvider metadata from a plugin instance.
/// </summary>
/// <param name="plugin">The plugin instance.</param>
/// <returns>The BepInPluginProvider metadata of the plugin instance.</returns>
public static BepInPlugin GetPluginProviderMetadata(object plugin) => GetPluginProviderMetadata(plugin.GetType());
/// <summary>
/// Gets the specified attributes of a type, if they exist.
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Logging;
namespace BepInEx;
/// <summary>
/// The base plugin provider type that is used by the BepInEx plugin loader.
/// </summary>
public abstract class BasePluginProvider
{
/// <summary>
/// Create a new instance of a plugin provider and all of its tied in objects.
/// </summary>
/// <exception cref="InvalidOperationException">BepInPluginProvider attribute is missing.</exception>
protected BasePluginProvider()
{
var metadata = MetadataHelper.GetPluginProviderMetadata(this);
if (metadata == null)
throw new InvalidOperationException("Can't create an instance of " + GetType().FullName +
" because it inherits from BasePluginProvider and the BepInPluginProvider attribute is missing.");
Info = new PluginInfo
{
Metadata = metadata,
Instance = this,
Dependencies = MetadataHelper.GetDependencies(GetType()),
Processes = MetadataHelper.GetAttributes<BepInProcess>(GetType()),
};
Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name);
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata);
}
/// <summary>
/// Information about this plugin provider as it was loaded.
/// </summary>
public PluginInfo Info { get; }
/// <summary>
/// Logger instance tied to this plugin provider.
/// </summary>
protected ManualLogSource Logger { get; }
/// <summary>
/// Default config file tied to this plugin provider. The config file will not be created until
/// any settings are added and changed, or <see cref="ConfigFile.Save" /> is called.
/// </summary>
public ConfigFile Config { get; }
/// <summary>
/// Obtains a list of assemblies containing plugins to load
/// </summary>
/// <returns>A list of loaders, one per assembly</returns>
public abstract IList<IPluginLoader> GetPlugins();
/// <summary>
/// A custom assembly resolver that can be used by this provider to resolve assemblies
/// that have failed to resolve
/// </summary>
/// <param name="name">The assembly's name</param>
/// <returns>The resolved assembly or null if the assembly couldn't be resolved</returns>
public virtual Assembly ResolveAssembly(string name)
{
return null;
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace BepInEx;
/// <summary>
/// An interface that allows to dynamically load and track an assembly
/// which can be obtained from a provided
/// </summary>
public interface IPluginLoader
{
/// <summary>
/// An identifier that uniquely identifies an assembly from a provider no matter its revision
/// </summary>
public string AssemblyIdentifier { get; }
/// <summary>
/// An optional hash that changes each time the assembly changes which can be tracked for cache
/// invalidation purposes. If this is null, no caching occurs for this assembly load
/// </summary>
public string AssemblyHash { get; }
/// <summary>
/// Obtains the assembly's raw data without loading it into the appdomain.
/// This may be called multiple times by the chainloader
/// </summary>
/// <returns>The assembly's raw data in bytes</returns>
public byte[] GetAssemblyData();
}
+15 -15
View File
@@ -16,6 +16,11 @@ public class PluginInfo : ICacheable
/// General metadata about a plugin.
/// </summary>
public BepInPlugin Metadata { get; internal set; }
/// <summary>
/// The loader used to load this plugin, null if it is a provider
/// </summary>
public IPluginLoader Loader { get; internal set; }
/// <summary>
/// Collection of <see cref="BepInProcess" /> attributes that describe what processes the plugin can run on.
@@ -33,24 +38,23 @@ public class PluginInfo : ICacheable
/// </summary>
public IEnumerable<BepInIncompatibility> Incompatibilities { get; internal set; }
/// <summary>
/// File path to the plugin DLL
/// </summary>
public string Location { get; internal set; }
public string TypeName { get; internal set; }
/// <summary>
/// Instance of the plugin that represents this info. NULL if no plugin is instantiated from info (yet)
/// </summary>
public object Instance { get; internal set; }
public string TypeName { get; internal set; }
internal Version TargettedBepInExVersion { get; set; }
internal string Location { get; set; }
/// <inheritdoc />
public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}";
void ICacheable.Save(BinaryWriter bw)
public virtual void Save(BinaryWriter bw)
{
bw.Write(TypeName);
bw.Write(Location);
bw.Write(Metadata.GUID);
bw.Write(Metadata.Name);
@@ -74,10 +78,9 @@ public class PluginInfo : ICacheable
bw.Write(TargettedBepInExVersion.ToString(4));
}
void ICacheable.Load(BinaryReader br)
public virtual void Load(BinaryReader br)
{
TypeName = br.ReadString();
Location = br.ReadString();
Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString());
@@ -111,7 +114,4 @@ public class PluginInfo : ICacheable
TargettedBepInExVersion = new Version(br.ReadString());
}
/// <inheritdoc />
public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}";
}
+12
View File
@@ -75,6 +75,16 @@ public static class Paths
/// </summary>
public static string PatcherPluginPath { get; private set; }
/// <summary>
/// The path to the plugin provider folder which resides in the BepInEx folder.
/// </summary>
public static string PluginProviderPath { get; private set; }
/// <summary>
/// The path to the patcher plugin provider folder which resides in the BepInEx folder.
/// </summary>
public static string PatcherProviderPath { get; private set; }
/// <summary>
/// The path to the plugin folder which resides in the BepInEx folder.
/// <para>
@@ -113,7 +123,9 @@ public static class Paths
BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx");
ConfigPath = Path.Combine(BepInExRootPath, "config");
BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg");
PluginProviderPath = Path.Combine(BepInExRootPath, "plugins-providers");
PluginPath = Path.Combine(BepInExRootPath, "plugins");
PatcherProviderPath = Path.Combine(BepInExRootPath, "patchers-providers");
PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers");
BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core");
BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory,
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net35;netstandard2.0</TargetFrameworks>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<Description>BepInEx patcher provider module</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BepInEx.Preloader.Core\BepInEx.Preloader.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using System.IO;
namespace BepInEx.PatcherProvider;
public class BepInExPatcherLoader : IPluginLoader
{
public string AssemblyIdentifier { get; internal set; }
public string AssemblyHash { get; internal set; }
private byte[] assemblyData;
public byte[] GetAssemblyData()
{
if (assemblyData == null)
{
assemblyData = File.ReadAllBytes(AssemblyIdentifier);
}
return assemblyData;
}
}
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using BepInEx.Logging;
using BepInEx.Preloader.Core.Patching;
namespace BepInEx.PatcherProvider;
[PatcherProviderPluginInfo("io.bepinex.bepinexpatcherprovider", "BepInExPatcherProvider", "1.0")]
public class BepInExPatcherProvider : BasePatcherProvider
{
private static readonly Dictionary<string, string> AssemblyLocationsByFilename = new();
public override IList<IPluginLoader> GetPatchers()
{
var loaders = new List<IPluginLoader>();
foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PatcherPluginPath), "*.dll", SearchOption.AllDirectories))
{
try
{
AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll));
loaders.Add(new BepInExPatcherLoader
{
AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"),
AssemblyIdentifier = dll
});
}
catch (Exception e)
{
Log.Log(LogLevel.Error, e);
}
}
return loaders;
}
public override Assembly ResolveAssembly(string name)
{
if (!AssemblyLocationsByFilename.TryGetValue(name, out var location))
return null;
if (!Utility.TryResolveDllAssembly(new(name), location, out var ass))
return null;
return ass;
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net35;netstandard2.0</TargetFrameworks>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<Description>BepInEx plugin provider module</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BepInEx.Core\BepInEx.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All"/>
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using System.IO;
namespace BepInEx.PluginProvider;
public class BepInExPluginLoader : IPluginLoader
{
public string AssemblyIdentifier { get; internal set; }
public string AssemblyHash { get; internal set; }
private byte[] assemblyData;
public byte[] GetAssemblyData()
{
if (assemblyData == null)
{
assemblyData = File.ReadAllBytes(AssemblyIdentifier);
}
return assemblyData;
}
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using BepInEx.Logging;
namespace BepInEx.PluginProvider;
[BepInPluginProvider("io.bepinex.bepinexpluginprovider", "BepInExPluginProvider", "1.0")]
public class BepInExPluginProvider : BasePluginProvider
{
private static readonly Dictionary<string, string> AssemblyLocationsByFilename = new();
public override IList<IPluginLoader> GetPlugins()
{
var loaders = new List<IPluginLoader>();
foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories))
{
try
{
AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll));
loaders.Add(new BepInExPluginLoader
{
AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"),
AssemblyIdentifier = dll
});
}
catch (Exception e)
{
Logger.Log(LogLevel.Error, e);
}
}
return loaders;
}
public override Assembly ResolveAssembly(string name)
{
if (!AssemblyLocationsByFilename.TryGetValue(name, out var location))
return null;
if (!Utility.TryResolveDllAssembly(new(name), location, out var ass))
return null;
return ass;
}
}
@@ -11,6 +11,7 @@ using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Mono.Cecil;
using MonoMod.Utils;
namespace BepInEx.Preloader.Core.Patching;
@@ -24,6 +25,9 @@ public class AssemblyPatcher : IDisposable
private Func<byte[], string, Assembly> assemblyLoader;
private readonly List<IPluginLoader> loaders = new();
private readonly List<BasePatcherProvider> providers = new();
public AssemblyPatcher(Func<byte[], string, Assembly> assemblyLoader)
{
this.assemblyLoader = assemblyLoader;
@@ -61,16 +65,18 @@ public class AssemblyPatcher : IDisposable
// Clear to allow GC collection.
PatcherContext.PatcherPlugins.Clear();
AppDomain.CurrentDomain.AssemblyResolve -= PatcherProvidersAssemblyResolver;
}
private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath)
private PatcherPluginMetadata ToPatcherPlugin<T>(TypeDefinition type, IPluginLoader loader, string assemblyPath)
{
if (type.IsInterface || type.IsAbstract && !type.IsSealed)
return null;
try
{
if (!type.IsSubtypeOf(typeof(BasePatcher)))
if (!type.IsSubtypeOf(typeof(T)))
return null;
}
catch (AssemblyResolutionException)
@@ -79,8 +85,13 @@ public class AssemblyPatcher : IDisposable
return null;
}
var metadata = PatcherPluginInfoAttribute.FromCecilType(type);
if (type.IsSubtypeOf(typeof(BasePatcherProvider)))
return GetPatcherInfo(type, loader, PatcherProviderPluginInfoAttribute.FromCecilType(type));
return GetPatcherInfo(type, loader, PatcherPluginInfoAttribute.FromCecilType(type));
}
private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoader loader, PatcherPluginInfoAttribute metadata)
{
// Perform checks that will prevent the plugin from being loaded in ALL cases
if (metadata == null)
{
@@ -109,20 +120,160 @@ public class AssemblyPatcher : IDisposable
return new PatcherPluginMetadata
{
TypeName = type.FullName
TypeName = type.FullName,
Loader = loader
};
}
private bool HasPatcherPlugins(AssemblyDefinition ass)
private bool HasPatcherType<T>(AssemblyDefinition ass)
{
if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) &&
ass.Name.Name != CurrentAssemblyName)
return false;
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName))
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(T).FullName))
return false;
return true;
}
/// <summary>
/// Adds all patchers from all managed assemblies obtained from all patcher providers.
/// </summary>
public void AddPatchersFromProviders()
{
var allProviders = TypeLoader.FindPluginTypes(Paths.BepInExAssemblyDirectory, ToPatcherPlugin<BasePatcherProvider>, HasPatcherType<BasePatcherProvider>);
var foundProviders = TypeLoader.FindPluginTypes(Paths.PatcherProviderPath, ToPatcherPlugin<BasePatcherProvider>, HasPatcherType<BasePatcherProvider>);
allProviders.AddRange(foundProviders);
AppDomain.CurrentDomain.AssemblyResolve += PatcherProvidersAssemblyResolver;
foreach (var keyValuePair in allProviders)
{
var assemblyPath = keyValuePair.Key;
var patcherCollection = keyValuePair.Value;
if (patcherCollection.Count == 0)
continue;
var ass = Assembly.LoadFrom(assemblyPath);
foreach (var patcherPlugin in patcherCollection)
{
try
{
var type = ass.GetType(patcherPlugin.TypeName);
var instance = (BasePatcherProvider) Activator.CreateInstance(type);
providers.Add(instance);
loaders.AddRange(instance.GetPatchers());
}
catch (Exception e)
{
Logger.Log(LogLevel.Error,
$"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}");
}
}
}
var patchers = TypeLoader.GetPluginsFromLoaders(loaders, ToPatcherPlugin<BasePatcher>, HasPatcherType<BasePatcher>);
var sortedPatchers = new List<PatchDefinition>();
// TODO: Add dependency ordering and process attribute filtering
foreach (var patcherPlugin in patchers)
{
var ass = Assembly.Load(patcherPlugin.Loader.GetAssemblyData());
var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin);
sortedPatchers.AddRange(patchDefinitions);
var assName = ass.GetName();
Logger.Log(LogLevel.Debug, $"Loaded patcher {patcherPlugin.TypeName} from [{assName.Name} {assName.Version}]");
}
PatcherContext.PatchDefinitions.AddRange(sortedPatchers);
Logger.Log(LogLevel.Info, $"Loaded {sortedPatchers.Count} patcher{(sortedPatchers.Count > 0 ? "s" : "")}");
}
private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args)
{
foreach (BasePatcherProvider provider in providers)
{
var ass = provider.ResolveAssembly(args.Name);
if (ass != null)
return ass;
}
return null;
}
private List<PatchDefinition> LoadPatcherPlugin(Assembly ass, PatcherPluginMetadata patcher)
{
var patchDefinitions = new List<PatchDefinition>();
try
{
var type = ass.GetType(patcher.TypeName);
var instance = (BasePatcher) Activator.CreateInstance(type);
instance.Context = PatcherContext;
PatcherContext.PatcherPlugins.Add(instance);
var methods =
type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in methods)
{
var targetAssemblies = MetadataHelper.GetAttributes<TargetAssemblyAttribute>(method);
var targetTypes = MetadataHelper.GetAttributes<TargetTypeAttribute>(method);
if (targetAssemblies.Length == 0 && targetTypes.Length == 0)
continue;
var parameters = method.GetParameters();
if (parameters.Length < 1 || parameters.Length > 2
// Next few lines ensure that the first parameter is AssemblyDefinition and does not have any
// target type attributes, and vice versa
|| !(
parameters[0].ParameterType == typeof(AssemblyDefinition)
|| parameters[0].ParameterType ==
typeof(AssemblyDefinition).MakeByRefType()
&& targetTypes.Length == 0
|| parameters[0].ParameterType == typeof(TypeDefinition)
&& targetAssemblies.Length == 0
)
|| parameters.Length == 2 &&
parameters[1].ParameterType != typeof(string)
|| method.ReturnType != typeof(void) &&
method.ReturnType != typeof(bool)
)
{
Logger
.Log(LogLevel.Warning,
$"Skipping method [{method.FullDescription()}] as it is not a valid patcher method");
continue;
}
void AddDefinition(PatchDefinition definition)
{
Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]");
patchDefinitions.Add(definition);
}
foreach (var targetAssembly in targetAssemblies)
AddDefinition(new PatchDefinition(targetAssembly, instance, method));
foreach (var targetType in targetTypes)
AddDefinition(new PatchDefinition(targetType, instance, method));
}
}
catch (Exception e)
{
Logger.Log(LogLevel.Error,
$"Failed to load patchers from type [{patcher.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}");
}
return patchDefinitions;
}
/// <summary>
/// Adds all patchers from all managed assemblies specified in a directory.
@@ -135,7 +286,7 @@ public class AssemblyPatcher : IDisposable
var sortedPatchers = new List<PatchDefinition>();
var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins);
var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin<BasePatcher>, HasPatcherType<BasePatcher>);
// TODO: Add dependency ordering and process attribute filtering
@@ -150,68 +301,10 @@ public class AssemblyPatcher : IDisposable
var ass = Assembly.LoadFrom(assemblyPath);
foreach (var patcherPlugin in patcherCollection)
try
{
var type = ass.GetType(patcherPlugin.TypeName);
var instance = (BasePatcher) Activator.CreateInstance(type);
instance.Context = PatcherContext;
PatcherContext.PatcherPlugins.Add(instance);
var methods =
type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in methods)
{
var targetAssemblies = MetadataHelper.GetAttributes<TargetAssemblyAttribute>(method);
var targetTypes = MetadataHelper.GetAttributes<TargetTypeAttribute>(method);
if (targetAssemblies.Length == 0 && targetTypes.Length == 0)
continue;
var parameters = method.GetParameters();
if (parameters.Length < 1 || parameters.Length > 2
// Next few lines ensure that the first parameter is AssemblyDefinition and does not have any
// target type attributes, and vice versa
|| !(
parameters[0].ParameterType == typeof(AssemblyDefinition)
|| parameters[0].ParameterType ==
typeof(AssemblyDefinition).MakeByRefType()
&& targetTypes.Length == 0
|| parameters[0].ParameterType == typeof(TypeDefinition)
&& targetAssemblies.Length == 0
)
|| parameters.Length == 2 &&
parameters[1].ParameterType != typeof(string)
|| method.ReturnType != typeof(void) &&
method.ReturnType != typeof(bool)
)
{
Logger
.Log(LogLevel.Warning,
$"Skipping method [{method.FullDescription()}] as it is not a valid patcher method");
continue;
}
void AddDefinition(PatchDefinition definition)
{
Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]");
sortedPatchers.Add(definition);
}
foreach (var targetAssembly in targetAssemblies)
AddDefinition(new PatchDefinition(targetAssembly, instance, method));
foreach (var targetType in targetTypes)
AddDefinition(new PatchDefinition(targetType, instance, method));
}
}
catch (Exception e)
{
Logger.Log(LogLevel.Error,
$"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}");
}
{
var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin);
sortedPatchers.AddRange(patchDefinitions);
}
var assName = ass.GetName();
Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
@@ -79,6 +79,31 @@ public class PatcherPluginInfoAttribute : Attribute
}
}
/// <summary>
/// This attribute denotes that a class is a patcher plugin provider, and specifies the required metadata.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PatcherProviderPluginInfoAttribute : PatcherPluginInfoAttribute {
/// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
/// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
/// <param name="Version">The specific version of the plugin.</param>
public PatcherProviderPluginInfoAttribute(string GUID, string Name, string Version) : base(GUID, Name, Version)
{
}
internal static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td)
{
var attr = MetadataHelper.GetCustomAttributes<PatcherProviderPluginInfoAttribute>(td, false).FirstOrDefault();
if (attr == null)
return null;
return new PatcherProviderPluginInfoAttribute((string) attr.ConstructorArguments[0].Value,
(string) attr.ConstructorArguments[1].Value,
(string) attr.ConstructorArguments[2].Value);
}
}
/// <summary>
/// Defines an assembly that a patch method will target.
/// </summary>
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Logging;
namespace BepInEx.Preloader.Core.Patching;
/// <summary>
/// A patcher provider that can discover and load assemblies containing patcher plugins
/// </summary>
public abstract class BasePatcherProvider
{
protected BasePatcherProvider()
{
Info = PatcherPluginInfoAttribute.FromType(GetType());
Log = Logger.CreateLogSource(Info.Name);
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false,
new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString()));
}
/// <summary>
/// A <see cref="ILogSource" /> instance created for use by this patcher plugin provider.
/// </summary>
public ManualLogSource Log { get; }
/// <summary>
/// A configuration file binding created with the <see cref="PatcherPluginInfoAttribute.GUID" /> of this provider as the
/// filename.
/// </summary>
public ConfigFile Config { get; }
/// <summary>
/// Metadata associated with this patcher plugin provider.
/// </summary>
public PatcherPluginInfoAttribute Info { get; }
/// <summary>
/// Obtains a list of assemblies containing patchers to load
/// </summary>
/// <returns>A list of loaders, one per assembly</returns>
public abstract IList<IPluginLoader> GetPatchers();
/// <summary>
/// A custom assembly resolver that can be used by this provider to resolve assemblies
/// that have failed to resolve
/// </summary>
/// <param name="name">The assembly's name</param>
/// <returns>The resolved assembly or null if the assembly couldn't be resolved</returns>
public virtual Assembly ResolveAssembly(string name)
{
return null;
}
}
@@ -13,6 +13,11 @@ internal class PatcherPluginMetadata : ICacheable
/// </summary>
public string TypeName { get; set; } = string.Empty;
/// <summary>
/// The loader used to load this patcher, null if it is a provider
/// </summary>
public IPluginLoader Loader { get; set; }
/// <inheritdoc />
public void Save(BinaryWriter bw) => bw.Write(TypeName);
+20
View File
@@ -51,6 +51,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doorstop", "Doorstop", "{CE
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj", "{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PluginProvider", "BepInEx.PluginProvider\BepInEx.PluginProvider.csproj", "{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PatcherProvider", "BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj", "{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
BepInEx.NetLauncher.Shared\BepInEx.NetLauncher.Shared.projitems*{067bcc8c-fc25-4929-b4f4-c6a5a7496994}*SharedItemsImports = 13
@@ -147,6 +151,22 @@ Global
{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU
{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU
{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.Build.0 = Release|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -8,7 +8,7 @@ namespace BepInEx.NET.Common
{
protected BasePlugin()
{
var metadata = MetadataHelper.GetMetadata(this);
var metadata = MetadataHelper.GetPluginMetadata(this);
HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID);
@@ -12,6 +12,18 @@
<ItemGroup>
<ProjectReference Include="..\..\..\BepInEx.Core\BepInEx.Core.csproj"/>
<ProjectReference Include="..\..\..\BepInEx.PluginProvider\BepInEx.PluginProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.Preloader.Core\BepInEx.Preloader.Core.csproj"/>
</ItemGroup>
@@ -54,7 +54,7 @@ namespace BepInEx.NET.CoreCLR
using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data)))
{
assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
assemblyPatcher.AddPatchersFromProviders();
Log.LogInfo($"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded");
@@ -110,7 +110,7 @@ public static class NetPreloader
using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data)))
{
assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
assemblyPatcher.AddPatchersFromProviders();
Log.Log(LogLevel.Info,
$"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded");
@@ -8,7 +8,7 @@ public abstract class BasePlugin
{
protected BasePlugin()
{
var metadata = MetadataHelper.GetMetadata(this);
var metadata = MetadataHelper.GetPluginMetadata(this);
Log = Logger.CreateLogSource(metadata.Name);
@@ -9,6 +9,18 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BepInEx.Core\BepInEx.Core.csproj"/>
<ProjectReference Include="..\..\..\BepInEx.PluginProvider\BepInEx.PluginProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.Preloader.Core\BepInEx.Preloader.Core.csproj" PrivateAssets="All"/>
<ProjectReference Include="..\BepInEx.Unity.Common\BepInEx.Unity.Common.csproj"/>
</ItemGroup>
@@ -67,7 +67,7 @@ public static class Preloader
using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data)))
{
assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
assemblyPatcher.AddPatchersFromProviders();
Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded");
@@ -77,7 +77,7 @@ internal static class UnityPreloader
using (var assemblyPatcher = new AssemblyPatcher(MonoAssemblyHelper.LoadFromMemory))
{
assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory);
assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
assemblyPatcher.AddPatchersFromProviders();
Log.Log(LogLevel.Info,
$"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded");
@@ -16,7 +16,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour
/// <exception cref="InvalidOperationException">BepInPlugin attribute is missing.</exception>
protected BaseUnityPlugin()
{
var metadata = MetadataHelper.GetMetadata(this);
var metadata = MetadataHelper.GetPluginMetadata(this);
if (metadata == null)
throw new InvalidOperationException("Can't create an instance of " + GetType().FullName +
" because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing.");
@@ -27,7 +27,6 @@ public abstract class BaseUnityPlugin : MonoBehaviour
Instance = this,
Dependencies = MetadataHelper.GetDependencies(GetType()),
Processes = MetadataHelper.GetAttributes<BepInProcess>(GetType()),
Location = GetType().Assembly.Location
};
Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name);
@@ -11,6 +11,18 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BepInEx.Core\BepInEx.Core.csproj"/>
<ProjectReference Include="..\..\..\BepInEx.PluginProvider\BepInEx.PluginProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Targets>Build;DocumentationProjectOutputGroup</Targets>
</ProjectReference>
<ProjectReference Include="..\..\..\BepInEx.Preloader.Core\BepInEx.Preloader.Core.csproj" PrivateAssets="All"/>
<ProjectReference Include="..\BepInEx.Unity.Mono.Preloader\BepInEx.Unity.Mono.Preloader.csproj" PrivateAssets="All"/>
</ItemGroup>
+2
View File
@@ -239,8 +239,10 @@ public sealed class MakeDistTask : FrostingTask<BuildContext>
var bepInExCoreDir = bepInExDir.Combine("core");
ctx.CreateDirectory(bepInExDir);
ctx.CreateDirectory(bepInExCoreDir);
ctx.CreateDirectory(bepInExDir.Combine("plugins-providers"));
ctx.CreateDirectory(bepInExDir.Combine("plugins"));
ctx.CreateDirectory(bepInExDir.Combine("patchers"));
ctx.CreateDirectory(bepInExDir.Combine("patchers-providers"));
File.WriteAllText(targetDir.CombineWithFilePath("changelog.txt").FullPath, changelog);