From 9d00a91eb33565fc07b8542e74f06c48d8f70136 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:35:39 +0100 Subject: [PATCH 1/4] Inherit patcher types and attributes --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 2 +- BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs | 12 ++++++++---- BepInEx.Preloader.Core/Patching/Attributes.cs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 3aa6d92d5..e85999fee 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -88,7 +88,7 @@ public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocati Location = assemblyLocation }; } - static bool ReferencesThisAssembly(AssemblyDefinition ass, HashSet seen = null) + internal static bool ReferencesThisAssembly(AssemblyDefinition ass, HashSet seen = null) { seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 4ba5396c0..8cb90b5ff 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -65,6 +65,7 @@ public void Dispose() private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) { + if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; @@ -115,12 +116,15 @@ private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemb private bool HasPatcherPlugins(AssemblyDefinition ass) { - if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) && - ass.Name.Name != CurrentAssemblyName) - return false; + if (!BepInEx.Bootstrap.BaseChainloader.ReferencesThisAssembly(ass)) return false; if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName)) + { + if (ass.MainModule.GetTypeReferences().Any((TypeReference r) => MetadataHelper.TypeInheretsFrom(r, typeof(BasePatcher)))) + { + return true; + } return false; - + } return true; } diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index c8c76c138..d8e31108b 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -58,7 +58,7 @@ private static Version TryParseLongVersion(string version) internal static PatcherPluginInfoAttribute FromCecilType(TypeDefinition td) { - var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); + var attr = MetadataHelper.GetCustomAttributes(td, false, true).FirstOrDefault(); if (attr == null) return null; From 88e4fd8656f0a3231ac1786aecc434fa2b783841 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:36:36 +0100 Subject: [PATCH 2/4] Remove empty line --- BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 8cb90b5ff..9df4935a9 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -65,7 +65,6 @@ public void Dispose() private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) { - if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; From 9b32f502e73b08e10c5d0c69e42f9b50a0f6fcd7 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 15 Sep 2026 16:15:23 +0200 Subject: [PATCH 3/4] fix: harden inherited patcher-type discovery Signed-off-by: hazre --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 2 +- BepInEx.Core/Contract/Attributes.cs | 19 +++++++++++++++++-- .../Patching/AssemblyPatcher.cs | 12 +++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index e85999fee..3aa6d92d5 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -88,7 +88,7 @@ public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocati Location = assemblyLocation }; } - internal static bool ReferencesThisAssembly(AssemblyDefinition ass, HashSet seen = null) + static bool ReferencesThisAssembly(AssemblyDefinition ass, HashSet seen = null) { seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index f8f813bc6..647e87e42 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -254,12 +254,27 @@ public static class MetadataHelper { internal static bool TypeInheretsFrom(TypeReference derived, Type type) { - var td = derived.Resolve(); + TypeDefinition td; + try + { + td = derived.Resolve(); + } + catch (AssemblyResolutionException) + { + return false; + } while (td != null) { if (td.FullName == type.FullName) return true; - td = td.BaseType?.Resolve(); + try + { + td = td.BaseType?.Resolve(); + } + catch (AssemblyResolutionException) + { + return false; + } } return false; } diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 9df4935a9..a8058e27a 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -20,8 +20,6 @@ namespace BepInEx.Preloader.Core.Patching; /// public class AssemblyPatcher : IDisposable { - private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; - private Func assemblyLoader; public AssemblyPatcher(Func assemblyLoader) @@ -115,14 +113,10 @@ private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemb private bool HasPatcherPlugins(AssemblyDefinition ass) { - if (!BepInEx.Bootstrap.BaseChainloader.ReferencesThisAssembly(ass)) return false; - if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName)) + var typeReferences = ass.MainModule.GetTypeReferences().ToList(); + if (typeReferences.All(r => r.FullName != typeof(BasePatcher).FullName)) { - if (ass.MainModule.GetTypeReferences().Any((TypeReference r) => MetadataHelper.TypeInheretsFrom(r, typeof(BasePatcher)))) - { - return true; - } - return false; + return typeReferences.Any(r => MetadataHelper.TypeInheretsFrom(r, typeof(BasePatcher))); } return true; } From e8e40b553eb381055c7de62bbf40d5146c453553 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 15 Sep 2026 18:13:25 +0200 Subject: [PATCH 4/4] feat: resolve renamed patcher assemblies by assembly name Signed-off-by: hazre --- BepInEx.Core/Bootstrap/TypeLoader.cs | 41 +++++++++++++++++++ .../Patching/AssemblyPatcher.cs | 2 + .../BepInEx.NET.Shared/SharedEntrypoint.cs | 9 ++++ 3 files changed, 52 insertions(+) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 8c4b9b8a8..175ebaba1 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -63,6 +63,38 @@ public static class TypeLoader public static HashSet SearchDirectories = new(); + private static readonly Dictionary AssemblyPathByName = new(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Maps every managed assembly file in a directory to its assembly name, so files whose + /// filename does not match the assembly name (e.g. renamed by the user) still resolve. + /// First file wins when two files share an assembly name. + /// + public static void RegisterAssemblyPaths(string directory) + { + if (!Directory.Exists(directory)) + return; + + foreach (var dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) + { + AssemblyName name; + try + { + name = AssemblyName.GetAssemblyName(dll); + } + catch (Exception) + { + continue; + } + + if (!AssemblyPathByName.ContainsKey(name.Name)) + AssemblyPathByName[name.Name] = dll; + } + } + + public static bool TryGetRegisteredAssemblyPath(string name, out string path) => + AssemblyPathByName.TryGetValue(name, out path) && File.Exists(path); + #region Config private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind( @@ -104,6 +136,15 @@ public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNa return assembly; } + if (TryGetRegisteredAssemblyPath(name.Name, out var mappedPath)) + { + try + { + return AssemblyDefinition.ReadAssembly(mappedPath, ReaderParameters); + } + catch (BadImageFormatException) { } + } + return AssemblyResolve?.Invoke(sender, reference); } diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index a8058e27a..a6fc099cf 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -132,6 +132,8 @@ public void AddPatchersFromDirectory(string directory) var sortedPatchers = new List(); + TypeLoader.RegisterAssemblyPaths(directory); + var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins); // TODO: Add dependency ordering and process attribute filtering diff --git a/Runtimes/NET/BepInEx.NET.Shared/SharedEntrypoint.cs b/Runtimes/NET/BepInEx.NET.Shared/SharedEntrypoint.cs index 638ba9dab..09a163515 100644 --- a/Runtimes/NET/BepInEx.NET.Shared/SharedEntrypoint.cs +++ b/Runtimes/NET/BepInEx.NET.Shared/SharedEntrypoint.cs @@ -78,6 +78,15 @@ public static Assembly LocalResolve(object sender, ResolveEventArgs args) || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) return foundAssembly; + if (Bootstrap.TypeLoader.TryGetRegisteredAssemblyPath(assemblyName.Name, out var mappedPath)) + { + try + { + return Utility.LoadContext.LoadFromAssemblyPath(mappedPath); + } + catch (Exception) { } + } + return null; } }