diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs
index b7a2ffe2e..1a6830a3a 100644
--- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs
+++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs
@@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder
/// or the default app context.
///
/// The name of the assembly.
+ /// Pull assmeblies recursively.
/// The builder.
- public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
+ public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName, bool recursive)
{
+ if (!recursive)
+ {
+ this.defaultAssemblies.Add(assemblyName.Name);
+ return this;
+ }
+
var names = new Queue(new[] { assemblyName });
while (names.TryDequeue(out var name))
diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs
index d3fcdc99e..0b2150069 100644
--- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs
+++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs
@@ -46,7 +46,7 @@ internal class LoaderConfig
/// Gets a list of assemblies which should be unified between the host and the plugin.
///
/// what-are-shared-types
- public ICollection SharedAssemblies { get; } = new List();
+ public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>();
///
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.
diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs
index 4bb326ce4..e0629217a 100644
--- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs
+++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs
@@ -194,7 +194,18 @@ internal class ManagedLoadContext : AssemblyLoadContext
}
}
- return null;
+ // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed#algorithm
+ // > These assemblies are loaded (load-by-name) as needed by the runtime.
+ // For load-by-name assembiles, the following will happen in order:
+ // (1) this.Load will be called.
+ // (2) AssemblyLoadContext.Default's cache will be referred for lookup.
+ // (3) Default probing will be done from PLATFORM_RESOURCE_ROOTS and APP_PATHS.
+ // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing
+ // > TRUSTED_PLATFORM_ASSEMBLIES: List of platform and application assembly file paths.
+ // > APP_PATHS: is not populated by default and is omitted for most applications.
+ // If we return null here, if the assembly has not been already loaded, the resolution will fail.
+ // Therefore as the final attempt, we try loading from the default load context.
+ return this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
}
///
diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs
index 53aec60ef..63b47cf17 100644
--- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs
+++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs
@@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable
builder.ShadowCopyNativeLibraries();
}
- foreach (var assemblyName in config.SharedAssemblies)
+ foreach (var (assemblyName, recursive) in config.SharedAssemblies)
{
- builder.PreferDefaultLoadContextAssembly(assemblyName);
+ builder.PreferDefaultLoadContextAssembly(assemblyName, recursive);
}
- // This allows plugins to search for dependencies in the Dalamud directory when their assembly
- // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud
- // itself yet.
- builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
-
- // Also make sure that plugins do not load their own Dalamud assembly.
- builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName());
+ // Note: not adding Dalamud path here as a probing path.
+ // It will be dealt as the last resort from ManagedLoadContext.Load.
+ // See there for more details.
return builder;
}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
index aff9a8b43..0ddd4b23e 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = false;
- config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
- config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
+
+ // Pin Lumina and its dependencies recursively (compatibility behavior).
+ // It currently only pulls in System.* anyway.
+ // TODO(api10): Remove this. We don't want to pin Lumina anymore, plugins should be able to provide their own.
+ config.SharedAssemblies.Add((typeof(Lumina.GameData).Assembly.GetName(), true));
+ config.SharedAssemblies.Add((typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName(), true));
+
+ // Make sure that plugins do not load their own Dalamud assembly.
+ // We do not pin this recursively; if a plugin loads its own assembly of Dalamud, it is always wrong,
+ // but plugins may load other versions of assemblies that Dalamud depends on.
+ config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false));
+ config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false));
}
private void EnsureLoader()