diff --git a/BepInEx.Preloader/BepInEx.Preloader.csproj b/BepInEx.Preloader/BepInEx.Preloader.csproj
index 0f6996d..5c7cee9 100644
--- a/BepInEx.Preloader/BepInEx.Preloader.csproj
+++ b/BepInEx.Preloader/BepInEx.Preloader.csproj
@@ -48,8 +48,9 @@
+
-
+
@@ -73,5 +74,6 @@
+
\ No newline at end of file
diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs
index 8935781..1450b71 100644
--- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs
+++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Text.RegularExpressions;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
@@ -26,22 +27,14 @@ namespace BepInEx.Preloader.Patching
{
private const BindingFlags ALL = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.IgnoreCase;
- public static List PatcherPlugins { get; } = new List();
+ public static List LegacyPatcherPlugins { get; } = new List();
+ public static List PatcherPlugins { get; } = new List();
private static readonly string DumpedAssembliesPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies");
- ///
- /// Adds a single assembly patcher to the pool of applicable patches.
- ///
- /// Patcher to apply.
- public static void AddPatcher(PatcherPlugin patcher)
- {
- PatcherPlugins.Add(patcher);
- }
-
private static T CreateDelegate(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null;
- private static PatcherPlugin ToPatcherPlugin(TypeDefinition type)
+ private static LegacyPatcherPlugin ToLegacyPatcherPlugin(TypeDefinition type)
{
if (type.IsInterface || type.IsAbstract && !type.IsSealed)
return null;
@@ -65,7 +58,30 @@ namespace BepInEx.Preloader.Patching
if (patch == null)
return null;
- return new PatcherPlugin
+ return new LegacyPatcherPlugin
+ {
+ TypeName = type.FullName
+ };
+ }
+
+ private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$");
+ private static PatcherDefinition ToPatcherPlugin(TypeDefinition type)
+ {
+ if (type.IsInterface || type.IsAbstract)
+ return null;
+
+ try
+ {
+ if (!type.IsSubtypeOf(typeof(BasePatcher)))
+ return null;
+ }
+ catch (AssemblyResolutionException)
+ {
+ // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
+ return null;
+ }
+
+ return new PatcherDefinition
{
TypeName = type.FullName
};
@@ -81,25 +97,24 @@ namespace BepInEx.Preloader.Patching
if (!Directory.Exists(directory))
return;
- var sortedPatchers = new SortedDictionary();
+ var legacyPatchers = TypeLoader.FindPluginTypes(directory, ToLegacyPatcherPlugin);
+ var patchers = TypeLoader.FindPluginTypesCacheless(directory, ToPatcherPlugin);
- var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin);
-
- foreach (var keyValuePair in patchers)
+ foreach (var keyValuePair in legacyPatchers)
{
var assemblyPath = keyValuePair.Key;
var patcherCollection = keyValuePair.Value;
- if(patcherCollection.Count == 0)
+ if (patcherCollection.Count == 0)
continue;
- var ass = Assembly.LoadFile(assemblyPath);
+ var assembly = Assembly.LoadFile(assemblyPath);
foreach (var patcherPlugin in patcherCollection)
{
try
{
- var type = ass.GetType(patcherPlugin.TypeName);
+ var type = assembly.GetType(patcherPlugin.TypeName);
var methods = type.GetMethods(ALL);
@@ -129,7 +144,7 @@ namespace BepInEx.Preloader.Patching
pAss = (AssemblyDefinition)args[0];
};
- sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", patcherPlugin);
+ LegacyPatcherPlugins.Add(patcherPlugin);
}
catch (Exception e)
{
@@ -142,23 +157,111 @@ namespace BepInEx.Preloader.Patching
}
Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
- $"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}");
+ $"Loaded {patcherCollection.Count} legacy patcher methods from {assembly.GetName().FullName}");
}
- foreach (KeyValuePair patcher in sortedPatchers)
- AddPatcher(patcher.Value);
+ foreach (var keyValuePair in patchers)
+ {
+ var assemblyPath = keyValuePair.Key;
+ var patcherCollection = keyValuePair.Value;
+
+ if (patcherCollection.Count == 0)
+ continue;
+
+ var assembly = Assembly.LoadFile(assemblyPath);
+
+ foreach (var patcherPlugin in patcherCollection)
+ {
+ try
+ {
+ var type = assembly.GetType(patcherPlugin.TypeName);
+
+ patcherPlugin.PatcherInfo = (PatcherInfo)type.GetCustomAttributes(typeof(PatcherInfo), false).FirstOrDefault();
+
+ if (patcherPlugin.PatcherInfo == null)
+ {
+ Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
+ continue;
+ }
+
+ if (string.IsNullOrEmpty(patcherPlugin.PatcherInfo.GUID) || !allowedGuidRegex.IsMatch(patcherPlugin.PatcherInfo.GUID))
+ {
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{patcherPlugin.PatcherInfo.GUID}] is of an illegal format.");
+ continue;
+ }
+
+ if (patcherPlugin.PatcherInfo.Version == null)
+ {
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid.");
+ continue;
+ }
+
+ if (patcherPlugin.PatcherInfo.Name == null)
+ {
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null.");
+ continue;
+ }
+
+ var methods = type.GetMethods(ALL);
+
+ foreach (var method in methods)
+ {
+ var parameters = method.GetParameters();
+
+ if (parameters.Length == 1 && parameters[0].ParameterType == typeof(AssemblyDefinition))
+ {
+ var targetAssemblyAttributes = method.GetCustomAttributes(typeof(TargetAssemblyAttribute), false);
+
+ foreach (var attribute in targetAssemblyAttributes)
+ {
+ patcherPlugin.AssemblyDefinitionPatchers[(TargetAssemblyAttribute)attribute] = method;
+ }
+ }
+ else if (parameters.Length == 1 && parameters[0].ParameterType == typeof(TypeDefinition))
+ {
+ var targetClassAttributes = method.GetCustomAttributes(typeof(TargetClassAttribute), false);
+
+ foreach (var attribute in targetClassAttributes)
+ {
+ patcherPlugin.TypeDefinitionPatchers[(TargetClassAttribute)attribute] = method;
+ }
+ }
+ }
+
+ patcherPlugin.Instance = (BasePatcher)Activator.CreateInstance(type);
+
+ Logger.LogInfo($"Loaded patcher [{patcherPlugin.PatcherInfo.Name} {patcherPlugin.PatcherInfo.Version}]");
+
+ PatcherPlugins.Add(patcherPlugin);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Failed to load patcher [{patcherPlugin.TypeName}]: {e.Message}");
+ if (e is ReflectionTypeLoadException re)
+ Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
+ else
+ Logger.LogDebug(e.ToString());
+ }
+ }
+ }
}
- private static void InitializePatchers()
+ private static void InitializePatchers(PatchContext context)
{
- foreach (var assemblyPatcher in PatcherPlugins)
+ foreach (var assemblyPatcher in LegacyPatcherPlugins)
assemblyPatcher.Initializer?.Invoke();
+
+ foreach (var patcher in PatcherPlugins)
+ patcher.Instance.Setup(context);
}
private static void FinalizePatching()
{
- foreach (var assemblyPatcher in PatcherPlugins)
+ foreach (var assemblyPatcher in LegacyPatcherPlugins)
assemblyPatcher.Finalizer?.Invoke();
+
+ foreach (var patcher in PatcherPlugins)
+ patcher.Instance.Finalize();
}
///
@@ -166,7 +269,7 @@ namespace BepInEx.Preloader.Patching
///
public static void DisposePatchers()
{
- PatcherPlugins.Clear();
+ LegacyPatcherPlugins.Clear();
}
///
@@ -176,7 +279,8 @@ namespace BepInEx.Preloader.Patching
public static void PatchAndLoad(string directory)
{
// First, load patchable assemblies into Cecil
- var assemblies = new Dictionary();
+ var assemblies = new List();
+ var filenameDictionary = new Dictionary();
foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
{
@@ -197,30 +301,82 @@ namespace BepInEx.Preloader.Patching
Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
continue;
}
-
- assemblies.Add(Path.GetFileName(assemblyPath), assembly);
+
+ assemblies.Add(assembly);
+ filenameDictionary[Path.GetFileName(assemblyPath)] = assembly;
UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
}
+ // Create the patch context
+ PatchContext context = new PatchContext
+ {
+ LoadedPatchers = PatcherPlugins.AsReadOnly(),
+ AvailableAssemblies = assemblies,
+ AssemblyLocations = filenameDictionary
+ };
+
// Next, initialize all the patchers
- InitializePatchers();
+ InitializePatchers(context);
// Then, perform the actual patching
+
+ // Legacy patchers
var patchedAssemblies = new HashSet();
- foreach (var assemblyPatcher in PatcherPlugins)
+ foreach (var assemblyPatcher in LegacyPatcherPlugins)
foreach (string targetDll in assemblyPatcher.TargetDLLs())
- if (assemblies.TryGetValue(targetDll, out var assembly))
+ if (filenameDictionary.TryGetValue(targetDll, out var assembly))
{
if (AppDomain.CurrentDomain.GetAssemblies().Any(x => x.GetName().Name == assembly.Name.Name))
Logger.LogWarning($"Trying to patch an already loaded assembly [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]");
Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]");
+ var oldAssembly = assembly;
+
assemblyPatcher.Patcher?.Invoke(ref assembly);
- assemblies[targetDll] = assembly;
+
+ if (!ReferenceEquals(oldAssembly, assembly))
+ {
+ assemblies.Remove(oldAssembly);
+ assemblies.Add(assembly);
+ }
+
+ filenameDictionary[targetDll] = assembly;
patchedAssemblies.Add(targetDll);
}
+ // V2 patchers
+
+ foreach (var patcherDefinition in PatcherPlugins)
+ {
+ patcherDefinition.Instance.PatchAll(assemblies);
+ }
+
+ foreach (var patcherDefinition in PatcherPlugins)
+ {
+ if (patcherDefinition.AssemblyDefinitionPatchers != null)
+ foreach (var assemblyPatchDefinition in patcherDefinition.AssemblyDefinitionPatchers)
+ {
+ var assemblyDefinition = assemblies.FirstOrDefault(x => x.Name.Name == assemblyPatchDefinition.Key.AssemblyName);
+ patchedAssemblies.Add(filenameDictionary.First(x => x.Value == assemblyDefinition).Key);
+
+ assemblyPatchDefinition.Value.Invoke(patcherDefinition.Instance, new object[] { assemblyDefinition });
+ }
+ }
+
+ foreach (var patcherDefinition in PatcherPlugins)
+ {
+ if (patcherDefinition.TypeDefinitionPatchers != null)
+ foreach (var typePatchDefinition in patcherDefinition.TypeDefinitionPatchers)
+ {
+ var assemblyDefinition = assemblies.FirstOrDefault(x => x.Name.Name == typePatchDefinition.Key.AssemblyName);
+ patchedAssemblies.Add(filenameDictionary.First(x => x.Value == assemblyDefinition).Key);
+
+ var typeDefinition = assemblyDefinition?.MainModule.Types.FirstOrDefault(x => x.FullName == typePatchDefinition.Key.ClassName);
+
+ typePatchDefinition.Value.Invoke(patcherDefinition.Instance, new object[] { typeDefinition });
+ }
+ }
// Finally, load patched assemblies into memory
if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value)
@@ -228,7 +384,7 @@ namespace BepInEx.Preloader.Patching
if (!Directory.Exists(DumpedAssembliesPath))
Directory.CreateDirectory(DumpedAssembliesPath);
- foreach (KeyValuePair kv in assemblies)
+ foreach (KeyValuePair kv in filenameDictionary)
{
string filename = kv.Key;
var assembly = kv.Value;
@@ -246,7 +402,7 @@ namespace BepInEx.Preloader.Patching
Debugger.Break();
}
- foreach (var kv in assemblies)
+ foreach (var kv in filenameDictionary)
{
string filename = kv.Key;
var assembly = kv.Value;
diff --git a/BepInEx.Preloader/Patching/Attributes.cs b/BepInEx.Preloader/Patching/Attributes.cs
new file mode 100644
index 0000000..904aa1f
--- /dev/null
+++ b/BepInEx.Preloader/Patching/Attributes.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace BepInEx.Preloader
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class TargetAssemblyAttribute : Attribute
+ {
+ public string AssemblyName { get; }
+
+ public TargetAssemblyAttribute(string assemblyName)
+ {
+ AssemblyName = assemblyName;
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class TargetClassAttribute : Attribute
+ {
+ public string AssemblyName { get; }
+ public string ClassName { get; }
+
+ public TargetClassAttribute(string assemblyName, string className)
+ {
+ AssemblyName = assemblyName;
+ ClassName = className;
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Class)]
+ public class PatcherInfo : Attribute
+ {
+ public string GUID { get; }
+ public string Name { get; }
+ public Version Version { get; }
+
+ public PatcherInfo(string guid, string name, string version)
+ {
+ GUID = guid;
+ Name = name;
+ Version = new Version(version);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BepInEx.Preloader/Patching/BasePatcher.cs b/BepInEx.Preloader/Patching/BasePatcher.cs
new file mode 100644
index 0000000..a3dabe7
--- /dev/null
+++ b/BepInEx.Preloader/Patching/BasePatcher.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using BepInEx.Preloader.Patching;
+using Mono.Cecil;
+
+namespace BepInEx.Preloader
+{
+ public abstract class BasePatcher
+ {
+ private PatchContext _patchContext;
+ protected PatchContext PatchContext => _patchContext;
+
+ public virtual void Setup(PatchContext patchContext)
+ {
+ _patchContext = patchContext;
+ }
+
+ public virtual void PatchAll(IList assemblyDefinitions)
+ {
+
+ }
+
+ public virtual void Finalize()
+ {
+
+ }
+
+ [TargetAssembly(assemblyName: "Terraria")]
+ public void Patch(AssemblyDefinition assemblyDefinition)
+ {
+
+ }
+ }
+}
diff --git a/BepInEx.Preloader/Patching/PatchContext.cs b/BepInEx.Preloader/Patching/PatchContext.cs
new file mode 100644
index 0000000..223fd0e
--- /dev/null
+++ b/BepInEx.Preloader/Patching/PatchContext.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Reflection;
+using BepInEx.Bootstrap;
+using Mono.Cecil;
+
+namespace BepInEx.Preloader.Patching
+{
+ public class PatchContext
+ {
+ public ReadOnlyCollection LoadedPatchers { get; internal set; }
+
+ ///
+ /// A read-only list of legacy patchers that are loaded. Will be removed in a future release.
+ ///
+ [Obsolete("Will be removed in a future release.", false)]
+ public IList LoadedLegacyPatchers { get; internal set; }
+
+ public IList AvailableAssemblies { get; internal set; }
+
+ public Dictionary AssemblyLocations { get; internal set; }
+
+ internal PatchContext() { }
+ }
+
+ ///
+ /// A preloader assembly patcher.
+ ///
+ public class PatcherDefinition
+ {
+ public PatcherInfo PatcherInfo { get; internal set; }
+
+ public string TypeName { get; internal set; }
+ public BasePatcher Instance { get; internal set; }
+
+ public Dictionary AssemblyDefinitionPatchers { get; internal set; } = new Dictionary();
+
+ public Dictionary TypeDefinitionPatchers { get; internal set; } = new Dictionary();
+
+ internal PatcherDefinition() { }
+ }
+
+ ///
+ /// A single assembly patcher, based on the old reflection contract.
+ ///
+ internal class LegacyPatcherPlugin : ICacheable
+ {
+ ///
+ /// Target assemblies to patch.
+ ///
+ public Func> TargetDLLs { get; set; } = null;
+
+ ///
+ /// Initializer method that is run before any patching occurs.
+ ///
+ public Action Initializer { get; set; } = null;
+
+ ///
+ /// Finalizer method that is run after all patching is done.
+ ///
+ public Action Finalizer { get; set; } = null;
+
+ ///
+ /// The main patcher method that is called on every DLL defined in .
+ ///
+ public AssemblyPatcherDelegate Patcher { get; set; } = null;
+
+ ///
+ /// Type name of the patcher.
+ ///
+ public string TypeName { get; set; } = string.Empty;
+
+ public void Save(BinaryWriter bw)
+ {
+ bw.Write(TypeName);
+ }
+
+ public void Load(BinaryReader br)
+ {
+ TypeName = br.ReadString();
+ }
+ }
+}
diff --git a/BepInEx.Preloader/Patching/PatcherPlugin.cs b/BepInEx.Preloader/Patching/PatcherPlugin.cs
deleted file mode 100644
index 390cc88..0000000
--- a/BepInEx.Preloader/Patching/PatcherPlugin.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using BepInEx.Bootstrap;
-
-namespace BepInEx.Preloader.Patching
-{
- ///
- /// A single assembly patcher.
- ///
- internal class PatcherPlugin : ICacheable
- {
- ///
- /// Target assemblies to patch.
- ///
- public Func> TargetDLLs { get; set; } = null;
-
- ///
- /// Initializer method that is run before any patching occurs.
- ///
- public Action Initializer { get; set; } = null;
-
- ///
- /// Finalizer method that is run after all patching is done.
- ///
- public Action Finalizer { get; set; } = null;
-
- ///
- /// The main patcher method that is called on every DLL defined in .
- ///
- public AssemblyPatcherDelegate Patcher { get; set; } = null;
-
- ///
- /// Type name of the patcher.
- ///
- public string TypeName { get; set; } = string.Empty;
-
- public void Save(BinaryWriter bw)
- {
- bw.Write(TypeName);
- }
-
- public void Load(BinaryReader br)
- {
- TypeName = br.ReadString();
- }
- }
-}
\ No newline at end of file
diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs
index d6f6c69..9d4867e 100644
--- a/BepInEx.Preloader/Preloader.cs
+++ b/BepInEx.Preloader/Preloader.cs
@@ -79,7 +79,7 @@ namespace BepInEx.Preloader
Logger.LogMessage("Preloader started");
- AssemblyPatcher.AddPatcher(new PatcherPlugin
+ AssemblyPatcher.LegacyPatcherPlugins.Add(new LegacyPatcherPlugin
{
TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
Patcher = PatchEntrypoint,
@@ -88,7 +88,7 @@ namespace BepInEx.Preloader
AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
- Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");
+ Logger.LogInfo($"{AssemblyPatcher.LegacyPatcherPlugins.Count + AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");
AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
AssemblyPatcher.DisposePatchers();
diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs
index 9a4b865..d6f3dc0 100644
--- a/BepInEx/Bootstrap/TypeLoader.cs
+++ b/BepInEx/Bootstrap/TypeLoader.cs
@@ -104,18 +104,18 @@ namespace BepInEx.Bootstrap
}
}
- var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters);
+ var assembly = AssemblyDefinition.ReadAssembly(dll, readerParameters);
- if (!assemblyFilter?.Invoke(ass) ?? false)
+ if (!assemblyFilter?.Invoke(assembly) ?? false)
{
result[dll] = new List();
- ass.Dispose();
+ assembly.Dispose();
continue;
}
- var matches = ass.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList();
+ var matches = assembly.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList();
result[dll] = matches;
- ass.Dispose();
+ assembly.Dispose();
}
catch (Exception e)
{
@@ -128,6 +128,43 @@ namespace BepInEx.Bootstrap
return result;
}
+ ///
+ /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata.
+ ///
+ /// The specific base type to search for.
+ /// The directory to search for assemblies.
+ /// A function to check if a type should be selected and to build the type metadata.
+ /// A filter function to quickly determine if the assembly can be loaded.
+ /// The name of the cache to get cached types from.
+ /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the selector.
+ public static Dictionary> FindPluginTypesCacheless(string directory, Func typeSelector, Func assemblyFilter = null)
+ {
+ var result = new Dictionary>();
+
+ foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories))
+ try
+ {
+ var assembly = AssemblyDefinition.ReadAssembly(dll, readerParameters);
+
+ if (!assemblyFilter?.Invoke(assembly) ?? false)
+ {
+ result[dll] = new List();
+ assembly.Dispose();
+ continue;
+ }
+
+ var matches = assembly.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList();
+ result[dll] = matches;
+ assembly.Dispose();
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e.ToString());
+ }
+
+ return result;
+ }
+
///
/// Loads an index of type metadatas from a cache.
///