1010
1111namespace 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>
3671public 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 )
0 commit comments