@@ -175,14 +175,37 @@ protected virtual void InitializeLoggers()
175175 Logger . Sources . Add ( new HarmonyLogSource ( ) ) ;
176176 }
177177
178- protected virtual IList < PluginInfo > DiscoverPlugins ( )
178+ /// <summary>
179+ /// Discovers all plugins in the plugin directory without loading them.
180+ /// </summary>
181+ /// <remarks>
182+ /// This is useful for discovering BepInEx plugin metadata.
183+ /// </remarks>
184+ /// <param name="path">Path from which to search the plugins.</param>
185+ /// <param name="cacheName">Cache name to use. If null, results are not cached.</param>
186+ /// <returns>List of discovered plugins and their metadata.</returns>
187+ protected IList < PluginInfo > DiscoverPluginsFrom ( string path , string cacheName = "chainloader" )
179188 {
180189 var pluginsToLoad =
181- TypeLoader . FindPluginTypes ( Paths . PluginPath , ToPluginInfo , HasBepinPlugins , "chainloader" ) ;
182-
190+ TypeLoader . FindPluginTypes ( path , ToPluginInfo , HasBepinPlugins , cacheName ) ;
183191 return pluginsToLoad . SelectMany ( p => p . Value ) . ToList ( ) ;
184192 }
185193
194+ /// <summary>
195+ /// Discovers plugins to load.
196+ /// </summary>
197+ /// <returns>List of plugins to be loaded.</returns>
198+ protected virtual IList < PluginInfo > DiscoverPlugins ( )
199+ {
200+ return DiscoverPluginsFrom ( Paths . PluginPath ) ;
201+ }
202+
203+ /// <summary>
204+ /// Preprocess the plugins and modify the load order.
205+ /// </summary>
206+ /// <remarks>Some plugins may be skipped if they cannot be loaded (wrong metadata, etc).</remarks>
207+ /// <param name="plugins">Plugins to process.</param>
208+ /// <returns>List of plugins to load in the correct load order.</returns>
186209 protected virtual IList < PluginInfo > ModifyLoadOrder ( IList < PluginInfo > plugins )
187210 {
188211 // We use a sorted dictionary to ensure consistent load order
@@ -192,6 +215,13 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
192215
193216 foreach ( var pluginInfoGroup in plugins . GroupBy ( info => info . Metadata . GUID ) )
194217 {
218+ if ( Plugins . TryGetValue ( pluginInfoGroup . Key , out var loadedPlugin ) )
219+ {
220+ Logger . Log ( LogLevel . Warning ,
221+ $ "Skipping [{ pluginInfoGroup . Key } ] because a plugin with a similar GUID ([{ loadedPlugin } ]) has been already loaded.") ;
222+ continue ;
223+ }
224+
195225 PluginInfo loadedVersion = null ;
196226 foreach ( var pluginInfo in pluginInfoGroup . OrderByDescending ( x => x . Metadata . Version ) )
197227 {
@@ -225,14 +255,18 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
225255
226256 foreach ( var pluginInfo in pluginsByGuid . Values . ToList ( ) )
227257 if ( pluginInfo . Incompatibilities . Any ( incompatibility =>
228- pluginsByGuid . ContainsKey ( incompatibility . IncompatibilityGUID ) )
258+ pluginsByGuid . ContainsKey ( incompatibility . IncompatibilityGUID )
259+ || Plugins . ContainsKey ( incompatibility . IncompatibilityGUID ) )
229260 )
230261 {
231262 pluginsByGuid . Remove ( pluginInfo . Metadata . GUID ) ;
232263 dependencyDict . Remove ( pluginInfo . Metadata . GUID ) ;
233264
234- var incompatiblePlugins = pluginInfo . Incompatibilities . Select ( x => x . IncompatibilityGUID )
235- . Where ( x => pluginsByGuid . ContainsKey ( x ) ) . ToArray ( ) ;
265+ var incompatiblePluginsNew = pluginInfo . Incompatibilities . Select ( x => x . IncompatibilityGUID )
266+ . Where ( x => pluginsByGuid . ContainsKey ( x ) ) ;
267+ var incompatiblePluginsExisting = pluginInfo . Incompatibilities . Select ( x => x . IncompatibilityGUID )
268+ . Where ( x => Plugins . ContainsKey ( x ) ) ;
269+ var incompatiblePlugins = incompatiblePluginsNew . Concat ( incompatiblePluginsExisting ) . ToArray ( ) ;
236270 var message =
237271 $@ "Could not load [{ pluginInfo } ] because it is incompatible with: { string . Join ( ", " , incompatiblePlugins ) } ";
238272 DependencyErrors . Add ( message ) ;
@@ -246,6 +280,8 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
246280 Logger . Log ( LogLevel . Warning , message ) ;
247281 }
248282
283+ // We don't add already loaded plugins to the dependency graph as they are already loaded
284+
249285 var emptyDependencies = new string [ 0 ] ;
250286
251287 // Sort plugins by their dependencies.
@@ -259,107 +295,142 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
259295 return sortedPlugins . Where ( pluginsByGuid . ContainsKey ) . Select ( x => pluginsByGuid [ x ] ) . ToList ( ) ;
260296 }
261297
298+ /// <summary>
299+ /// Run the chainloader and load all plugins from the plugins folder.
300+ /// </summary>
262301 public virtual void Execute ( )
263302 {
264303 try
265304 {
266305 var plugins = DiscoverPlugins ( ) ;
267-
268306 Logger . Log ( LogLevel . Info , $ "{ plugins . Count } plugin{ ( plugins . Count == 1 ? "" : "s" ) } to load") ;
307+ LoadPlugins ( plugins ) ;
308+ }
309+ catch ( Exception ex )
310+ {
311+ try
312+ {
313+ ConsoleManager . CreateConsole ( ) ;
314+ }
315+ catch { }
269316
270- var sortedPlugins = ModifyLoadOrder ( plugins ) ;
317+ Logger . Log ( LogLevel . Error , $ "Error occurred loading plugins: { ex } ") ;
318+ }
271319
272- var invalidPlugins = new HashSet < string > ( ) ;
273- var processedPlugins = new Dictionary < string , SemanticVersioning . Version > ( ) ;
274- var loadedAssemblies = new Dictionary < string , Assembly > ( ) ;
320+ Logger . Log ( LogLevel . Message , "Chainloader startup complete" ) ;
321+ }
275322
276- foreach ( var plugin in sortedPlugins )
277- {
278- var dependsOnInvalidPlugin = false ;
279- var missingDependencies = new List < BepInDependency > ( ) ;
280- foreach ( var dependency in plugin . Dependencies )
281- {
282- static bool IsHardDependency ( BepInDependency dep ) =>
283- ( dep . Flags & BepInDependency . DependencyFlags . HardDependency ) != 0 ;
284-
285- // If the dependency wasn't already processed, it's missing altogether
286- var dependencyExists =
287- processedPlugins . TryGetValue ( dependency . DependencyGUID , out var pluginVersion ) ;
288- if ( ! dependencyExists || dependency . VersionRange != null &&
289- ! dependency . VersionRange . IsSatisfied ( pluginVersion ) )
290- {
291- // If the dependency is hard, collect it into a list to show
292- if ( IsHardDependency ( dependency ) )
293- missingDependencies . Add ( dependency ) ;
294- continue ;
295- }
296-
297- // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
298- if ( invalidPlugins . Contains ( dependency . DependencyGUID ) && IsHardDependency ( dependency ) )
299- {
300- dependsOnInvalidPlugin = true ;
301- break ;
302- }
303- }
323+ private IList < PluginInfo > LoadPlugins ( IList < PluginInfo > plugins )
324+ {
325+ var sortedPlugins = ModifyLoadOrder ( plugins ) ;
304326
305- processedPlugins . Add ( plugin . Metadata . GUID , plugin . Metadata . Version ) ;
327+ var invalidPlugins = new HashSet < string > ( ) ;
328+ var processedPlugins = new Dictionary < string , SemanticVersioning . Version > ( ) ;
329+ var loadedAssemblies = new Dictionary < string , Assembly > ( ) ;
330+ var loadedPlugins = new List < PluginInfo > ( ) ;
306331
307- if ( dependsOnInvalidPlugin )
332+ foreach ( var plugin in sortedPlugins )
333+ {
334+ var dependsOnInvalidPlugin = false ;
335+ var missingDependencies = new List < BepInDependency > ( ) ;
336+ foreach ( var dependency in plugin . Dependencies )
337+ {
338+ static bool IsHardDependency ( BepInDependency dep ) =>
339+ ( dep . Flags & BepInDependency . DependencyFlags . HardDependency ) != 0 ;
340+
341+ // If the dependency wasn't already processed, it's missing altogether
342+ var dependencyExists =
343+ processedPlugins . TryGetValue ( dependency . DependencyGUID , out var pluginVersion ) ;
344+ // Alternatively, if the dependency hasn't been loaded before, it's missing too
345+ if ( ! dependencyExists )
308346 {
309- var message =
310- $ "Skipping [{ plugin } ] because it has a dependency that was not loaded. See previous errors for details.";
311- DependencyErrors . Add ( message ) ;
312- Logger . Log ( LogLevel . Warning , message ) ;
313- continue ;
347+ dependencyExists = Plugins . TryGetValue ( dependency . DependencyGUID , out var pluginInfo ) ;
348+ pluginVersion = pluginInfo ? . Metadata . Version ;
314349 }
315350
316- if ( missingDependencies . Count != 0 )
351+ if ( ! dependencyExists || dependency . VersionRange != null &&
352+ ! dependency . VersionRange . IsSatisfied ( pluginVersion ) )
317353 {
318- var message = $@ "Could not load [{ plugin } ] because it has missing dependencies: {
319- string . Join ( ", " , missingDependencies . Select ( s => s . VersionRange == null ? s . DependencyGUID : $ "{ s . DependencyGUID } ({ s . VersionRange } )") . ToArray ( ) )
320- } ";
321- DependencyErrors . Add ( message ) ;
322- Logger . Log ( LogLevel . Error , message ) ;
323-
324- invalidPlugins . Add ( plugin . Metadata . GUID ) ;
354+ // If the dependency is hard, collect it into a list to show
355+ if ( IsHardDependency ( dependency ) )
356+ missingDependencies . Add ( dependency ) ;
325357 continue ;
326358 }
327359
328- try
360+ // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
361+ if ( invalidPlugins . Contains ( dependency . DependencyGUID ) && IsHardDependency ( dependency ) )
329362 {
330- Logger . Log ( LogLevel . Info , $ "Loading [{ plugin } ]") ;
363+ dependsOnInvalidPlugin = true ;
364+ break ;
365+ }
366+ }
331367
332- if ( ! loadedAssemblies . TryGetValue ( plugin . Location , out var ass ) )
333- loadedAssemblies [ plugin . Location ] = ass = Assembly . LoadFile ( plugin . Location ) ;
368+ processedPlugins . Add ( plugin . Metadata . GUID , plugin . Metadata . Version ) ;
334369
335- Plugins [ plugin . Metadata . GUID ] = plugin ;
336- TryRunModuleCtor ( plugin , ass ) ;
337- plugin . Instance = LoadPlugin ( plugin , ass ) ;
370+ if ( dependsOnInvalidPlugin )
371+ {
372+ var message =
373+ $ "Skipping [{ plugin } ] because it has a dependency that was not loaded. See previous errors for details.";
374+ DependencyErrors . Add ( message ) ;
375+ Logger . Log ( LogLevel . Warning , message ) ;
376+ continue ;
377+ }
338378
339- //_plugins.Add((TPlugin)plugin.Instance);
340- }
341- catch ( Exception ex )
342- {
343- invalidPlugins . Add ( plugin . Metadata . GUID ) ;
344- Plugins . Remove ( plugin . Metadata . GUID ) ;
379+ if ( missingDependencies . Count != 0 )
380+ {
381+ var message = $@ "Could not load [{ plugin } ] because it has missing dependencies: {
382+ string . Join ( ", " , missingDependencies . Select ( s => s . VersionRange == null ? s . DependencyGUID : $ "{ s . DependencyGUID } ({ s . VersionRange } )") . ToArray ( ) )
383+ } ";
384+ DependencyErrors . Add ( message ) ;
385+ Logger . Log ( LogLevel . Error , message ) ;
345386
346- Logger . Log ( LogLevel . Error ,
347- $ "Error loading [{ plugin } ]: { ( ex is ReflectionTypeLoadException re ? TypeLoader . TypeLoadExceptionToString ( re ) : ex . ToString ( ) ) } ") ;
348- }
387+ invalidPlugins . Add ( plugin . Metadata . GUID ) ;
388+ continue ;
349389 }
350- }
351- catch ( Exception ex )
352- {
390+
353391 try
354392 {
355- ConsoleManager . CreateConsole ( ) ;
393+ Logger . Log ( LogLevel . Info , $ "Loading [{ plugin } ]") ;
394+
395+ if ( ! loadedAssemblies . TryGetValue ( plugin . Location , out var ass ) )
396+ loadedAssemblies [ plugin . Location ] = ass = Assembly . LoadFile ( plugin . Location ) ;
397+
398+ Plugins [ plugin . Metadata . GUID ] = plugin ;
399+ TryRunModuleCtor ( plugin , ass ) ;
400+ plugin . Instance = LoadPlugin ( plugin , ass ) ;
401+ loadedPlugins . Add ( plugin ) ;
402+
403+ //_plugins.Add((TPlugin)plugin.Instance);
356404 }
357- catch { }
405+ catch ( Exception ex )
406+ {
407+ invalidPlugins . Add ( plugin . Metadata . GUID ) ;
408+ Plugins . Remove ( plugin . Metadata . GUID ) ;
358409
359- Logger . Log ( LogLevel . Error , $ "Error occurred starting the game: { ex } ") ;
410+ Logger . Log ( LogLevel . Error ,
411+ $ "Error loading [{ plugin } ]: { ( ex is ReflectionTypeLoadException re ? TypeLoader . TypeLoadExceptionToString ( re ) : ex . ToString ( ) ) } ") ;
412+ }
360413 }
361414
362- Logger . Log ( LogLevel . Message , "Chainloader startup complete" ) ;
415+ return loadedPlugins ;
416+ }
417+
418+ /// <summary>
419+ /// Detects and loads all plugins in the specified directories.
420+ /// </summary>
421+ /// <remarks>
422+ /// It is better to collect all paths at once and use a single call to LoadPlugins than multiple calls.
423+ /// This allows to run proper dependency resolving and to load all plugins in one go.
424+ /// </remarks>
425+ /// <param name="pluginsPaths">Directories to search the plugins from.</param>
426+ /// <returns>List of loaded plugin infos.</returns>
427+ public IList < PluginInfo > LoadPlugins ( params string [ ] pluginsPaths )
428+ {
429+ // TODO: This is a temporary solution for 3rd party loaders. Instead, this should be done via metaplugins.
430+ var plugins = new List < PluginInfo > ( ) ;
431+ foreach ( var pluginsPath in pluginsPaths )
432+ plugins . AddRange ( DiscoverPluginsFrom ( pluginsPath ) ) ;
433+ return LoadPlugins ( plugins ) ;
363434 }
364435
365436 private static void TryRunModuleCtor ( PluginInfo plugin , Assembly assembly )
0 commit comments