Skip to content

Commit 2a61b61

Browse files
authored
Merge pull request #960 from aldelaro5/metaplugins
Implement patcher and plugin providers
2 parents 6b38cee + 2474dce commit 2a61b61

35 files changed

+1149
-249
lines changed

BepInEx.Core/Bootstrap/BaseChainloader.cs

Lines changed: 174 additions & 108 deletions
Large diffs are not rendered by default.

BepInEx.Core/Bootstrap/TypeLoader.cs

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,41 @@
1010

1111
namespace BepInEx.Bootstrap;
1212

13+
internal class CachedPluginLoadContext : IPluginLoadContext, IDisposable
14+
{
15+
public IPluginLoadContext PluginLoadContext { get; }
16+
private byte[] assemblyData;
17+
private byte[] assemblySymbolsData;
18+
19+
public CachedPluginLoadContext(IPluginLoadContext pluginLoadContext)
20+
{
21+
PluginLoadContext = pluginLoadContext;
22+
}
23+
24+
public string AssemblyIdentifier => PluginLoadContext.AssemblyIdentifier;
25+
public string AssemblyHash => PluginLoadContext.AssemblyHash;
26+
public byte[] GetAssemblyData()
27+
{
28+
return assemblyData ??= PluginLoadContext.GetAssemblyData();
29+
}
30+
31+
public byte[] GetAssemblySymbolsData()
32+
{
33+
return assemblySymbolsData ??= PluginLoadContext.GetAssemblySymbolsData();
34+
}
35+
36+
public byte[] GetFile(string relativePath)
37+
{
38+
return PluginLoadContext.GetFile(relativePath);
39+
}
40+
41+
public void Dispose()
42+
{
43+
assemblyData = null;
44+
assemblySymbolsData = null;
45+
}
46+
}
47+
1348
/// <summary>
1449
/// A cacheable metadata item. Can be used with <see cref="TypeLoader.LoadAssemblyCache{T}" /> and
1550
/// <see cref="TypeLoader.SaveAssemblyCache{T}" /> to cache plugin metadata.
@@ -35,6 +70,11 @@ public interface ICacheable
3570
/// <typeparam name="T"></typeparam>
3671
public class CachedAssembly<T> where T : ICacheable
3772
{
73+
/// <summary>
74+
/// The version of the cache which increments on each format changes
75+
/// </summary>
76+
public const int Version = 0;
77+
3878
/// <summary>
3979
/// List of cached items inside the assembly.
4080
/// </summary>
@@ -89,7 +129,9 @@ public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNa
89129
{
90130
Paths.BepInExAssemblyDirectory,
91131
Paths.PluginPath,
132+
Paths.PluginProviderPath,
92133
Paths.PatcherPluginPath,
134+
Paths.PatcherProviderPath,
93135
Paths.ManagedPath
94136
}.Concat(SearchDirectories);
95137

@@ -126,9 +168,8 @@ public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNa
126168
/// selector.
127169
/// </returns>
128170
public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory,
129-
Func<TypeDefinition, string, T> typeSelector,
130-
Func<AssemblyDefinition, bool> assemblyFilter =
131-
null,
171+
Func<TypeDefinition, IPluginLoadContext, string, T> typeSelector,
172+
Func<AssemblyDefinition, bool> assemblyFilter = null,
132173
string cacheName = null)
133174
where T : ICacheable, new()
134175
{
@@ -153,19 +194,7 @@ public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory,
153194
continue;
154195
}
155196

156-
using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
157-
Logger.Log(LogLevel.Debug, $"Examining '{dll}'");
158-
159-
if (!assemblyFilter?.Invoke(ass) ?? false)
160-
{
161-
result[dll] = new List<T>();
162-
continue;
163-
}
164-
165-
var matches = ass.MainModule.Types
166-
.Select(t => typeSelector(t, dll))
167-
.Where(t => t != null).ToList();
168-
result[dll] = matches;
197+
result[dll] = ExamineStream(typeSelector, assemblyFilter, dllMs, null, dll);
169198
}
170199
catch (BadImageFormatException e)
171200
{
@@ -183,6 +212,82 @@ public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory,
183212
return result;
184213
}
185214

215+
/// <summary>
216+
/// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata.
217+
/// </summary>
218+
/// <typeparam name="T">The specific base type to search for.</typeparam>
219+
/// <param name="loadContexts">The load contexts to obtain the assemblies from.</param>
220+
/// <param name="typeSelector">A function to check if a type should be selected and to build the type metadata.</param>
221+
/// <param name="assemblyFilter">A filter function to quickly determine if the assembly can be loaded.</param>
222+
/// <param name="cacheName">The name of the cache to get cached types from.</param>
223+
/// <returns>
224+
/// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the
225+
/// selector.
226+
/// </returns>
227+
public static List<T> GetPluginsFromLoadContexts<T>(IEnumerable<IPluginLoadContext> loadContexts,
228+
Func<TypeDefinition, IPluginLoadContext, string, T> typeSelector,
229+
Func<AssemblyDefinition, bool> assemblyFilter = null,
230+
string cacheName = null)
231+
where T : ICacheable, new()
232+
{
233+
var result = new Dictionary<string, List<T>>();
234+
var hashes = new Dictionary<string, string>();
235+
Dictionary<string, CachedAssembly<T>> cache = null;
236+
237+
if (cacheName != null)
238+
cache = LoadAssemblyCache<T>(cacheName);
239+
240+
foreach (IPluginLoadContext loadContext in loadContexts)
241+
{
242+
IList<T> plugins;
243+
if (cache != null && loadContext.AssemblyHash != null &&
244+
cache.TryGetValue(loadContext.AssemblyIdentifier, out var cacheEntry) &&
245+
loadContext.AssemblyHash == cacheEntry.Hash)
246+
{
247+
plugins = cacheEntry.CacheItems;
248+
}
249+
else
250+
{
251+
var assemblyData = loadContext.GetAssemblyData();
252+
using var memory = new MemoryStream(assemblyData);
253+
plugins = ExamineStream(typeSelector, assemblyFilter, memory, loadContext, null);
254+
}
255+
256+
foreach (T pluginInfo in plugins)
257+
{
258+
if (!result.ContainsKey(loadContext.AssemblyIdentifier))
259+
result[loadContext.AssemblyIdentifier] = new();
260+
result[loadContext.AssemblyIdentifier].Add(pluginInfo);
261+
}
262+
}
263+
264+
if (cache != null)
265+
SaveAssemblyCache(cacheName, result, hashes);
266+
267+
return result.SelectMany(x => x.Value).ToList();
268+
}
269+
270+
private static List<T> ExamineStream<T>(Func<TypeDefinition, IPluginLoadContext, string, T> typeSelector,
271+
Func<AssemblyDefinition, bool> assemblyFilter,
272+
MemoryStream dllMs,
273+
IPluginLoadContext loadContext,
274+
string location)
275+
where T : ICacheable, new()
276+
{
277+
using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
278+
Logger.Log(LogLevel.Debug, $"Examining '{ass.Name}'");
279+
280+
if (!assemblyFilter?.Invoke(ass) ?? false)
281+
{
282+
return new List<T>();
283+
}
284+
285+
var matches = ass.MainModule.Types
286+
.Select(t => typeSelector(t, loadContext, location))
287+
.Where(t => t != null).ToList();
288+
return matches;
289+
}
290+
186291
/// <summary>
187292
/// Loads an index of type metadatas from a cache.
188293
/// </summary>
@@ -205,7 +310,9 @@ public static Dictionary<string, CachedAssembly<T>> LoadAssemblyCache<T>(string
205310
if (!File.Exists(path))
206311
return null;
207312

208-
using (var br = new BinaryReader(File.OpenRead(path)))
313+
using var br = new BinaryReader(File.OpenRead(path));
314+
var version = br.ReadInt32();
315+
if (version == CachedAssembly<T>.Version)
209316
{
210317
var entriesCount = br.ReadInt32();
211318

@@ -259,6 +366,7 @@ public static void SaveAssemblyCache<T>(string cacheName,
259366
var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
260367

261368
using var bw = new BinaryWriter(File.OpenWrite(path));
369+
bw.Write(CachedAssembly<T>.Version);
262370
bw.Write(entries.Count);
263371

264372
foreach (var kv in entries)

BepInEx.Core/Contract/Attributes.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ internal static BepInPlugin FromCecilType(TypeDefinition td)
7676
}
7777
}
7878

79+
/// <summary>
80+
/// This attribute denotes that a class is a plugin provider, and specifies the required metadata.
81+
/// </summary>
82+
[AttributeUsage(AttributeTargets.Class)]
83+
public class BepInPluginProvider : BepInPlugin
84+
{
85+
/// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
86+
/// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
87+
/// <param name="Version">The specific version of the plugin.</param>
88+
public BepInPluginProvider(string GUID, string Name, string Version) : base(GUID, Name, Version)
89+
{
90+
}
91+
92+
internal new static BepInPluginProvider FromCecilType(TypeDefinition td)
93+
{
94+
var attr = MetadataHelper.GetCustomAttributes<BepInPluginProvider>(td, false).FirstOrDefault();
95+
96+
if (attr == null)
97+
return null;
98+
99+
return new BepInPluginProvider((string) attr.ConstructorArguments[0].Value,
100+
(string) attr.ConstructorArguments[1].Value,
101+
(string) attr.ConstructorArguments[2].Value);
102+
}
103+
}
104+
79105
/// <summary>
80106
/// This attribute specifies any dependencies that this plugin has on other plugins.
81107
/// </summary>
@@ -281,6 +307,28 @@ public static BepInPlugin GetMetadata(Type pluginType)
281307
/// <returns>The BepInPlugin metadata of the plugin instance.</returns>
282308
public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType());
283309

310+
/// <summary>
311+
/// Retrieves the BepInPluginProvider metadata from a plugin type.
312+
/// </summary>
313+
/// <param name="pluginType">The plugin type.</param>
314+
/// <returns>The BepInPluginProvider metadata of the plugin type.</returns>
315+
public static BepInPlugin GetPluginProviderMetadata(Type pluginType)
316+
{
317+
var attributes = pluginType.GetCustomAttributes(typeof(BepInPluginProvider), false);
318+
319+
if (attributes.Length == 0)
320+
return null;
321+
322+
return (BepInPlugin) attributes[0];
323+
}
324+
325+
/// <summary>
326+
/// Retrieves the BepInPluginProvider metadata from a plugin instance.
327+
/// </summary>
328+
/// <param name="plugin">The plugin instance.</param>
329+
/// <returns>The BepInPluginProvider metadata of the plugin instance.</returns>
330+
public static BepInPlugin GetPluginProviderMetadata(object plugin) => GetPluginProviderMetadata(plugin.GetType());
331+
284332
/// <summary>
285333
/// Gets the specified attributes of a type, if they exist.
286334
/// </summary>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection;
5+
using BepInEx.Configuration;
6+
using BepInEx.Logging;
7+
8+
namespace BepInEx;
9+
10+
/// <summary>
11+
/// The base plugin provider type that is used by the BepInEx plugin loader.
12+
/// </summary>
13+
public abstract class BasePluginProvider
14+
{
15+
/// <summary>
16+
/// Create a new instance of a plugin provider and all of its tied in objects.
17+
/// </summary>
18+
/// <exception cref="InvalidOperationException">BepInPluginProvider attribute is missing.</exception>
19+
protected BasePluginProvider()
20+
{
21+
var metadata = MetadataHelper.GetPluginProviderMetadata(this);
22+
if (metadata == null)
23+
throw new InvalidOperationException("Can't create an instance of " + GetType().FullName +
24+
" because it inherits from BasePluginProvider and the BepInPluginProvider attribute is missing.");
25+
26+
Info = new PluginInfo
27+
{
28+
Metadata = metadata,
29+
Instance = this,
30+
Dependencies = MetadataHelper.GetDependencies(GetType()),
31+
Processes = MetadataHelper.GetAttributes<BepInProcess>(GetType()),
32+
};
33+
34+
Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name);
35+
36+
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata);
37+
}
38+
39+
/// <summary>
40+
/// Information about this plugin provider as it was loaded.
41+
/// </summary>
42+
public PluginInfo Info { get; }
43+
44+
/// <summary>
45+
/// Logger instance tied to this plugin provider.
46+
/// </summary>
47+
protected ManualLogSource Logger { get; }
48+
49+
/// <summary>
50+
/// Default config file tied to this plugin provider. The config file will not be created until
51+
/// any settings are added and changed, or <see cref="ConfigFile.Save" /> is called.
52+
/// </summary>
53+
public ConfigFile Config { get; }
54+
55+
/// <summary>
56+
/// Obtains a list of assemblies containing plugins to load
57+
/// </summary>
58+
/// <returns>A list of load context, one per assembly</returns>
59+
public abstract IList<IPluginLoadContext> GetPlugins();
60+
61+
/// <summary>
62+
/// A custom assembly resolver that can be used by this provider to resolve assemblies
63+
/// that have failed to resolve
64+
/// </summary>
65+
/// <param name="name">The assembly's name</param>
66+
/// <returns>The resolved assembly or null if the assembly couldn't be resolved</returns>
67+
public virtual Assembly ResolveAssembly(string name)
68+
{
69+
return null;
70+
}
71+
}

BepInEx.Core/Contract/IPlugin.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@
77

88
namespace BepInEx.Contract
99
{
10-
public interface IPlugin
11-
{
12-
/// <summary>
13-
/// Information about this plugin as it was loaded.
14-
/// </summary>
15-
PluginInfo Info { get; }
10+
public interface IPlugin
11+
{
12+
/// <summary>
13+
/// Information about this plugin as it was loaded.
14+
/// </summary>
15+
PluginInfo Info { get; }
1616

17-
/// <summary>
18-
/// Logger instance tied to this plugin.
19-
/// </summary>
20-
ManualLogSource Logger { get; }
17+
/// <summary>
18+
/// Logger instance tied to this plugin.
19+
/// </summary>
20+
ManualLogSource Logger { get; }
2121

22-
/// <summary>
23-
/// Default config file tied to this plugin. The config file will not be created until
24-
/// any settings are added and changed, or <see cref="ConfigFile.Save"/> is called.
25-
/// </summary>
26-
ConfigFile Config { get; }
27-
}
22+
/// <summary>
23+
/// Default config file tied to this plugin. The config file will not be created until
24+
/// any settings are added and changed, or <see cref="ConfigFile.Save"/> is called.
25+
/// </summary>
26+
ConfigFile Config { get; }
27+
}
2828
}

0 commit comments

Comments
 (0)